diff --git a/common-contract/pdo/contracts/guardian/common/guardian_service.py b/common-contract/pdo/contracts/guardian/common/guardian_service.py index 3cc78f7..b5d3a55 100644 --- a/common-contract/pdo/contracts/guardian/common/guardian_service.py +++ b/common-contract/pdo/contracts/guardian/common/guardian_service.py @@ -91,8 +91,11 @@ def __post_request__(self, path, request) : response.raise_for_status() return response.json() - except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : - logger.warn('network error connecting to service (%s); %s', path, str(e)) + except requests.HTTPError as he : + logger.warning('HTTP error [%s]; %s, %s', path, he.response.status_code, he.response.text.strip()) + raise MessageException(f'HTTP error [{he.response.status_code}]: {he.response.text.strip()}') from he + except (requests.ConnectionError, requests.Timeout) as e : + logger.warning('network error connecting to service (%s); %s', path, str(e)) raise MessageException(str(e)) from e # ----------------------------------------------------------------- @@ -111,7 +114,10 @@ def __get_request__(self, path) : response.raise_for_status() return response.json() - except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : + except requests.HTTPError as he : + logger.warn('HTTP error [%s]; %s, %s', path, he.response.status_code, he.response.text.strip()) + raise MessageException(f'HTTP error [{he.response.status_code}]: {he.response.text.strip()}') from he + except (requests.ConnectionError, requests.Timeout) as e : logger.warn('network error connecting to service (%s); %s', path, str(e)) raise MessageException(str(e)) from e diff --git a/common-contract/pdo/contracts/guardian/scripts/guardianCLI.py b/common-contract/pdo/contracts/guardian/scripts/guardianCLI.py index b26a6ae..41c1442 100644 --- a/common-contract/pdo/contracts/guardian/scripts/guardianCLI.py +++ b/common-contract/pdo/contracts/guardian/scripts/guardianCLI.py @@ -47,29 +47,6 @@ from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.web.wsgi import WSGIResource -## ---------------------------------------------------------------- -def ErrorResponse(request, error_code, msg) : - """Generate a common error response for broken requests - """ - - result = "" - if request.method != 'HEAD' : - result = msg + '\n' - result = result.encode('utf8') - - request.setResponseCode(error_code) - request.setHeader(b'Content-Type', b'text/plain') - request.setHeader(b'Content-Length', len(result)) - request.write(result) - - try : - request.finish() - except : - logger.exception("exception during request finish") - raise - - return request - # ----------------------------------------------------------------- # ----------------------------------------------------------------- def __shutdown__(*args) : diff --git a/common-contract/pdo/contracts/guardian/wsgi/process_capability.py b/common-contract/pdo/contracts/guardian/wsgi/process_capability.py index d8080a8..39c757d 100644 --- a/common-contract/pdo/contracts/guardian/wsgi/process_capability.py +++ b/common-contract/pdo/contracts/guardian/wsgi/process_capability.py @@ -44,17 +44,21 @@ class ProcessCapabilityApp(object) : "session_key_iv" : { "type" : "string" }, "encrypted_message" : { "type" : "string" }, }, + "required" : [ "encrypted_session_key", "session_key_iv", "encrypted_message" ] }, - } + }, + "required" : [ "minted_identity", "operation" ], } __operation_schema__ = { "type" : "object", "properties" : { "nonce" : { "type" : "string" }, + "request_identifier" : { "type" : "string" }, "method_name" : { "type" : "string" }, "parameters" : { "type" : "object" }, - } + }, + "required" : [ "nonce", "method_name", "parameters" ], } # ----------------------------------------------------------------- @@ -62,6 +66,7 @@ def __init__(self, config, capability_store, endpoint_registry) : self.config = config self.capability_store = capability_store self.endpoint_registry = endpoint_registry + self.request_registry = {} # map of sets, used to check for duplicate requests try : operation_module_name = config['GuardianService']['Operations'] @@ -81,39 +86,62 @@ def __call__(self, environ, start_response) : try : request = UnpackJSONRequest(environ) if not ValidateJSON(request, self.__input_schema__) : - return ErrorResponse(start_response, "invalid JSON") + return ErrorResponse(start_response, "invalid JSON, malformed request") - capability_key = self.capability_store.get_capability_key(request['minted_identity']) + minted_identity = request['minted_identity'] + capability_key = self.capability_store.get_capability_key(minted_identity) operation_message = recv_secret(capability_key, request['operation']) if not ValidateJSON(operation_message, self.__operation_schema__) : - return ErrorResponse(start_response, "invalid JSON") + return ErrorResponse(start_response, "invalid JSON, malformed operation") except KeyError as ke : - logger.error(f'missing field in request: {ke}') + logger.info(f'missing field in request: {ke}') return ErrorResponse(start_response, f'missing field in request: {ke}') except Exception as e : logger.error(f'unknown exception unpacking request (ProcessCapability); {e}') return ErrorResponse(start_response, "unknown exception while unpacking request") - # dispatch the operation - try : - method_name = operation_message['method_name'] - parameters = operation_message['parameters'] - except KeyError as ke : - logger.error(f'missing field {ke}') - return ErrorResponse(start_response, f'missing field {ke}') + # find the operation, we've already validated the JSON so no errors here + method_name = operation_message['method_name'] + parameters = operation_message['parameters'] logger.info("process capability operation %s with parameters %s", method_name, parameters) try : operation = self.capability_handler_map[method_name] + except KeyError as ke : + logger.info(f'unknown operation {ke}') + return ErrorResponse(start_response, f'unknown operation {ke}', HTTPStatus.NOT_FOUND) + + # check for request replays + try : + if hasattr(operation, 'unique_requests') and operation.unique_requests is True : + request_identifier = operation_message.get('request_identifier') + if request_identifier is None : + logger.info('missing request identifier for unique operation') + return ErrorResponse(start_response, "missing request identifier for unique operation") + + # add the minted identity to the registry if it does not exist + if self.request_registry.get(minted_identity) is None : + self.request_registry[minted_identity] = set() + + # check if the request identifier is already in the registry for this minted identity + if request_identifier in self.request_registry[minted_identity] : + logger.info('duplicate request for unique operation') + return ErrorResponse(start_response, 'duplicate request for unique operation', HTTPStatus.UNAUTHORIZED) + + # add the request identifier to the registry for this minted identity + self.request_registry[minted_identity].add(request_identifier) + except Exception as e : + logger.error(f'unexpected error checking for duplicate request; {e}') + return ErrorResponse(start_response, "unexpected error checking for duplicate request") + + # dispatch the operation + try : operation_result = operation(parameters) if operation_result is None : - return ErrorResponse(start_response, "operation failed") - except KeyError as ke : - logger.error(f'unknown operation {ke}') - return ErrorResponse(start_response, f'unknown operation {ke}') + return ErrorResponse(start_response, "operation failed", HTTPStatus.UNPROCESSABLE_ENTITY) except Exception as e : logger.error(f'unknown exception performing operation (ProcessCapability); {e}') return ErrorResponse(start_response, "unknown exception while performing operation") diff --git a/exchange-contract/exchange/contracts/token_object.cpp b/exchange-contract/exchange/contracts/token_object.cpp index eafd407..8c482a2 100644 --- a/exchange-contract/exchange/contracts/token_object.cpp +++ b/exchange-contract/exchange/contracts/token_object.cpp @@ -67,17 +67,25 @@ bool ww::exchange::token_object::get_token_metadata( ERROR_IF_NOT(deserialized_token_metadata.deserialize(serialized_token_metadata.c_str()), "unexpected error: failed to deserialize token metadata"); - ww::value::Object token_metadata_schema; - ERROR_IF_NOT(token_metadata_schema.deserialize(schema.c_str()), - "unexpected error: failed to deserialize token metadata schema"); - - ERROR_IF_NOT(deserialized_token_metadata.validate_schema(token_metadata_schema), + ERROR_IF_NOT(deserialized_token_metadata.validate_schema(schema.c_str()), "unexpected error: token metadata does not match schema"); token_metadata.set(deserialized_token_metadata); return true; } +// ----------------------------------------------------------------- +// get_token_identity +// +// Return the minted identity for this token object. +// ----------------------------------------------------------------- +bool ww::exchange::token_object::get_token_identity( + std::string& token_identity) +{ + ERROR_IF_NOT(token_object_store.get(minted_identity_key, token_identity), + "unexpected error: failed to get minted identity"); + return true; +} // ----------------------------------------------------------------- // METHOD: initialize_contract @@ -463,6 +471,24 @@ bool ww::exchange::token_object::create_operation_package( const std::string& method_name, const ww::value::Object& parameters, ww::value::Object& capability_result) +{ + // No request identifier is specified so we'll generate a new one + ww::types::ByteArray identifier_raw; + if (! ww::crypto::random_identifier(identifier_raw)) + return false; + + std::string identifier; + if (! ww::crypto::b64_encode(identifier_raw, identifier)) + return false; + + return create_operation_package(identifier, method_name, parameters, capability_result); +} + +bool ww::exchange::token_object::create_operation_package( + const std::string& request_identifier, + const std::string& method_name, + const ww::value::Object& parameters, + ww::value::Object& capability_result) { // Create the operation package, this will be the message in the // secret that we are going to create for the capability @@ -479,6 +505,9 @@ bool ww::exchange::token_object::create_operation_package( if (! operation.set_string("nonce", nonce.c_str())) return false; + if (! operation.set_string("request_identifier", request_identifier.c_str())) + return false; + if (! operation.set_string("method_name", method_name.c_str())) return false; diff --git a/exchange-contract/exchange/token_object.h b/exchange-contract/exchange/token_object.h index 00b04ee..0d3a6a9 100644 --- a/exchange-contract/exchange/token_object.h +++ b/exchange-contract/exchange/token_object.h @@ -48,6 +48,7 @@ #define TO_OPERATION_SCHEMA \ "{" \ SCHEMA_KW(nonce,"") "," \ + SCHEMA_KW(request_identifier,"") "," \ SCHEMA_KW(method_name, "") "," \ SCHEMA_KWS(parameters, "{}") \ "}" @@ -71,6 +72,13 @@ namespace token_object // utility functions bool initialize_contract(const Environment& env); + + bool create_operation_package( + const std::string& request_identifier, + const std::string& method_name, + const ww::value::Object& parameters, + ww::value::Object& capability_result); + bool create_operation_package( const std::string& method_name, const ww::value::Object& parameters, @@ -80,6 +88,9 @@ namespace token_object const std::string& schema, ww::value::Object& token_metadata); + bool get_token_identity( + std::string& token_identity); + }; // token_object }; // exchange }; // ww