diff --git a/src/api-config.json b/src/api-config.json deleted file mode 100644 index 34c08e0..0000000 --- a/src/api-config.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "$schema": "./api-config.schema.json", - "title": "API Gateway Config", - "description": "Configuration for the Serverless API Gateway", - "servers": [ - { - "alias": "serverlessapigateway-api", - "url": "https://74ec-2a02-e0-665f-2400-4803-52e0-7bcf-8789.ngrok-free.app" - }, - { - "alias": "serverlessapigateway-api-sub", - "url": "https://4e05-2a02-e0-665f-2400-e945-4e3-409c-d532.ngrok-free.app/sub" - } - ], - "services": [ - { - "alias": "endpoint1", - "entrypoint": "./services/endpoint1" - } - ], - "serviceBindings": [ - { - "alias": "userendpoint", - "binding": "USER_SERVICE" - } - ], - "cors": { - "allow_origins": [ - "https://api1.serverlessapigateway.com", - "http://api1.serverlessapigateway.com", - "https://api2.serverlessapigateway.com" - ], - "allow_methods": [ - "GET", - "POST", - "PUT", - "DELETE", - "OPTIONS" - ], - "allow_headers": [ - "*" - ], - "expose_headers": [ - "*" - ], - "allow_credentials": true, - "max_age": 3600 - }, - "authorizer": { - "type": "jwt", - "secret": "TESTcc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2", - "algorithm": "HS256", - "audience": "opensourcecommunity", - "issuer": "serverlessapigw" - }, - "variables": { - "global_variable": "global_variable_value" - }, - "paths": [ - { - "method": "GET", - "path": "/api/v1/mapping", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - }, - "auth": true, - "mapping": { - "headers": { - "x-jwt-sub": "$request.jwt.sub", - "x-jwt-aud": "$request.jwt.aud", - "x-jwt-iss": "$request.jwt.iss", - "x-jwt-name": "$request.jwt.name", - "x-jwt-email": "$request.jwt.email", - "x-config-api-key": "$config.api_key", - "x-config-database-url": "$config.database-url", - "x-config-nested-config-key": "$config.nested.config.key", - "x-query-userId": "$request.query.userId", - "x-query-redirect_uri": "$request.query.redirect_uri", - "x-global-variable": "$config.global_variable" - }, - "query": { - "jwt-sub": "$request.jwt.sub", - "jwt-aud": "$request.jwt.aud", - "jwt-iss": "$request.jwt.iss", - "jwt-name": "$request.jwt.name", - "jwt-email": "$request.jwt.email", - "config-api-key": "$config.api_key", - "config-database-url": "$config.database-url", - "config-nested-config-key": "$config.nested.config.key" - } - }, - "variables": { - "api_key": "API_KEY_VALUE", - "database-url": "sqlite://db.sqlite", - "nested.config.key": "nested config value", - "global_variable": "this-not-global-variable" - } - }, - { - "method": "GET", - "path": "/api/v1/auth", - "response": { - "status": "This is authenticated GET method" - }, - "auth": true - }, - { - "method": "GET", - "path": "/api/v1/no-auth", - "response": { - "status": "This is un-authenticated GET method" - }, - "auth": false - }, - { - "method": "GET", - "path": "/api/v1/proxy", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - } - }, - { - "method": "GET", - "path": "/api/v1/proxy/{parameter}", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - } - }, - { - "method": "ANY", - "path": "/api/v1/proxy/{.+}", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - } - }, - { - "method": "ANY", - "path": "/{.+}", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - } - }, - { - "method": "GET", - "path": "/api/v1/proxy/sub", - "integration": { - "type": "http", - "server": "serverlessapigateway-api-sub" - } - }, - { - "method": "GET", - "path": "/api/v1/method", - "response": { - "status": "This is GET method" - } - }, - { - "method": "POST", - "path": "/api/v1/method", - "response": { - "status": "This is POST method" - } - }, - { - "method": "ANY", - "path": "/api/v1/method", - "response": { - "status": "This is ANY method" - } - }, - { - "method": "OPTIONS", - "path": "/api/v1/method", - "response": { - "status": "This is OPTIONS method" - } - }, - { - "method": "POST", - "path": "/api/v1/proxy", - "integration": { - "type": "http_proxy", - "server": "serverlessapigateway-api" - } - }, - { - "method": "GET", - "path": "/api/v1/health", - "response": { - "status": "OK" - } - }, - { - "method": "GET", - "path": "/api/v1/health/ready", - "response": { - "status": "Ready OK" - } - }, - { - "method": "GET", - "path": "/api/v1/health/live", - "response": { - "status": "Live OK" - } - }, - { - "method": "GET", - "path": "/api/v1/health/string", - "response": "String OK" - }, - { - "method": "POST", - "path": "/api/v1/health", - "response": { - "status": "OK" - } - }, - { - "method": "ANY", - "path": "/api/v1/health/any", - "response": { - "status": "OK" - } - }, - { - "method": "GET", - "path": "/api/v1/endpoint1", - "integration": { - "type": "service", - "binding": "endpoint1" - } - }, - { - "method": "GET", - "path": "/api/v1/user", - "integration": { - "type": "service_binding", - "binding": "userendpoint", - "function": "fetch" - }, - "auth": false - } - ] -} diff --git a/src/auth.js b/src/auth.js index ea0da8e..a2dee13 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,8 +1,7 @@ import {jwtVerify, errors } from 'jose'; -import apiConfig from './api-config.json'; import { AuthError } from "./types/error_types"; -async function jwtAuth(request) { +async function jwtAuth(request, apiConfig) { const secret = new TextEncoder().encode(apiConfig.authorizer?.secret); const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { diff --git a/src/cors.js b/src/cors.js index 56d7f0d..7fe8846 100644 --- a/src/cors.js +++ b/src/cors.js @@ -1,8 +1,8 @@ -import apiConfig from './api-config.json'; - -function setCorsHeaders(request, response) { - const corsConfig = apiConfig.cors; - +function setCorsHeaders(request, response, corsConfig) { + console.log('Setting CORS headers'); + console.log('Request headers:', request); + console.log('Response headers:', response.headers); + console.log('CORS config:', corsConfig); const origin = request.headers.get('Origin'); const matchingOrigin = corsConfig.allow_origins.find((allowedOrigin) => allowedOrigin === origin); @@ -14,11 +14,13 @@ function setCorsHeaders(request, response) { headers.set('Access-Control-Allow-Credentials', corsConfig.allow_credentials.toString()); headers.set('Access-Control-Max-Age', corsConfig.max_age.toString()); - return new Response(response.body, { + const newResponse = new Response(response.body, { status: response.status, statusText: response.statusText, headers: headers, }); + console.log('New response:', newResponse); + return newResponse; } export { setCorsHeaders }; diff --git a/src/index.js b/src/index.js index 0f94f30..733d802 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,6 @@ const responses = await import('./responses'); const { ValueMapper } = await import('./mapping'); const { setCorsHeaders } = await import('./cors'); const { PathOperator } = await import('./path-ops'); -const _apiConfig = await import('./api-config.json') const { AuthError } = await import('./types/error_types'); const { setPoweredByHeader } = await import('./powered-by'); const { createProxiedRequest } = await import('./requests'); @@ -15,7 +14,17 @@ export default { async fetch(request, env, ctx) { const url = new URL(request.url); - const apiConfig = _apiConfig; + let apiConfig; + try { + if (typeof env.CONFIG === 'undefined' || await env.CONFIG.get("api-config.json") === null) { + apiConfig = await import('./api-config.json'); + } else { + apiConfig = JSON.parse(await env.CONFIG.get("api-config.json")); + } + } catch (e) { + console.error('Error loading API configuration', e); + return setPoweredByHeader(request, responses.configIsMissingResponse()); + } // Handle CORS preflight (OPTIONS) requests directly if (apiConfig.cors && request.method === 'OPTIONS') { @@ -24,7 +33,7 @@ export default { return item.method === 'OPTIONS' && matchResult.matchedCount > 0 && matchResult.methodMatches; }); if (!matchedItem) { - return setPoweredByHeader(setCorsHeaders(request, new Response(null, { status: 204 }))); + return setPoweredByHeader(setCorsHeaders(request, new Response(null, { status: 204 }), apiConfig.cors)); } } @@ -61,7 +70,7 @@ export default { // Check if the matched path requires authorization if (apiConfig.authorizer && matchedPath.config.auth && apiConfig.authorizer.type == 'jwt') { try { - jwtPayload = await jwtAuth(request); + jwtPayload = await jwtAuth(request, apiConfig); } catch (error) { if (error instanceof AuthError) { return setPoweredByHeader( @@ -71,10 +80,11 @@ export default { status: error.statusCode, headers: { 'Content-Type': 'application/json' }, }), + apiConfig.cors ), ); } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse())); + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), apiConfig.cors)); } } } @@ -90,10 +100,11 @@ export default { status: error.statusCode, headers: { 'Content-Type': 'application/json' }, }), + apiConfig.cors ), ); } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse())); + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), apiConfig.cors)); } } } @@ -113,7 +124,7 @@ export default { globalVariables: apiConfig.variables, }); } - return fetch(modifiedRequest).then((response) => setPoweredByHeader(setCorsHeaders(request, response))); + return fetch(modifiedRequest).then((response) => setPoweredByHeader(setCorsHeaders(request, response, apiConfig.cors))); } } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['SERVICE']) { const service = @@ -125,7 +136,7 @@ export default { const Service = module.default; const serviceInstance = new Service(); const response = serviceInstance.fetch(request, env, ctx); - return setPoweredByHeader(setCorsHeaders(request, response)); + return setPoweredByHeader(setCorsHeaders(request, response, apiConfig.cors)); } } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['SERVICE_BINDING']) { const service = @@ -134,35 +145,32 @@ export default { if (service) { const response = await env[service.binding][matchedPath.config.integration.function](request, env, ctx); - return setPoweredByHeader(setCorsHeaders(request, response)); + return setPoweredByHeader(setCorsHeaders(request, response, apiConfig.cors)); } - } - else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0CALLBACK']) { + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0CALLBACK']) { const urlParams = new URLSearchParams(url.search); const code = urlParams.get('code'); - return auth0CallbackHandler(code); - } - else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0USERINFO']) { + return auth0CallbackHandler(code, apiConfig); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0USERINFO']) { const urlParams = new URLSearchParams(url.search); const accessToken = urlParams.get('access_token'); return getProfile(accessToken); - } - else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0CALLBACKREDIRECT']) { + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum['AUTH0CALLBACKREDIRECT']) { const urlParams = new URLSearchParams(url.search); return redirectToLogin({ state: urlParams.get('state') }); - } - else { + } else { return setPoweredByHeader( setCorsHeaders( request, new Response(JSON.stringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }), + apiConfig.cors ), ); } } - return setPoweredByHeader(setCorsHeaders(request, responses.noMatchResponse())); + return setPoweredByHeader(setCorsHeaders(request, responses.noMatchResponse(), apiConfig.cors)); }, }; diff --git a/src/integrations/auth0.js b/src/integrations/auth0.js index 1730300..9949aff 100644 --- a/src/integrations/auth0.js +++ b/src/integrations/auth0.js @@ -1,8 +1,7 @@ import { jwtVerify, createLocalJWKSet, createRemoteJWKSet, errors } from 'jose'; -import apiConfig from '../api-config.json'; import { AuthError } from "../types/error_types"; -async function auth0CallbackHandler(code) { +async function auth0CallbackHandler(code, apiConfig) { const { domain, client_id, client_secret } = apiConfig.authorizer; const tokenUrl = `https://${domain}/oauth/token`; diff --git a/src/responses.js b/src/responses.js index 7ed20b3..4c61b1a 100644 --- a/src/responses.js +++ b/src/responses.js @@ -1,6 +1,14 @@ +export const badRequestResponse = () => + new Response(JSON.stringify({ message: 'Bad request' }), { headers: { 'Content-Type': 'application/json' }, status: 400 }); export const noMatchResponse = () => new Response(JSON.stringify({ message: 'No match found.' }), { headers: { 'Content-Type': 'application/json' }, status: 404 }); export const unauthorizedResponse = () => new Response(JSON.stringify({ message: 'Unauthorized' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); +export const forbiddenResponse = () => + new Response(JSON.stringify({ message: 'Forbidden' }), { headers: { 'Content-Type': 'application/json' }, status: 403 }); +export const notFoundResponse = () => + new Response(JSON.stringify({ message: 'Not found' }), { headers: { 'Content-Type': 'application/json' }, status: 404 }); export const internalServerErrorResponse = () => new Response(JSON.stringify({ message: 'Internal server error' }), { headers: { 'Content-Type': 'application/json' }, status: 500 }); +export const configIsMissingResponse = () => + new Response(JSON.stringify({ message: 'API configuration is missing' }), { headers: { 'Content-Type': 'application/json' }, status: 501 }); diff --git a/wrangler.toml b/wrangler.toml index e1deadb..9503b8c 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,64 +1,21 @@ -name = "serverlessapigateway" +name = "serverlessapigw" main = "src/index.js" -compatibility_date = "2024-01-17" +compatibility_date = "2024-02-08" +route = { pattern = "api-test.serverlessapigw.com/*", zone_name = "serverlessapigw.com" } +logpush = false +send_metrics = true +minify = true +workers_dev = true + +kv_namespaces =[ + { binding="CONFIG", id="1863fa3d004549e4800edeebeb36c0ff", preview_id = "1989f3848bb645b090f1cc82a5eb04b1"} +] find_additional_modules = true rules = [ - { type = "ESModule", globs = ["services/*.js"]} -] - -services = [ - { binding = "USER_SERVICE", service = "user_service" } + { type = "ESModule", globs = ["services/*.js","services_by_link/*.js"]} ] [observability] enabled = true head_sampling_rate = 0.3 - -# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) -# Note: Use secrets to store sensitive data. -# Docs: https://developers.cloudflare.com/workers/platform/environment-variables -# [vars] -# MY_VARIABLE = "production_value" - -# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. -# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv -# [[kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - -# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. -# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ -# [[r2_buckets]] -# binding = "MY_BUCKET" -# bucket_name = "my-bucket" - -# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. -# Docs: https://developers.cloudflare.com/queues/get-started -# [[queues.producers]] -# binding = "MY_QUEUE" -# queue = "my-queue" - -# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. -# Docs: https://developers.cloudflare.com/queues/get-started -# [[queues.consumers]] -# queue = "my-queue" - -# Bind another Worker service. Use this binding to call another Worker without network overhead. -# Docs: https://developers.cloudflare.com/workers/platform/services -# [[services]] -# binding = "MY_SERVICE" -# service = "my-service" - -# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. -# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. -# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects -# [[durable_objects.bindings]] -# name = "MY_DURABLE_OBJECT" -# class_name = "MyDurableObject" - -# Durable Object migrations. -# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations -# [[migrations]] -# tag = "v1" -# new_classes = ["MyDurableObject"]