def get_api_from_headers(headers, method=None, path=None, data=None): """Determine API and backend port based on "Authorization" or "Host" headers.""" # initialize result result = API_UNKNOWN, 0 target = headers.get("x-amz-target", "") host = headers.get("host", "") auth_header = headers.get("authorization", "") if not auth_header and not target and "." not in host: return result[0], result[1], path, host path = path or "/" # https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html try: service = extract_service_name_from_auth_header(headers) assert service result = service, get_service_port_for_account(service, headers) except Exception: pass result_before = result # Fallback rules and route customizations applied below if host.endswith("cloudfront.net"): path = path or "/" result = "cloudfront", config.PORT_CLOUDFRONT elif target.startswith("AWSCognitoIdentityProviderService") or "cognito-idp." in host: result = "cognito-idp", config.PORT_COGNITO_IDP elif target.startswith("AWSCognitoIdentityService") or "cognito-identity." in host: result = "cognito-identity", config.PORT_COGNITO_IDENTITY elif result[0] == "s3" or uses_host_addressing(headers): result = "s3", config.PORT_S3 elif result[0] == "states" in auth_header or host.startswith("states."): result = "stepfunctions", config.PORT_STEPFUNCTIONS elif "route53." in host: result = "route53", config.PORT_ROUTE53 elif result[0] == "monitoring": result = "cloudwatch", config.PORT_CLOUDWATCH elif result[0] == "email": result = "ses", config.PORT_SES elif result[0] == "execute-api" or ".execute-api." in host: result = "apigateway", config.PORT_APIGATEWAY elif target.startswith("Firehose_"): result = "firehose", config.PORT_FIREHOSE elif target.startswith("DynamoDB_"): result = "dynamodb", config.PORT_DYNAMODB elif target.startswith("DynamoDBStreams") or host.startswith("streams.dynamodb."): # Note: DDB streams requests use ../dynamodb/.. auth header, hence we also need to update result_before result = result_before = "dynamodbstreams", config.PORT_DYNAMODBSTREAMS elif result[0] == "EventBridge" or target.startswith("AWSEvents"): result = "events", config.PORT_EVENTS elif target.startswith("ResourceGroupsTaggingAPI_"): result = "resourcegroupstaggingapi", config.PORT_RESOURCEGROUPSTAGGINGAPI elif result[0] == "resource-groups": result = "resource-groups", config.PORT_RESOURCE_GROUPS return result[0], result_before[1] or result[1], path, host
def test_uses_host_address(self): addresses = [ ({"host": f"https://aws.{LOCALHOST}:4566"}, False), # attention: This is **not** a host style reference according to s3 specs but a special case from our side ({"host": f"https://aws.{LOCALHOST}.localstack.cloud:4566"}, True), ({"host": f"https://{LOCALHOST}.aws:4566"}, False), ({"host": f"https://{LOCALHOST}.swa:4566"}, False), ({"host": f"https://swa.{LOCALHOST}:4566"}, False), ({"host": "https://bucket.s3.localhost.localstack.cloud"}, True), ({"host": "bucket.s3.eu-west-1.amazonaws.com"}, True), ({"host": "https://s3.eu-west-1.localhost.localstack.cloud/bucket"}, False), ({"host": "https://s3.eu-west-1.localhost.localstack.cloud/bucket/key"}, False), ({"host": "https://s3.localhost.localstack.cloud/bucket"}, False), ({"host": "https://bucket.s3.eu-west-1.localhost.localstack.cloud/key"}, True), ( { "host": "https://bucket.s3.eu-west-1.localhost.localstack.cloud/key/key/content.png" }, True, ), ({"host": "https://s3.localhost.localstack.cloud/bucket/key"}, False), ({"host": "https://bucket.s3.eu-west-1.localhost.localstack.cloud"}, True), ({"host": "https://bucket.s3.localhost.localstack.cloud/key"}, True), ({"host": "bucket.s3.eu-west-1.amazonaws.com"}, True), ({"host": "bucket.s3.amazonaws.com"}, True), ({"host": "notabucket.amazonaws.com"}, False), ({"host": "s3.amazonaws.com"}, False), ({"host": "s3.eu-west-1.amazonaws.com"}, False), ] for headers, expected_result in addresses: self.assertEqual(expected_result, s3_utils.uses_host_addressing(headers))
def subdomain_based_buckets(self, request): return s3_utils.uses_host_addressing(request.headers)
def get_api_from_headers(headers, method=None, path=None, data=None): """Determine API and backend port based on "Authorization" or "Host" headers.""" # initialize result result = API_UNKNOWN, 0 target = headers.get("x-amz-target", "") host = headers.get("host", "") auth_header = headers.get("authorization", "") if not auth_header and not target and "." not in host: return result[0], result[1], path, host path = path or "/" # https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html try: service = extract_service_name_from_auth_header(headers) assert service result = service, get_service_port_for_account(service, headers) except Exception: pass result_before = result # Fallback rules and route customizations applied below if host.endswith("cloudfront.net"): path = path or "/" result = "cloudfront", config.service_port("cloudfront") elif target.startswith( "AWSCognitoIdentityProviderService") or "cognito-idp." in host: result = "cognito-idp", config.service_port("cognito-idp") elif target.startswith( "AWSCognitoIdentityService") or "cognito-identity." in host: result = "cognito-identity", config.service_port("cognito-identity") elif result[0] == "s3" or uses_host_addressing(headers): result = "s3", config.service_port("s3") elif result[0] == "states" in auth_header or host.startswith("states."): result = "stepfunctions", config.service_port("stepfunctions") elif "route53." in host: result = "route53", config.service_port("route53") elif result[0] == "monitoring": result = "cloudwatch", config.service_port("cloudwatch") elif result[0] == "email": result = "ses", config.service_port("ses") elif result[0] == "execute-api" or ".execute-api." in host: result = "apigateway", config.service_port("apigateway") elif target.startswith("Firehose_"): result = "firehose", config.service_port("firehose") elif target.startswith("DynamoDB_"): result = "dynamodb", config.service_port("dynamodb") elif target.startswith("DynamoDBStreams") or host.startswith( "streams.dynamodb."): # Note: DDB streams requests use ../dynamodb/.. auth header, hence we also need to update result_before result = result_before = "dynamodbstreams", config.service_port( "dynamodbstreams") elif result[0] == "EventBridge" or target.startswith("AWSEvents"): result = "events", config.service_port("events") elif target.startswith("ResourceGroupsTaggingAPI_"): result = "resourcegroupstaggingapi", config.service_port( "resourcegroupstaggingapi") elif result[0] == "resource-groups": result = "resource-groups", config.service_port("resource-groups") elif result[0] == "es" and path is not None and not path.startswith( "/2015-01-01/"): # For OpenSearch, the auth header points to the API ("es"). # However, if the path does _not_ start with /2015-01-01 (the API version path prefix for the only ES API # version) it is a request to the opensearch API. result = "opensearch", config.service_port("opensearch") return result[0], result_before[1] or result[1], path, host
def legacy_rules(request: Request) -> Optional[str]: """ *Legacy* rules which migrate routing logic which will become obsolete with the ASF Gateway. All rules which are implemented here should be migrated to the new router once these services are migrated to ASF. TODO: These custom rules should become obsolete by migrating these to use the http/router.py """ path = request.path method = request.method host = hostname_from_url(request.host) # API Gateway invocation URLs # TODO: deprecated with #6040, where API GW user routes are served through the gateway directly if ("/%s/" % PATH_USER_REQUEST) in request.path or ( host.endswith(LOCALHOST_HOSTNAME) and "execute-api" in host): return "apigateway" # DynamoDB shell URLs if path.startswith("/shell") or path.startswith("/dynamodb/shell"): return "dynamodb" # TODO The remaining rules here are special S3 rules - needs to be discussed how these should be handled. # Some are similar to other rules and not that greedy, others are nearly general fallbacks. stripped = path.strip("/") if method in ["GET", "HEAD"] and stripped: # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>` return "s3" # detect S3 URLs if stripped and "/" not in stripped: if method == "PUT": # assume that this is an S3 PUT bucket request with URL path `/<bucket>` return "s3" if method == "POST" and "key" in request.values: # assume that this is an S3 POST request with form parameters or multipart form in the body return "s3" # detect S3 requests sent from aws-cli using --no-sign-request option if "aws-cli/" in str(request.user_agent): return "s3" # detect S3 pre-signed URLs (v2 and v4) values = request.values if any(value in values for value in [ "AWSAccessKeyId", "Signature", "X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-SignedHeaders", "X-Amz-Signature", ]): return "s3" # S3 delete object requests if method == "POST" and "delete" in values: data_bytes = to_bytes(request.data) if b"<Delete" in data_bytes and b"<Key>" in data_bytes: return "s3" # Put Object API can have multiple keys if stripped.count("/") >= 1 and method == "PUT": # assume that this is an S3 PUT bucket object request with URL path `/<bucket>/object` # or `/<bucket>/object/object1/+` return "s3" # detect S3 requests with "AWS id:key" Auth headers auth_header = request.headers.get("Authorization") or "" if auth_header.startswith("AWS "): return "s3" if uses_host_addressing(request.headers): # Note: This needs to be the last rule (and therefore is not in the host rules), since it is incredibly greedy return "s3"
def _is_vhost_address(request: HttpRequest) -> bool: from localstack.services.s3.s3_utils import uses_host_addressing return uses_host_addressing(request.headers)