diff --git a/.env.example b/.env.example index c43eb8b7..13665fa7 100644 --- a/.env.example +++ b/.env.example @@ -8,13 +8,13 @@ AWS_SUBNET_ID_3= API_PORT=3001 LAMBDA_INVOKE_URL=http://localhost:3002/ DATA_AGGREGATOR_LAMBDA_NAME=hathor-explorer-service-dev-node_data_aggregator_handler -HATHOR_CORE_DOMAIN=node1.foxtrot.testnet.hathor.network -HATHOR_NODES=node1.foxtrot.testnet.hathor.network +HATHOR_CORE_DOMAIN=node1.golf.testnet.hathor.network +HATHOR_NODES=node1.golf.testnet.hathor.network REDIS_KEY_PREFIX=hathor-explorer-service-dev REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 S3_ENDPOINT=http://localhost:4569/ METADATA_BUCKET=metadata-testnet -CORS_ALLOWED_ORIGIN="" +CORS_ALLOWED_REGEX=https? NODE_CACHE_TTL=30 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0bbdb0f3..46f11922 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,7 +81,7 @@ jobs: REDIS_PORT: 6379 REDIS_DB: 0 METADATA_BUCKET: hathor-explorer-metadata-testnet - CORS_ALLOWED_ORIGIN: https://explorer.testnet.hathor.network + CORS_ALLOWED_REGEX: https?://([a-z0-9]*\.){0,5}hathor\.network NODE_CACHE_TTL: 30 - name: Deploy Lambdas Mainnet if: ${{ needs.init.outputs.environment == 'mainnet' }} @@ -96,7 +96,7 @@ jobs: REDIS_PORT: 6379 REDIS_DB: 0 METADATA_BUCKET: hathor-explorer-metadata-mainnet - CORS_ALLOWED_ORIGIN: https://explorer.hathor.network + CORS_ALLOWED_REGEX: https?://([a-z0-9]*\.){0,5}hathor\.network NODE_CACHE_TTL: 30 - name: Deploy Daemons Testnet if: ${{ needs.init.outputs.environment == 'testnet' }} diff --git a/common/configuration.py b/common/configuration.py index d6359b0a..2004b0b9 100644 --- a/common/configuration.py +++ b/common/configuration.py @@ -66,6 +66,6 @@ def default(cls) -> 'LogRenderer': METADATA_BUCKET = config('METADATA_BUCKET', default=None) -CORS_ALLOWED_ORIGIN = config('CORS_ALLOWED_ORIGIN', default=None) +CORS_ALLOWED_REGEX = config('CORS_ALLOWED_REGEX', default=r'https?://([a-z0-9]*\.){0,5}hathor\.network') LOG_RENDERER = LogRenderer(config('LOG_RENDERER', default=LogRenderer.default().value)) diff --git a/gateways/clients/hathor_core_client.py b/gateways/clients/hathor_core_client.py index 154097d5..fb9ba3a0 100644 --- a/gateways/clients/hathor_core_client.py +++ b/gateways/clients/hathor_core_client.py @@ -51,11 +51,12 @@ async def get(self, path: str, callback: Callable[[dict], None], params: Optiona try: async with aiohttp.ClientSession() as session: async with session.get(url, params=params) as response: - self.log.info( - "hathor_core_response", - path=path, - status=response.status, - body=await response.text()) + if response.status > 299: + self.log.warning( + "hathor_core_error", + path=path, + status=response.status, + body=await response.text()) callback(await response.json()) except Exception as e: self.log.error("hathor_core_error", path=path, error=repr(e)) @@ -95,11 +96,6 @@ def get_text(self, path: str, params: Optional[dict] = None, **kwargs: Any) -> O status=response.status_code, body=response.text) return None - self.log.info( - "hathor_core_response", - path=path, - status=response.status_code, - body=response.text) return response.text except requests.ReadTimeout: diff --git a/gateways/node_api_gateway.py b/gateways/node_api_gateway.py index 17e6a423..7c8ca3ca 100644 --- a/gateways/node_api_gateway.py +++ b/gateways/node_api_gateway.py @@ -168,8 +168,3 @@ def get_token(self, id: str) -> Optional[dict]: """Retrieve token by id """ return self.hathor_core_client.get(TOKEN_ENDPOINT, params={'id': id}) - - def list_tokens(self) -> Optional[dict]: - """Retrieve list of tokens - """ - return self.hathor_core_client.get(TOKEN_ENDPOINT) diff --git a/handlers/node_api.py b/handlers/node_api.py index e8e49b6c..a5a6e93c 100644 --- a/handlers/node_api.py +++ b/handlers/node_api.py @@ -274,27 +274,6 @@ def get_token( } -@ApiGateway() -def list_tokens( - event: ApiGatewayEvent, - _context: LambdaContext, - node_api: Optional[NodeApi] = None -) -> dict: - """Get a list of tokens with details. - - *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. - """ - node_api = node_api or NodeApi() - response = node_api.list_tokens() - return { - "statusCode": 200, - "body": json.dumps(response or {}), - "headers": { - "Content-Type": "application/json" - } - } - - @ApiGateway() def decode_tx( event: ApiGatewayEvent, diff --git a/package-lock.json b/package-lock.json index 6596e130..75cfebf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-explorer-service", - "version": "0.1.17", + "version": "0.1.18", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d2212f28..761a89fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hathor-explorer-service", - "version": "0.1.17", + "version": "0.1.18", "description": "Hathor Explorer Service Serverless deps", "dependencies": { "serverless": "^2.44.0", diff --git a/pyproject.toml b/pyproject.toml index 9176a6be..c81c9cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hathor-explorer-service" -version = "0.1.17" +version = "0.1.18" description = "" authors = ["Hathor Labs "] license = "MIT" diff --git a/serverless.yml b/serverless.yml index 499b06f8..3ebd2b4d 100644 --- a/serverless.yml +++ b/serverless.yml @@ -28,7 +28,7 @@ provider: REDIS_PORT: ${env:REDIS_PORT} REDIS_DB: ${env:REDIS_DB} METADATA_BUCKET: ${env:METADATA_BUCKET} - CORS_ALLOWED_ORIGIN: ${env:CORS_ALLOWED_ORIGIN} + CORS_ALLOWED_REGEX: ${env:CORS_ALLOWED_REGEX} NODE_CACHE_TTL: ${env:NODE_CACHE_TTL} plugins: @@ -337,23 +337,6 @@ functions: cacheKeyParameters: - name: request.querystring.id - node_api_list_tokens: - handler: handlers/node_api.list_tokens - maximumRetryAttempts: 0 - package: - patterns: - - 'handlers/node_api.py' - layers: - - { Ref: PythonRequirementsLambdaLayer } - events: - - http: - path: node_api/tokens - method: get - cors: true - caching: - enabled: true - ttlInSeconds: 3600 - node_api_decode_tx: handler: handlers/node_api.decode_tx maximumRetryAttempts: 0 diff --git a/tests/unit/gateways/test_node_api_gateway.py b/tests/unit/gateways/test_node_api_gateway.py index d0fd29b9..c4813956 100644 --- a/tests/unit/gateways/test_node_api_gateway.py +++ b/tests/unit/gateways/test_node_api_gateway.py @@ -206,16 +206,6 @@ def test_get_token(self, hathor_client): assert result assert sorted(result) == sorted(obj) - @patch('gateways.node_api_gateway.TOKEN_ENDPOINT', 'mock-endpoint') - def test_list_tokens(self, hathor_client): - obj = {'foo': 'bar'} - hathor_client.get = MagicMock(return_value=obj) - gateway = NodeApiGateway(hathor_core_client=hathor_client) - result = gateway.list_tokens() - hathor_client.get.assert_called_once_with('mock-endpoint') - assert result - assert sorted(result) == sorted(obj) - @patch('gateways.node_api_gateway.DECODE_TX_ENDPOINT', 'mock-endpoint') def test_decode_tx(self, hathor_client): obj = {'foo': 'bar'} diff --git a/tests/unit/usecases/test_node_api.py b/tests/unit/usecases/test_node_api.py index ee0983e6..b851b65d 100644 --- a/tests/unit/usecases/test_node_api.py +++ b/tests/unit/usecases/test_node_api.py @@ -245,15 +245,6 @@ def test_get_token(self, node_api_gateway): assert result assert sorted(result) == sorted(obj) - def test_list_tokens(self, node_api_gateway): - obj = {"foo": "bar"} - node_api_gateway.list_tokens = MagicMock(return_value=obj) - node_api = NodeApi(node_api_gateway) - result = node_api.list_tokens() - node_api_gateway.list_tokens.assert_called_once() - assert result - assert sorted(result) == sorted(obj) - def test_decode_tx(self, node_api_gateway): obj = {"foo": "bar"} node_api_gateway.decode_tx = MagicMock(return_value=obj) diff --git a/tests/unit/utils/wrappers/aws/test_api_gateway.py b/tests/unit/utils/wrappers/aws/test_api_gateway.py index 8b5d7c97..a9cef7e9 100644 --- a/tests/unit/utils/wrappers/aws/test_api_gateway.py +++ b/tests/unit/utils/wrappers/aws/test_api_gateway.py @@ -88,14 +88,76 @@ def test_parse_json_fail_proof(self): function.assert_called() assert result['statusCode'] == 200 - @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_ORIGIN', 'explorer.hathor.network') + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') def test_returning_cors_headers(self): function = MagicMock(return_value={'statusCode': 200, 'headers': {}}) api_gateway = ApiGateway() event = { 'body': '{}', - 'headers': {} + 'headers': { + 'origin': 'https://explorer.hathor.network' + } + } + + context = LambdaContext() + + result = api_gateway.__call__(function)(event, context) + + function.assert_called() + assert result['headers']['Access-Control-Allow-Origin'] == 'https://explorer.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True + + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') + def test_returning_cors_headers_testnet(self): + function = MagicMock(return_value={'statusCode': 200, 'headers': {}}) + api_gateway = ApiGateway() + + event = { + 'body': '{}', + 'headers': { + 'origin': 'https://explorer.testnet.hathor.network' + } + } + + context = LambdaContext() + + result = api_gateway.__call__(function)(event, context) + + function.assert_called() + assert result['headers']['Access-Control-Allow-Origin'] == r'https://explorer.testnet.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True + + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') + def test_returning_cors_headers_golf(self): + function = MagicMock(return_value={'statusCode': 200, 'headers': {}}) + api_gateway = ApiGateway() + + event = { + 'body': '{}', + 'headers': { + 'origin': 'https://explorer.golf.hathor.network' + } + } + + context = LambdaContext() + + result = api_gateway.__call__(function)(event, context) + + function.assert_called() + assert result['headers']['Access-Control-Allow-Origin'] == 'https://explorer.golf.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True + + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') + def test_cors_fail_regex_not_matching(self): + function = MagicMock(return_value={'statusCode': 200, 'headers': {}}) + api_gateway = ApiGateway() + + event = { + 'body': '{}', + 'headers': { + 'origin': 'https://explorer.anydomain.network' + } } context = LambdaContext() @@ -103,17 +165,18 @@ def test_returning_cors_headers(self): result = api_gateway.__call__(function)(event, context) function.assert_called() - assert result['headers']['Access-Control-Allow-Origin'] - assert result['headers']['Access-Control-Allow-Credentials'] + assert result['headers'] == {} - @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_ORIGIN', 'explorer.hathor.network') + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') def test_returning_cors_on_apierror(self): function = MagicMock(side_effect=ApiError('Boom!')) api_gateway = ApiGateway() event = { 'body': '{}', - 'headers': {} + 'headers': { + 'origin': 'https://explorer.hathor.network' + } } context = LambdaContext() @@ -121,17 +184,19 @@ def test_returning_cors_on_apierror(self): result = api_gateway.__call__(function)(event, context) function.assert_called() - assert result['headers']['Access-Control-Allow-Origin'] - assert result['headers']['Access-Control-Allow-Credentials'] + assert result['headers']['Access-Control-Allow-Origin'] == 'https://explorer.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True - @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_ORIGIN', 'explorer.hathor.network') + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') def test_returning_cors_on_error(self): function = MagicMock(side_effect=Exception('Boom!')) api_gateway = ApiGateway() event = { 'body': '{}', - 'headers': {} + 'headers': { + 'origin': 'https://explorer.hathor.network' + } } context = LambdaContext() @@ -139,17 +204,19 @@ def test_returning_cors_on_error(self): result = api_gateway.__call__(function)(event, context) function.assert_called() - assert result['headers']['Access-Control-Allow-Origin'] - assert result['headers']['Access-Control-Allow-Credentials'] + assert result['headers']['Access-Control-Allow-Origin'] == 'https://explorer.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True - @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_ORIGIN', 'explorer.hathor.network') + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', r'https?://([a-z0-9]*\.){0,5}hathor\.network') def test_works_with_no_headers_from_function(self): function = MagicMock(return_value={'statusCode': 200}) api_gateway = ApiGateway() event = { 'body': '{}', - 'headers': {} + 'headers': { + 'origin': 'https://explorer.hathor.network' + } } context = LambdaContext() @@ -158,8 +225,10 @@ def test_works_with_no_headers_from_function(self): function.assert_called() assert result['headers'] + assert result['headers']['Access-Control-Allow-Origin'] == 'https://explorer.hathor.network' + assert result['headers']['Access-Control-Allow-Credentials'] is True - @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_ORIGIN', None) + @patch('utils.wrappers.aws.api_gateway.CORS_ALLOWED_REGEX', None) def test_no_headers_with_no_config(self): function = MagicMock(return_value={'statusCode': 200}) api_gateway = ApiGateway() diff --git a/usecases/node_api.py b/usecases/node_api.py index d454bf65..667409f3 100644 --- a/usecases/node_api.py +++ b/usecases/node_api.py @@ -83,6 +83,3 @@ def list_transactions( def get_token(self, id: str) -> Optional[dict]: return self.node_api_gateway.get_token(id) - - def list_tokens(self) -> Optional[dict]: - return self.node_api_gateway.list_tokens() diff --git a/utils/wrappers/aws/api_gateway.py b/utils/wrappers/aws/api_gateway.py index 164cdcfb..ed60f8cf 100644 --- a/utils/wrappers/aws/api_gateway.py +++ b/utils/wrappers/aws/api_gateway.py @@ -1,9 +1,10 @@ import json +import re from typing import Any, Callable from aws_lambda_context import LambdaContext -from common.configuration import CORS_ALLOWED_ORIGIN +from common.configuration import CORS_ALLOWED_REGEX from common.errors import ApiError from common.logging import get_logger @@ -41,11 +42,13 @@ def wrapper(event: dict, context: LambdaContext, *args: Any, **kwargs: Any) -> d 'not_found': 404 } - headers = {} + requestHeaders = event.get('headers', {}) + origin = requestHeaders.get('origin', requestHeaders.get('Origin', None)) - if CORS_ALLOWED_ORIGIN: - headers = { - 'Access-Control-Allow-Origin': CORS_ALLOWED_ORIGIN, + responseHeaders = {} + if origin is not None and re.match(CORS_ALLOWED_REGEX, origin): + responseHeaders = { + 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': True } @@ -56,7 +59,7 @@ def wrapper(event: dict, context: LambdaContext, *args: Any, **kwargs: Any) -> d if result.get('headers') is None: result['headers'] = {} - result['headers'].update(headers) + result['headers'].update(responseHeaders) return result except ApiError as error: @@ -67,7 +70,7 @@ def wrapper(event: dict, context: LambdaContext, *args: Any, **kwargs: Any) -> d error_key = 'internal_error' return { - 'headers': headers, + 'headers': responseHeaders, 'statusCode': errors_status[error_key], 'body': json.dumps({ 'error': str(error) @@ -76,7 +79,7 @@ def wrapper(event: dict, context: LambdaContext, *args: Any, **kwargs: Any) -> d except Exception as error: logger.exception(error) return { - 'headers': headers, + 'headers': responseHeaders, 'statusCode': 500, 'body': json.dumps({ 'error': str(error)