serverless-offline 8.7.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -95
- package/package.json +41 -69
- package/src/ServerlessOffline.js +412 -0
- package/src/config/commandOptions.js +155 -0
- package/src/config/constants.js +22 -0
- package/{dist → src}/config/defaultOptions.js +8 -17
- package/src/config/index.js +4 -0
- package/src/config/supportedRuntimes.js +47 -0
- package/src/events/authCanExecuteResource.js +35 -0
- package/src/events/authFunctionNameExtractor.js +75 -0
- package/src/events/authMatchPolicyResource.js +71 -0
- package/src/events/authValidateContext.js +51 -0
- package/src/events/http/Endpoint.js +135 -0
- package/src/events/http/Http.js +50 -0
- package/src/events/http/HttpEventDefinition.js +20 -0
- package/src/events/http/HttpServer.js +1277 -0
- package/src/events/http/OfflineEndpoint.js +33 -0
- package/src/events/http/authJWTSettingsExtractor.js +70 -0
- package/src/events/http/createAuthScheme.js +176 -0
- package/src/events/http/createJWTAuthScheme.js +106 -0
- package/src/events/http/index.js +1 -0
- package/src/events/http/javaHelpers.js +102 -0
- package/src/events/http/lambda-events/LambdaIntegrationEvent.js +57 -0
- package/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +233 -0
- package/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +190 -0
- package/src/events/http/lambda-events/VelocityContext.js +147 -0
- package/src/events/http/lambda-events/index.js +4 -0
- package/src/events/http/lambda-events/renderVelocityTemplateObject.js +93 -0
- package/{dist → src}/events/http/parseResources.js +73 -78
- package/src/events/http/payloadSchemaValidator.js +13 -0
- package/{dist → src}/events/http/templates/offline-default.req.vm +0 -0
- package/{dist → src}/events/http/templates/offline-default.res.vm +0 -0
- package/src/events/schedule/Schedule.js +131 -0
- package/src/events/schedule/ScheduleEvent.js +18 -0
- package/src/events/schedule/ScheduleEventDefinition.js +21 -0
- package/src/events/schedule/index.js +1 -0
- package/src/events/websocket/HttpServer.js +69 -0
- package/src/events/websocket/WebSocket.js +52 -0
- package/src/events/websocket/WebSocketClients.js +462 -0
- package/src/events/websocket/WebSocketEventDefinition.js +18 -0
- package/src/events/websocket/WebSocketServer.js +73 -0
- package/src/events/websocket/http-routes/_catchAll/catchAllRoute.js +16 -0
- package/src/events/websocket/http-routes/_catchAll/index.js +1 -0
- package/src/events/websocket/http-routes/connections/ConnectionsController.js +28 -0
- package/src/events/websocket/http-routes/connections/connectionsRoutes.js +70 -0
- package/src/events/websocket/http-routes/connections/index.js +1 -0
- package/src/events/websocket/http-routes/index.js +2 -0
- package/src/events/websocket/index.js +1 -0
- package/src/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +65 -0
- package/src/events/websocket/lambda-events/WebSocketConnectEvent.js +68 -0
- package/src/events/websocket/lambda-events/WebSocketDisconnectEvent.js +31 -0
- package/src/events/websocket/lambda-events/WebSocketEvent.js +29 -0
- package/src/events/websocket/lambda-events/WebSocketRequestContext.js +67 -0
- package/src/events/websocket/lambda-events/index.js +4 -0
- package/src/index.js +12 -0
- package/src/lambda/HttpServer.js +108 -0
- package/src/lambda/Lambda.js +68 -0
- package/src/lambda/LambdaContext.js +33 -0
- package/src/lambda/LambdaFunction.js +308 -0
- package/src/lambda/LambdaFunctionPool.js +109 -0
- package/src/lambda/__tests__/LambdaContext.test.js +30 -0
- package/src/lambda/__tests__/LambdaFunction.test.js +196 -0
- package/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js +47 -0
- package/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js +46 -0
- package/src/lambda/__tests__/fixtures/Lambda/package.json +3 -0
- package/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js +145 -0
- package/src/lambda/__tests__/fixtures/package.json +3 -0
- package/src/lambda/__tests__/routes/invocations/InvocationsController.test.js +42 -0
- package/src/lambda/handler-runner/HandlerRunner.js +136 -0
- package/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +72 -0
- package/src/lambda/handler-runner/child-process-runner/childProcessHelper.js +42 -0
- package/src/lambda/handler-runner/child-process-runner/index.js +1 -0
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +417 -0
- package/src/lambda/handler-runner/docker-runner/DockerImage.js +35 -0
- package/src/lambda/handler-runner/docker-runner/DockerRunner.js +63 -0
- package/src/lambda/handler-runner/docker-runner/index.js +1 -0
- package/src/lambda/handler-runner/go-runner/GoRunner.js +166 -0
- package/src/lambda/handler-runner/go-runner/index.js +1 -0
- package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +125 -0
- package/src/lambda/handler-runner/in-process-runner/index.js +1 -0
- package/src/lambda/handler-runner/index.js +1 -0
- package/src/lambda/handler-runner/java-runner/JavaRunner.js +114 -0
- package/src/lambda/handler-runner/java-runner/index.js +1 -0
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +138 -0
- package/src/lambda/handler-runner/python-runner/index.js +1 -0
- package/{dist → src}/lambda/handler-runner/python-runner/invoke.py +0 -0
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +107 -0
- package/src/lambda/handler-runner/ruby-runner/index.js +1 -0
- package/{dist → src}/lambda/handler-runner/ruby-runner/invoke.rb +0 -0
- package/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +70 -0
- package/src/lambda/handler-runner/worker-thread-runner/index.js +1 -0
- package/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +29 -0
- package/src/lambda/index.js +1 -0
- package/src/lambda/routes/index.js +2 -0
- package/src/lambda/routes/invocations/InvocationsController.js +102 -0
- package/src/lambda/routes/invocations/index.js +1 -0
- package/src/lambda/routes/invocations/invocationsRoute.js +77 -0
- package/src/lambda/routes/invoke-async/InvokeAsyncController.js +20 -0
- package/src/lambda/routes/invoke-async/index.js +1 -0
- package/src/lambda/routes/invoke-async/invokeAsyncRoute.js +33 -0
- package/src/utils/__tests__/createUniqueId.test.js +18 -0
- package/src/utils/__tests__/formatToClfTime.test.js +14 -0
- package/src/utils/__tests__/generateHapiPath.test.js +46 -0
- package/src/utils/__tests__/lowerCaseKeys.test.js +30 -0
- package/src/utils/__tests__/parseHeaders.test.js +13 -0
- package/src/utils/__tests__/parseMultiValueHeaders.test.js +24 -0
- package/src/utils/__tests__/parseMultiValueQueryStringParameters.test.js +159 -0
- package/src/utils/__tests__/parseQueryStringParameters.test.js +15 -0
- package/src/utils/__tests__/splitHandlerPathAndName.test.js +54 -0
- package/src/utils/__tests__/unflatten.test.js +32 -0
- package/src/utils/checkDockerDaemon.js +19 -0
- package/src/utils/checkGoVersion.js +16 -0
- package/src/utils/createApiKey.js +5 -0
- package/src/utils/createUniqueId.js +5 -0
- package/src/utils/detectExecutable.js +11 -0
- package/{dist → src}/utils/formatToClfTime.js +6 -14
- package/src/utils/generateHapiPath.js +26 -0
- package/src/utils/getHttpApiCorsConfig.js +28 -0
- package/src/utils/index.js +42 -0
- package/src/utils/jsonPath.js +13 -0
- package/src/utils/logRoutes.js +64 -0
- package/src/utils/lowerCaseKeys.js +6 -0
- package/src/utils/parseHeaders.js +14 -0
- package/src/utils/parseMultiValueHeaders.js +27 -0
- package/src/utils/parseMultiValueQueryStringParameters.js +31 -0
- package/src/utils/parseQueryStringParameters.js +15 -0
- package/src/utils/resolveJoins.js +29 -0
- package/src/utils/splitHandlerPathAndName.js +31 -0
- package/src/utils/unflatten.js +11 -0
- package/dist/ServerlessOffline.js +0 -507
- package/dist/checkEngine.js +0 -21
- package/dist/config/commandOptions.js +0 -149
- package/dist/config/constants.js +0 -30
- package/dist/config/index.js +0 -55
- package/dist/config/supportedRuntimes.js +0 -40
- package/dist/debugLog.js +0 -10
- package/dist/events/authCanExecuteResource.js +0 -35
- package/dist/events/authFunctionNameExtractor.js +0 -87
- package/dist/events/authMatchPolicyResource.js +0 -62
- package/dist/events/http/Endpoint.js +0 -171
- package/dist/events/http/Http.js +0 -77
- package/dist/events/http/HttpEventDefinition.js +0 -36
- package/dist/events/http/HttpServer.js +0 -1363
- package/dist/events/http/OfflineEndpoint.js +0 -40
- package/dist/events/http/authJWTSettingsExtractor.js +0 -76
- package/dist/events/http/authValidateContext.js +0 -48
- package/dist/events/http/createAuthScheme.js +0 -184
- package/dist/events/http/createJWTAuthScheme.js +0 -155
- package/dist/events/http/index.js +0 -15
- package/dist/events/http/javaHelpers.js +0 -99
- package/dist/events/http/lambda-events/LambdaIntegrationEvent.js +0 -85
- package/dist/events/http/lambda-events/LambdaProxyIntegrationEvent.js +0 -244
- package/dist/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +0 -221
- package/dist/events/http/lambda-events/VelocityContext.js +0 -168
- package/dist/events/http/lambda-events/index.js +0 -39
- package/dist/events/http/lambda-events/renderVelocityTemplateObject.js +0 -108
- package/dist/events/http/payloadSchemaValidator.js +0 -13
- package/dist/events/schedule/Schedule.js +0 -182
- package/dist/events/schedule/ScheduleEvent.js +0 -27
- package/dist/events/schedule/ScheduleEventDefinition.js +0 -36
- package/dist/events/schedule/index.js +0 -15
- package/dist/events/websocket/HttpServer.js +0 -112
- package/dist/events/websocket/WebSocket.js +0 -78
- package/dist/events/websocket/WebSocketClients.js +0 -550
- package/dist/events/websocket/WebSocketEventDefinition.js +0 -32
- package/dist/events/websocket/WebSocketServer.js +0 -140
- package/dist/events/websocket/http-routes/_catchAll/catchAllRoute.js +0 -33
- package/dist/events/websocket/http-routes/_catchAll/index.js +0 -15
- package/dist/events/websocket/http-routes/connections/ConnectionsController.js +0 -45
- package/dist/events/websocket/http-routes/connections/connectionsRoutes.js +0 -95
- package/dist/events/websocket/http-routes/connections/index.js +0 -15
- package/dist/events/websocket/http-routes/index.js +0 -23
- package/dist/events/websocket/index.js +0 -15
- package/dist/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +0 -99
- package/dist/events/websocket/lambda-events/WebSocketConnectEvent.js +0 -101
- package/dist/events/websocket/lambda-events/WebSocketDisconnectEvent.js +0 -47
- package/dist/events/websocket/lambda-events/WebSocketEvent.js +0 -54
- package/dist/events/websocket/lambda-events/WebSocketRequestContext.js +0 -98
- package/dist/events/websocket/lambda-events/index.js +0 -39
- package/dist/index.js +0 -19
- package/dist/lambda/HttpServer.js +0 -122
- package/dist/lambda/Lambda.js +0 -113
- package/dist/lambda/LambdaContext.js +0 -53
- package/dist/lambda/LambdaFunction.js +0 -391
- package/dist/lambda/LambdaFunctionPool.js +0 -127
- package/dist/lambda/handler-runner/HandlerRunner.js +0 -223
- package/dist/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +0 -132
- package/dist/lambda/handler-runner/child-process-runner/childProcessHelper.js +0 -40
- package/dist/lambda/handler-runner/child-process-runner/index.js +0 -15
- package/dist/lambda/handler-runner/docker-runner/DockerContainer.js +0 -517
- package/dist/lambda/handler-runner/docker-runner/DockerImage.js +0 -67
- package/dist/lambda/handler-runner/docker-runner/DockerRunner.js +0 -74
- package/dist/lambda/handler-runner/docker-runner/index.js +0 -15
- package/dist/lambda/handler-runner/go-runner/GoRunner.js +0 -211
- package/dist/lambda/handler-runner/go-runner/index.js +0 -15
- package/dist/lambda/handler-runner/in-process-runner/InProcessRunner.js +0 -234
- package/dist/lambda/handler-runner/in-process-runner/index.js +0 -15
- package/dist/lambda/handler-runner/index.js +0 -15
- package/dist/lambda/handler-runner/java-runner/JavaRunner.js +0 -151
- package/dist/lambda/handler-runner/java-runner/index.js +0 -15
- package/dist/lambda/handler-runner/python-runner/PythonRunner.js +0 -180
- package/dist/lambda/handler-runner/python-runner/index.js +0 -15
- package/dist/lambda/handler-runner/ruby-runner/RubyRunner.js +0 -148
- package/dist/lambda/handler-runner/ruby-runner/index.js +0 -15
- package/dist/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +0 -94
- package/dist/lambda/handler-runner/worker-thread-runner/index.js +0 -15
- package/dist/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +0 -30
- package/dist/lambda/index.js +0 -15
- package/dist/lambda/routes/index.js +0 -23
- package/dist/lambda/routes/invocations/InvocationsController.js +0 -142
- package/dist/lambda/routes/invocations/index.js +0 -15
- package/dist/lambda/routes/invocations/invocationsRoute.js +0 -90
- package/dist/lambda/routes/invoke-async/InvokeAsyncController.js +0 -38
- package/dist/lambda/routes/invoke-async/index.js +0 -15
- package/dist/lambda/routes/invoke-async/invokeAsyncRoute.js +0 -43
- package/dist/main.js +0 -11
- package/dist/serverlessLog.js +0 -91
- package/dist/utils/checkDockerDaemon.js +0 -27
- package/dist/utils/checkGoVersion.js +0 -27
- package/dist/utils/createApiKey.js +0 -12
- package/dist/utils/createUniqueId.js +0 -14
- package/dist/utils/detectExecutable.js +0 -21
- package/dist/utils/generateHapiPath.js +0 -28
- package/dist/utils/getHttpApiCorsConfig.js +0 -44
- package/dist/utils/index.js +0 -158
- package/dist/utils/jsonPath.js +0 -21
- package/dist/utils/parseHeaders.js +0 -23
- package/dist/utils/parseMultiValueHeaders.js +0 -36
- package/dist/utils/parseMultiValueQueryStringParameters.js +0 -40
- package/dist/utils/parseQueryStringParameters.js +0 -26
- package/dist/utils/resolveJoins.js +0 -34
- package/dist/utils/satisfiesVersionRange.js +0 -20
- package/dist/utils/splitHandlerPathAndName.js +0 -41
- package/dist/utils/unflatten.js +0 -18
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import process, { exit } from 'node:process'
|
|
6
|
+
import h2o2 from '@hapi/h2o2'
|
|
7
|
+
import { Server } from '@hapi/hapi'
|
|
8
|
+
import { log } from '@serverless/utils/log.js'
|
|
9
|
+
import authFunctionNameExtractor from '../authFunctionNameExtractor.js'
|
|
10
|
+
import authJWTSettingsExtractor from './authJWTSettingsExtractor.js'
|
|
11
|
+
import createAuthScheme from './createAuthScheme.js'
|
|
12
|
+
import createJWTAuthScheme from './createJWTAuthScheme.js'
|
|
13
|
+
import Endpoint from './Endpoint.js'
|
|
14
|
+
import {
|
|
15
|
+
LambdaIntegrationEvent,
|
|
16
|
+
LambdaProxyIntegrationEvent,
|
|
17
|
+
renderVelocityTemplateObject,
|
|
18
|
+
VelocityContext,
|
|
19
|
+
} from './lambda-events/index.js'
|
|
20
|
+
import LambdaProxyIntegrationEventV2 from './lambda-events/LambdaProxyIntegrationEventV2.js'
|
|
21
|
+
import parseResources from './parseResources.js'
|
|
22
|
+
import payloadSchemaValidator from './payloadSchemaValidator.js'
|
|
23
|
+
import logRoutes from '../../utils/logRoutes.js'
|
|
24
|
+
import {
|
|
25
|
+
detectEncoding,
|
|
26
|
+
generateHapiPath,
|
|
27
|
+
getHttpApiCorsConfig,
|
|
28
|
+
jsonPath,
|
|
29
|
+
splitHandlerPathAndName,
|
|
30
|
+
} from '../../utils/index.js'
|
|
31
|
+
|
|
32
|
+
const { parse, stringify } = JSON
|
|
33
|
+
const { assign, entries, keys } = Object
|
|
34
|
+
|
|
35
|
+
export default class HttpServer {
|
|
36
|
+
#lambda = null
|
|
37
|
+
|
|
38
|
+
#lastRequestOptions = null
|
|
39
|
+
|
|
40
|
+
#options = null
|
|
41
|
+
|
|
42
|
+
#serverless = null
|
|
43
|
+
|
|
44
|
+
#server = null
|
|
45
|
+
|
|
46
|
+
#terminalInfo = []
|
|
47
|
+
|
|
48
|
+
constructor(serverless, options, lambda) {
|
|
49
|
+
this.#lambda = lambda
|
|
50
|
+
this.#options = options
|
|
51
|
+
this.#serverless = serverless
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
enforceSecureCookies,
|
|
55
|
+
host,
|
|
56
|
+
httpPort,
|
|
57
|
+
httpsProtocol,
|
|
58
|
+
noStripTrailingSlashInUrl,
|
|
59
|
+
} = this.#options
|
|
60
|
+
|
|
61
|
+
const serverOptions = {
|
|
62
|
+
host,
|
|
63
|
+
port: httpPort,
|
|
64
|
+
router: {
|
|
65
|
+
// allows for paths with trailing slashes to be the same as without
|
|
66
|
+
// e.g. : /my-path is the same as /my-path/
|
|
67
|
+
stripTrailingSlash: !noStripTrailingSlashInUrl,
|
|
68
|
+
},
|
|
69
|
+
state: enforceSecureCookies
|
|
70
|
+
? {
|
|
71
|
+
isHttpOnly: true,
|
|
72
|
+
isSameSite: false,
|
|
73
|
+
isSecure: true,
|
|
74
|
+
}
|
|
75
|
+
: {
|
|
76
|
+
isHttpOnly: false,
|
|
77
|
+
isSameSite: false,
|
|
78
|
+
isSecure: false,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// HTTPS support
|
|
83
|
+
if (typeof httpsProtocol === 'string' && httpsProtocol.length > 0) {
|
|
84
|
+
serverOptions.tls = {
|
|
85
|
+
cert: readFileSync(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
|
|
86
|
+
key: readFileSync(resolve(httpsProtocol, 'key.pem'), 'ascii'),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Hapijs server creation
|
|
91
|
+
this.#server = new Server(serverOptions)
|
|
92
|
+
|
|
93
|
+
// Enable CORS preflight response
|
|
94
|
+
this.#server.ext('onPreResponse', (request, h) => {
|
|
95
|
+
if (request.headers.origin) {
|
|
96
|
+
const response = request.response.isBoom
|
|
97
|
+
? request.response.output
|
|
98
|
+
: request.response
|
|
99
|
+
|
|
100
|
+
const explicitlySetHeaders = { ...response.headers }
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
this.#serverless.service.provider.httpApi &&
|
|
104
|
+
this.#serverless.service.provider.httpApi.cors
|
|
105
|
+
) {
|
|
106
|
+
const httpApiCors = getHttpApiCorsConfig(
|
|
107
|
+
this.#serverless.service.provider.httpApi.cors,
|
|
108
|
+
this,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (request.method === 'options') {
|
|
112
|
+
response.statusCode = 204
|
|
113
|
+
const allowAllOrigins =
|
|
114
|
+
httpApiCors.allowedOrigins.length === 1 &&
|
|
115
|
+
httpApiCors.allowedOrigins[0] === '*'
|
|
116
|
+
if (
|
|
117
|
+
!allowAllOrigins &&
|
|
118
|
+
!httpApiCors.allowedOrigins.includes(request.headers.origin)
|
|
119
|
+
) {
|
|
120
|
+
return h.continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
response.headers['access-control-allow-origin'] =
|
|
125
|
+
request.headers.origin
|
|
126
|
+
if (httpApiCors.allowCredentials) {
|
|
127
|
+
response.headers['access-control-allow-credentials'] = 'true'
|
|
128
|
+
}
|
|
129
|
+
if (httpApiCors.maxAge) {
|
|
130
|
+
response.headers['access-control-max-age'] = httpApiCors.maxAge
|
|
131
|
+
}
|
|
132
|
+
if (httpApiCors.exposedResponseHeaders) {
|
|
133
|
+
response.headers['access-control-expose-headers'] =
|
|
134
|
+
httpApiCors.exposedResponseHeaders.join(',')
|
|
135
|
+
}
|
|
136
|
+
if (httpApiCors.allowedMethods) {
|
|
137
|
+
response.headers['access-control-allow-methods'] =
|
|
138
|
+
httpApiCors.allowedMethods.join(',')
|
|
139
|
+
}
|
|
140
|
+
if (httpApiCors.allowedHeaders) {
|
|
141
|
+
response.headers['access-control-allow-headers'] =
|
|
142
|
+
httpApiCors.allowedHeaders.join(',')
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
response.headers['access-control-allow-origin'] =
|
|
146
|
+
request.headers.origin
|
|
147
|
+
response.headers['access-control-allow-credentials'] = 'true'
|
|
148
|
+
|
|
149
|
+
if (request.method === 'options') {
|
|
150
|
+
response.statusCode = 200
|
|
151
|
+
|
|
152
|
+
if (request.headers['access-control-expose-headers']) {
|
|
153
|
+
response.headers['access-control-expose-headers'] =
|
|
154
|
+
request.headers['access-control-expose-headers']
|
|
155
|
+
} else {
|
|
156
|
+
response.headers['access-control-expose-headers'] =
|
|
157
|
+
'content-type, content-length, etag'
|
|
158
|
+
}
|
|
159
|
+
response.headers['access-control-max-age'] = 60 * 10
|
|
160
|
+
|
|
161
|
+
if (request.headers['access-control-request-headers']) {
|
|
162
|
+
response.headers['access-control-allow-headers'] =
|
|
163
|
+
request.headers['access-control-request-headers']
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (request.headers['access-control-request-method']) {
|
|
167
|
+
response.headers['access-control-allow-methods'] =
|
|
168
|
+
request.headers['access-control-request-method']
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Override default headers with headers that have been explicitly set
|
|
173
|
+
entries(explicitlySetHeaders).forEach(([key, value]) => {
|
|
174
|
+
if (value) {
|
|
175
|
+
response.headers[key] = value
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return h.continue
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async start() {
|
|
185
|
+
const { host, httpPort, httpsProtocol } = this.#options
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await this.#server.start()
|
|
189
|
+
} catch (err) {
|
|
190
|
+
log.error(
|
|
191
|
+
`Unexpected error while starting serverless-offline server on port ${httpPort}:`,
|
|
192
|
+
err,
|
|
193
|
+
)
|
|
194
|
+
exit(1)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TODO move the following block
|
|
198
|
+
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
|
|
199
|
+
|
|
200
|
+
log.notice(`Server ready: ${server} 🚀`)
|
|
201
|
+
log.notice()
|
|
202
|
+
log.notice('Enter "rp" to replay the last request')
|
|
203
|
+
|
|
204
|
+
process.openStdin().addListener('data', (data) => {
|
|
205
|
+
// note: data is an object, and when converted to a string it will
|
|
206
|
+
// end with a linefeed. so we (rather crudely) account for that
|
|
207
|
+
// with toString() and then trim()
|
|
208
|
+
if (data.toString().trim() === 'rp') {
|
|
209
|
+
this.#injectLastRequest()
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// stops the server
|
|
215
|
+
stop(timeout) {
|
|
216
|
+
return this.#server.stop({
|
|
217
|
+
timeout,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async registerPlugins() {
|
|
222
|
+
try {
|
|
223
|
+
await this.#server.register([h2o2])
|
|
224
|
+
} catch (err) {
|
|
225
|
+
log.error(err)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#logPluginIssue() {
|
|
230
|
+
log.notice(
|
|
231
|
+
'If you think this is an issue with the plugin please submit it, thanks!\nhttps://github.com/dherault/serverless-offline/issues',
|
|
232
|
+
)
|
|
233
|
+
log.notice()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#extractJWTAuthSettings(endpoint) {
|
|
237
|
+
const result = authJWTSettingsExtractor(
|
|
238
|
+
endpoint,
|
|
239
|
+
this.#serverless.service.provider,
|
|
240
|
+
this.#options.ignoreJWTSignature,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return result.unsupportedAuth ? null : result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#configureJWTAuthorization(endpoint, functionKey, method, path) {
|
|
247
|
+
if (!endpoint.authorizer) {
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// right now _configureJWTAuthorization only handles AWS HttpAPI Gateway JWT
|
|
252
|
+
// authorizers that are defined in the serverless file
|
|
253
|
+
if (
|
|
254
|
+
this.#serverless.service.provider.name !== 'aws' ||
|
|
255
|
+
!endpoint.isHttpApi
|
|
256
|
+
) {
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const jwtSettings = this.#extractJWTAuthSettings(endpoint)
|
|
261
|
+
if (!jwtSettings) {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
log.notice(`Configuring JWT Authorization: ${method} ${path}`)
|
|
266
|
+
|
|
267
|
+
// Create a unique scheme per endpoint
|
|
268
|
+
// This allows the methodArn on the event property to be set appropriately
|
|
269
|
+
const authKey = `${functionKey}-${jwtSettings.authorizerName}-${method}-${path}`
|
|
270
|
+
const authSchemeName = `scheme-${authKey}`
|
|
271
|
+
const authStrategyName = `strategy-${authKey}` // set strategy name for the route config
|
|
272
|
+
|
|
273
|
+
log.debug(`Creating Authorization scheme for ${authKey}`)
|
|
274
|
+
|
|
275
|
+
// Create the Auth Scheme for the endpoint
|
|
276
|
+
const scheme = createJWTAuthScheme(jwtSettings, this)
|
|
277
|
+
|
|
278
|
+
// Set the auth scheme and strategy on the server
|
|
279
|
+
this.#server.auth.scheme(authSchemeName, scheme)
|
|
280
|
+
this.#server.auth.strategy(authStrategyName, authSchemeName)
|
|
281
|
+
|
|
282
|
+
return authStrategyName
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#extractAuthFunctionName(endpoint) {
|
|
286
|
+
const result = authFunctionNameExtractor(endpoint)
|
|
287
|
+
|
|
288
|
+
return result.unsupportedAuth ? null : result.authorizerName
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#configureAuthorization(endpoint, functionKey, method, path) {
|
|
292
|
+
if (!endpoint.authorizer) {
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const authFunctionName = this.#extractAuthFunctionName(endpoint)
|
|
297
|
+
|
|
298
|
+
if (!authFunctionName) {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
log.notice(`Configuring Authorization: ${path} ${authFunctionName}`)
|
|
303
|
+
|
|
304
|
+
const authFunction = this.#serverless.service.getFunction(authFunctionName)
|
|
305
|
+
|
|
306
|
+
if (!authFunction) {
|
|
307
|
+
log.error(`Authorization function ${authFunctionName} does not exist`)
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const authorizerOptions = {
|
|
312
|
+
identitySource: 'method.request.header.Authorization',
|
|
313
|
+
identityValidationExpression: '(.*)',
|
|
314
|
+
resultTtlInSeconds: '300',
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (typeof endpoint.authorizer === 'string') {
|
|
318
|
+
authorizerOptions.name = authFunctionName
|
|
319
|
+
} else {
|
|
320
|
+
assign(authorizerOptions, endpoint.authorizer)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Create a unique scheme per endpoint
|
|
324
|
+
// This allows the methodArn on the event property to be set appropriately
|
|
325
|
+
const authKey = `${functionKey}-${authFunctionName}-${method}-${path}`
|
|
326
|
+
const authSchemeName = `scheme-${authKey}`
|
|
327
|
+
const authStrategyName = `strategy-${authKey}` // set strategy name for the route config
|
|
328
|
+
|
|
329
|
+
log.debug(`Creating Authorization scheme for ${authKey}`)
|
|
330
|
+
|
|
331
|
+
// Create the Auth Scheme for the endpoint
|
|
332
|
+
const scheme = createAuthScheme(
|
|
333
|
+
authorizerOptions,
|
|
334
|
+
this.#serverless.service.provider,
|
|
335
|
+
this.#lambda,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// Set the auth scheme and strategy on the server
|
|
339
|
+
this.#server.auth.scheme(authSchemeName, scheme)
|
|
340
|
+
this.#server.auth.strategy(authStrategyName, authSchemeName)
|
|
341
|
+
|
|
342
|
+
return authStrategyName
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#setAuthorizationStrategy(endpoint, functionKey, method, path) {
|
|
346
|
+
/*
|
|
347
|
+
* The authentication strategy can be provided outside of this project
|
|
348
|
+
* by injecting the provider through a custom variable in the serverless.yml.
|
|
349
|
+
*
|
|
350
|
+
* see the example in the tests for more details
|
|
351
|
+
* /tests/integration/custom-authentication
|
|
352
|
+
*/
|
|
353
|
+
const customizations = this.#serverless.service.custom
|
|
354
|
+
if (
|
|
355
|
+
customizations &&
|
|
356
|
+
customizations.offline?.customAuthenticationProvider
|
|
357
|
+
) {
|
|
358
|
+
const root = resolve(this.#serverless.serviceDir, 'require-resolver')
|
|
359
|
+
const customRequire = createRequire(root)
|
|
360
|
+
|
|
361
|
+
const provider = customRequire(
|
|
362
|
+
customizations.offline.customAuthenticationProvider,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
const strategy = provider(endpoint, functionKey, method, path)
|
|
366
|
+
this.#server.auth.scheme(
|
|
367
|
+
strategy.scheme,
|
|
368
|
+
strategy.getAuthenticateFunction,
|
|
369
|
+
)
|
|
370
|
+
this.#server.auth.strategy(strategy.name, strategy.scheme)
|
|
371
|
+
return strategy.name
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// If the endpoint has an authorization function, create an authStrategy for the route
|
|
375
|
+
const authStrategyName = this.#options.noAuth
|
|
376
|
+
? null
|
|
377
|
+
: this.#configureJWTAuthorization(endpoint, functionKey, method, path) ||
|
|
378
|
+
this.#configureAuthorization(endpoint, functionKey, method, path)
|
|
379
|
+
return authStrategyName
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
createRoutes(functionKey, httpEvent, handler) {
|
|
383
|
+
const [handlerPath] = splitHandlerPathAndName(handler)
|
|
384
|
+
|
|
385
|
+
let method
|
|
386
|
+
let path
|
|
387
|
+
let hapiPath
|
|
388
|
+
|
|
389
|
+
if (httpEvent.isHttpApi) {
|
|
390
|
+
if (httpEvent.routeKey === '$default') {
|
|
391
|
+
method = 'ANY'
|
|
392
|
+
path = httpEvent.routeKey
|
|
393
|
+
hapiPath = '/{default*}'
|
|
394
|
+
} else {
|
|
395
|
+
;[method, path] = httpEvent.routeKey.split(' ')
|
|
396
|
+
hapiPath = generateHapiPath(
|
|
397
|
+
path,
|
|
398
|
+
{
|
|
399
|
+
...this.#options,
|
|
400
|
+
noPrependStageInUrl: true, // Serverless always uses the $default stage
|
|
401
|
+
},
|
|
402
|
+
this.#serverless,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
method = httpEvent.method.toUpperCase()
|
|
407
|
+
;({ path } = httpEvent)
|
|
408
|
+
hapiPath = generateHapiPath(path, this.#options, this.#serverless)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const endpoint = new Endpoint(
|
|
412
|
+
join(this.#serverless.config.servicePath, handlerPath),
|
|
413
|
+
httpEvent,
|
|
414
|
+
).generate()
|
|
415
|
+
|
|
416
|
+
const stage = endpoint.isHttpApi
|
|
417
|
+
? '$default'
|
|
418
|
+
: this.#options.stage || this.#serverless.service.provider.stage
|
|
419
|
+
const protectedRoutes = []
|
|
420
|
+
|
|
421
|
+
if (httpEvent.private) {
|
|
422
|
+
protectedRoutes.push(`${method}#${hapiPath}`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const { host, httpPort, httpsProtocol } = this.#options
|
|
426
|
+
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
|
|
427
|
+
|
|
428
|
+
this.#terminalInfo.push({
|
|
429
|
+
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
|
|
430
|
+
method,
|
|
431
|
+
path: hapiPath,
|
|
432
|
+
server,
|
|
433
|
+
stage:
|
|
434
|
+
endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const authStrategyName = this.#setAuthorizationStrategy(
|
|
438
|
+
endpoint,
|
|
439
|
+
functionKey,
|
|
440
|
+
method,
|
|
441
|
+
path,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
let cors = null
|
|
445
|
+
if (endpoint.cors) {
|
|
446
|
+
cors = {
|
|
447
|
+
credentials:
|
|
448
|
+
endpoint.cors.credentials || this.#options.corsConfig.credentials,
|
|
449
|
+
exposedHeaders: this.#options.corsConfig.exposedHeaders,
|
|
450
|
+
headers: endpoint.cors.headers || this.#options.corsConfig.headers,
|
|
451
|
+
origin: endpoint.cors.origins || this.#options.corsConfig.origin,
|
|
452
|
+
}
|
|
453
|
+
} else if (
|
|
454
|
+
this.#serverless.service.provider.httpApi &&
|
|
455
|
+
this.#serverless.service.provider.httpApi.cors
|
|
456
|
+
) {
|
|
457
|
+
const httpApiCors = getHttpApiCorsConfig(
|
|
458
|
+
this.#serverless.service.provider.httpApi.cors,
|
|
459
|
+
this,
|
|
460
|
+
)
|
|
461
|
+
cors = {
|
|
462
|
+
credentials: httpApiCors.allowCredentials,
|
|
463
|
+
exposedHeaders: httpApiCors.exposedResponseHeaders || [],
|
|
464
|
+
headers: httpApiCors.allowedHeaders || [],
|
|
465
|
+
maxAge: httpApiCors.maxAge,
|
|
466
|
+
origin: httpApiCors.allowedOrigins || [],
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const hapiMethod = method === 'ANY' ? '*' : method
|
|
471
|
+
|
|
472
|
+
const state = this.#options.disableCookieValidation
|
|
473
|
+
? {
|
|
474
|
+
failAction: 'ignore',
|
|
475
|
+
parse: false,
|
|
476
|
+
}
|
|
477
|
+
: {
|
|
478
|
+
failAction: 'error',
|
|
479
|
+
parse: true,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const hapiOptions = {
|
|
483
|
+
auth: authStrategyName,
|
|
484
|
+
cors,
|
|
485
|
+
state,
|
|
486
|
+
timeout: { socket: false },
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
490
|
+
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
491
|
+
if (hapiMethod === 'HEAD') {
|
|
492
|
+
log.notice(
|
|
493
|
+
'HEAD method event detected. Skipping HAPI server route mapping',
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
|
|
500
|
+
// maxBytes: Increase request size from 1MB default limit to 10MB.
|
|
501
|
+
// Cf AWS API GW payload limits.
|
|
502
|
+
hapiOptions.payload = {
|
|
503
|
+
maxBytes: 1024 * 1024 * 10,
|
|
504
|
+
parse: false,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const additionalRequestContext = {}
|
|
509
|
+
if (httpEvent.operationId) {
|
|
510
|
+
additionalRequestContext.operationName = httpEvent.operationId
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
hapiOptions.tags = ['api']
|
|
514
|
+
|
|
515
|
+
const hapiHandler = async (request, h) => {
|
|
516
|
+
// Here we go
|
|
517
|
+
// Store current request as the last one
|
|
518
|
+
this.#lastRequestOptions = {
|
|
519
|
+
headers: request.headers,
|
|
520
|
+
method: request.method,
|
|
521
|
+
payload: request.payload,
|
|
522
|
+
url: request.url.href,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const requestPath =
|
|
526
|
+
endpoint.isHttpApi || this.#options.noPrependStageInUrl
|
|
527
|
+
? request.path
|
|
528
|
+
: request.path.substr(`/${stage}`.length)
|
|
529
|
+
|
|
530
|
+
if (request.auth.credentials && request.auth.strategy) {
|
|
531
|
+
this.#lastRequestOptions.auth = request.auth
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Payload processing
|
|
535
|
+
const encoding = detectEncoding(request)
|
|
536
|
+
|
|
537
|
+
request.payload = request.payload && request.payload.toString(encoding)
|
|
538
|
+
request.rawPayload = request.payload
|
|
539
|
+
|
|
540
|
+
// Incomming request message
|
|
541
|
+
log.notice()
|
|
542
|
+
|
|
543
|
+
log.notice()
|
|
544
|
+
log.notice(`${method} ${request.path} (λ: ${functionKey})`)
|
|
545
|
+
|
|
546
|
+
// Check for APIKey
|
|
547
|
+
if (
|
|
548
|
+
(protectedRoutes.includes(`${hapiMethod}#${hapiPath}`) ||
|
|
549
|
+
protectedRoutes.includes(`ANY#${hapiPath}`)) &&
|
|
550
|
+
!this.#options.noAuth
|
|
551
|
+
) {
|
|
552
|
+
const errorResponse = () =>
|
|
553
|
+
h
|
|
554
|
+
.response({ message: 'Forbidden' })
|
|
555
|
+
.code(403)
|
|
556
|
+
.type('application/json')
|
|
557
|
+
.header('x-amzn-ErrorType', 'ForbiddenException')
|
|
558
|
+
|
|
559
|
+
const requestToken = request.headers['x-api-key']
|
|
560
|
+
|
|
561
|
+
if (requestToken) {
|
|
562
|
+
if (requestToken !== this.#options.apiKey) {
|
|
563
|
+
log.debug(
|
|
564
|
+
`Method ${method} of function ${functionKey} token ${requestToken} not valid`,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return errorResponse()
|
|
568
|
+
}
|
|
569
|
+
} else if (
|
|
570
|
+
request.auth &&
|
|
571
|
+
request.auth.credentials &&
|
|
572
|
+
request.auth.credentials.usageIdentifierKey
|
|
573
|
+
) {
|
|
574
|
+
const { usageIdentifierKey } = request.auth.credentials
|
|
575
|
+
|
|
576
|
+
if (usageIdentifierKey !== this.#options.apiKey) {
|
|
577
|
+
log.debug(
|
|
578
|
+
`Method ${method} of function ${functionKey} token ${usageIdentifierKey} not valid`,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return errorResponse()
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
log.debug(`Missing x-api-key on private function ${functionKey}`)
|
|
585
|
+
|
|
586
|
+
return errorResponse()
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const response = h.response()
|
|
591
|
+
const contentType = request.mime || 'application/json' // default content type
|
|
592
|
+
|
|
593
|
+
const { integration, requestTemplates } = endpoint
|
|
594
|
+
|
|
595
|
+
// default request template to '' if we don't have a definition pushed in from serverless or endpoint
|
|
596
|
+
const requestTemplate =
|
|
597
|
+
typeof requestTemplates !== 'undefined' && integration === 'AWS'
|
|
598
|
+
? requestTemplates[contentType]
|
|
599
|
+
: ''
|
|
600
|
+
|
|
601
|
+
const schemas =
|
|
602
|
+
typeof endpoint?.request?.schemas !== 'undefined'
|
|
603
|
+
? endpoint.request.schemas[contentType]
|
|
604
|
+
: ''
|
|
605
|
+
|
|
606
|
+
// https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
|
|
607
|
+
// so we have to do it ourselves
|
|
608
|
+
const contentTypesThatRequirePayloadParsing = [
|
|
609
|
+
'application/json',
|
|
610
|
+
'application/vnd.api+json',
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
contentTypesThatRequirePayloadParsing.includes(contentType) &&
|
|
615
|
+
request.payload &&
|
|
616
|
+
request.payload.length > 1
|
|
617
|
+
) {
|
|
618
|
+
try {
|
|
619
|
+
if (!request.payload || request.payload.length < 1) {
|
|
620
|
+
request.payload = '{}'
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
request.payload = parse(request.payload)
|
|
624
|
+
} catch (err) {
|
|
625
|
+
log.debug('error in converting request.payload to JSON:', err)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
log.debug('contentType:', contentType)
|
|
630
|
+
log.debug('requestTemplate:', requestTemplate)
|
|
631
|
+
log.debug('payload:', request.payload)
|
|
632
|
+
|
|
633
|
+
/* REQUEST PAYLOAD SCHEMA VALIDATION */
|
|
634
|
+
if (schemas) {
|
|
635
|
+
log.debug('schemas:', schemas)
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
payloadSchemaValidator(schemas, request.payload)
|
|
639
|
+
} catch (err) {
|
|
640
|
+
return this.#reply400(response, err.message, err)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* REQUEST TEMPLATE PROCESSING (event population) */
|
|
645
|
+
|
|
646
|
+
let event = {}
|
|
647
|
+
|
|
648
|
+
if (integration === 'AWS') {
|
|
649
|
+
if (requestTemplate) {
|
|
650
|
+
try {
|
|
651
|
+
log.debug('_____ REQUEST TEMPLATE PROCESSING _____')
|
|
652
|
+
|
|
653
|
+
event = new LambdaIntegrationEvent(
|
|
654
|
+
request,
|
|
655
|
+
stage,
|
|
656
|
+
requestTemplate,
|
|
657
|
+
requestPath,
|
|
658
|
+
).create()
|
|
659
|
+
} catch (err) {
|
|
660
|
+
return this.#reply502(
|
|
661
|
+
response,
|
|
662
|
+
`Error while parsing template "${contentType}" for ${functionKey}`,
|
|
663
|
+
err,
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
} else if (typeof request.payload === 'object') {
|
|
667
|
+
event = request.payload || {}
|
|
668
|
+
}
|
|
669
|
+
} else if (integration === 'AWS_PROXY') {
|
|
670
|
+
const stageVariables = this.#serverless.service.custom
|
|
671
|
+
? this.#serverless.service.custom.stageVariables
|
|
672
|
+
: null
|
|
673
|
+
|
|
674
|
+
const lambdaProxyIntegrationEvent =
|
|
675
|
+
endpoint.isHttpApi && endpoint.payload === '2.0'
|
|
676
|
+
? new LambdaProxyIntegrationEventV2(
|
|
677
|
+
request,
|
|
678
|
+
stage,
|
|
679
|
+
endpoint.routeKey,
|
|
680
|
+
stageVariables,
|
|
681
|
+
additionalRequestContext,
|
|
682
|
+
)
|
|
683
|
+
: new LambdaProxyIntegrationEvent(
|
|
684
|
+
request,
|
|
685
|
+
stage,
|
|
686
|
+
requestPath,
|
|
687
|
+
stageVariables,
|
|
688
|
+
endpoint.isHttpApi ? endpoint.routeKey : null,
|
|
689
|
+
additionalRequestContext,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
event = lambdaProxyIntegrationEvent.create()
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
log.debug('event:', event)
|
|
696
|
+
|
|
697
|
+
const lambdaFunction = this.#lambda.get(functionKey)
|
|
698
|
+
|
|
699
|
+
lambdaFunction.setEvent(event)
|
|
700
|
+
|
|
701
|
+
let result
|
|
702
|
+
let err
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
result = await lambdaFunction.runHandler()
|
|
706
|
+
} catch (_err) {
|
|
707
|
+
err = _err
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// const processResponse = (err, data) => {
|
|
711
|
+
// Everything in this block happens once the lambda function has resolved
|
|
712
|
+
|
|
713
|
+
log.debug('_____ HANDLER RESOLVED _____')
|
|
714
|
+
|
|
715
|
+
let responseName = 'default'
|
|
716
|
+
const { contentHandling, responseContentType } = endpoint
|
|
717
|
+
|
|
718
|
+
/* RESPONSE SELECTION (among endpoint's possible responses) */
|
|
719
|
+
|
|
720
|
+
// Failure handling
|
|
721
|
+
let errorStatusCode = '502'
|
|
722
|
+
if (err) {
|
|
723
|
+
// Since the --useChildProcesses option loads the handler in
|
|
724
|
+
// a separate process and serverless-offline communicates with it
|
|
725
|
+
// over IPC, we are unable to catch JavaScript unhandledException errors
|
|
726
|
+
// when the handler code contains bad JavaScript. Instead, we "catch"
|
|
727
|
+
// it here and reply in the same way that we would have above when
|
|
728
|
+
// we lazy-load the non-IPC handler function.
|
|
729
|
+
if (this.#options.useChildProcesses && err.ipcException) {
|
|
730
|
+
return this.#reply502(
|
|
731
|
+
response,
|
|
732
|
+
`Error while loading ${functionKey}`,
|
|
733
|
+
err,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const errorMessage = (err.message || err).toString()
|
|
738
|
+
|
|
739
|
+
const re = /\[(\d{3})]/
|
|
740
|
+
const found = errorMessage.match(re)
|
|
741
|
+
|
|
742
|
+
if (found && found.length > 1) {
|
|
743
|
+
;[, errorStatusCode] = found
|
|
744
|
+
} else {
|
|
745
|
+
errorStatusCode = '502'
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Mocks Lambda errors
|
|
749
|
+
result = {
|
|
750
|
+
errorMessage,
|
|
751
|
+
errorType: err.constructor.name,
|
|
752
|
+
stackTrace: this.#getArrayStackTrace(err.stack),
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
log.error(errorMessage)
|
|
756
|
+
|
|
757
|
+
if (!this.#options.hideStackTraces) {
|
|
758
|
+
log.error(err.stack)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
for (const [key, value] of entries(endpoint.responses)) {
|
|
762
|
+
if (
|
|
763
|
+
key !== 'default' &&
|
|
764
|
+
errorMessage.match(`^${value.selectionPattern || key}$`)
|
|
765
|
+
) {
|
|
766
|
+
responseName = key
|
|
767
|
+
break
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
log.debug(`Using response '${responseName}'`)
|
|
773
|
+
|
|
774
|
+
const chosenResponse = endpoint.responses[responseName]
|
|
775
|
+
|
|
776
|
+
/* RESPONSE PARAMETERS PROCCESSING */
|
|
777
|
+
|
|
778
|
+
const { responseParameters } = chosenResponse
|
|
779
|
+
|
|
780
|
+
if (responseParameters) {
|
|
781
|
+
log.debug('_____ RESPONSE PARAMETERS PROCCESSING _____')
|
|
782
|
+
log.debug(
|
|
783
|
+
`Found ${
|
|
784
|
+
keys(responseParameters).length
|
|
785
|
+
} responseParameters for '${responseName}' response`,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
// responseParameters use the following shape: "key": "value"
|
|
789
|
+
entries(responseParameters).forEach(([key, value]) => {
|
|
790
|
+
const keyArray = key.split('.') // eg: "method.response.header.location"
|
|
791
|
+
const valueArray = value.split('.') // eg: "integration.response.body.redirect.url"
|
|
792
|
+
|
|
793
|
+
log.debug(`Processing responseParameter "${key}": "${value}"`)
|
|
794
|
+
|
|
795
|
+
// For now the plugin only supports modifying headers
|
|
796
|
+
if (key.startsWith('method.response.header') && keyArray[3]) {
|
|
797
|
+
const headerName = keyArray.slice(3).join('.')
|
|
798
|
+
let headerValue
|
|
799
|
+
|
|
800
|
+
log.debug('Found header in left-hand:', headerName)
|
|
801
|
+
|
|
802
|
+
if (value.startsWith('integration.response')) {
|
|
803
|
+
if (valueArray[2] === 'body') {
|
|
804
|
+
log.debug('Found body in right-hand')
|
|
805
|
+
|
|
806
|
+
headerValue = valueArray[3]
|
|
807
|
+
? jsonPath(result, valueArray.slice(3).join('.'))
|
|
808
|
+
: result
|
|
809
|
+
if (
|
|
810
|
+
typeof headerValue === 'undefined' ||
|
|
811
|
+
headerValue === null
|
|
812
|
+
) {
|
|
813
|
+
headerValue = ''
|
|
814
|
+
} else {
|
|
815
|
+
headerValue = headerValue.toString()
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
log.notice()
|
|
819
|
+
|
|
820
|
+
log.warning()
|
|
821
|
+
log.warning(
|
|
822
|
+
`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" (for "${key}"") instead. Skipping.`,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
this.#logPluginIssue()
|
|
826
|
+
log.notice()
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value // See #34
|
|
830
|
+
}
|
|
831
|
+
// Applies the header;
|
|
832
|
+
if (headerValue === '') {
|
|
833
|
+
log.warning(
|
|
834
|
+
`Empty value for responseParameter "${key}": "${value}", it won't be set`,
|
|
835
|
+
)
|
|
836
|
+
} else {
|
|
837
|
+
log.debug(
|
|
838
|
+
`Will assign "${headerValue}" to header "${headerName}"`,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
response.header(headerName, headerValue)
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
log.notice()
|
|
845
|
+
|
|
846
|
+
log.warning()
|
|
847
|
+
log.warning(
|
|
848
|
+
`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
this.#logPluginIssue()
|
|
852
|
+
log.notice()
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let statusCode = 200
|
|
858
|
+
|
|
859
|
+
if (integration === 'AWS') {
|
|
860
|
+
const endpointResponseHeaders =
|
|
861
|
+
(endpoint.response && endpoint.response.headers) || {}
|
|
862
|
+
|
|
863
|
+
entries(endpointResponseHeaders)
|
|
864
|
+
.filter(
|
|
865
|
+
([, value]) => typeof value === 'string' && /^'.*?'$/.test(value),
|
|
866
|
+
)
|
|
867
|
+
.forEach(([key, value]) => response.header(key, value.slice(1, -1)))
|
|
868
|
+
|
|
869
|
+
/* LAMBDA INTEGRATION RESPONSE TEMPLATE PROCCESSING */
|
|
870
|
+
|
|
871
|
+
// If there is a responseTemplate, we apply it to the result
|
|
872
|
+
const { responseTemplates } = chosenResponse
|
|
873
|
+
|
|
874
|
+
if (typeof responseTemplates === 'object') {
|
|
875
|
+
if (keys(responseTemplates).length) {
|
|
876
|
+
// BAD IMPLEMENTATION: first key in responseTemplates
|
|
877
|
+
const responseTemplate = responseTemplates[responseContentType]
|
|
878
|
+
|
|
879
|
+
if (responseTemplate && responseTemplate !== '\n') {
|
|
880
|
+
log.debug('_____ RESPONSE TEMPLATE PROCCESSING _____')
|
|
881
|
+
log.debug(`Using responseTemplate '${responseContentType}'`)
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
const reponseContext = new VelocityContext(
|
|
885
|
+
request,
|
|
886
|
+
stage,
|
|
887
|
+
result,
|
|
888
|
+
).getContext()
|
|
889
|
+
|
|
890
|
+
result = renderVelocityTemplateObject(
|
|
891
|
+
{ root: responseTemplate },
|
|
892
|
+
reponseContext,
|
|
893
|
+
).root
|
|
894
|
+
} catch (error) {
|
|
895
|
+
log.error(
|
|
896
|
+
`Error while parsing responseTemplate '${responseContentType}' for lambda ${functionKey}:\n${error.stack}`,
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* LAMBDA INTEGRATION HAPIJS RESPONSE CONFIGURATION */
|
|
904
|
+
statusCode = chosenResponse.statusCode || 200
|
|
905
|
+
|
|
906
|
+
if (err) {
|
|
907
|
+
statusCode = errorStatusCode
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!chosenResponse.statusCode) {
|
|
911
|
+
log.notice()
|
|
912
|
+
|
|
913
|
+
log.warning()
|
|
914
|
+
log.warning(`No statusCode found for response "${responseName}".`)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
response.header('Content-Type', responseContentType, {
|
|
918
|
+
override: false, // Maybe a responseParameter set it already. See #34
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
response.statusCode = statusCode
|
|
922
|
+
|
|
923
|
+
if (contentHandling === 'CONVERT_TO_BINARY') {
|
|
924
|
+
response.encoding = 'binary'
|
|
925
|
+
response.source = Buffer.from(result, 'base64')
|
|
926
|
+
response.variety = 'buffer'
|
|
927
|
+
} else if (typeof result === 'string') {
|
|
928
|
+
response.source = stringify(result)
|
|
929
|
+
} else if (result && result.body && typeof result.body !== 'string') {
|
|
930
|
+
return this.#reply502(
|
|
931
|
+
response,
|
|
932
|
+
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
|
|
933
|
+
{},
|
|
934
|
+
)
|
|
935
|
+
} else {
|
|
936
|
+
response.source = result
|
|
937
|
+
}
|
|
938
|
+
} else if (integration === 'AWS_PROXY') {
|
|
939
|
+
/* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */
|
|
940
|
+
|
|
941
|
+
if (
|
|
942
|
+
endpoint.isHttpApi &&
|
|
943
|
+
endpoint.payload === '2.0' &&
|
|
944
|
+
(typeof result === 'string' || !result.statusCode)
|
|
945
|
+
) {
|
|
946
|
+
const body = typeof result === 'string' ? result : stringify(result)
|
|
947
|
+
result = {
|
|
948
|
+
body,
|
|
949
|
+
headers: {
|
|
950
|
+
'Content-Type': 'application/json',
|
|
951
|
+
},
|
|
952
|
+
isBase64Encoded: false,
|
|
953
|
+
statusCode: 200,
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (result && !result.errorType) {
|
|
958
|
+
statusCode = result.statusCode || 200
|
|
959
|
+
} else {
|
|
960
|
+
statusCode = 502
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
response.statusCode = statusCode
|
|
964
|
+
|
|
965
|
+
const headers = {}
|
|
966
|
+
|
|
967
|
+
if (result && result.headers) {
|
|
968
|
+
entries(result.headers).forEach(([headerKey, headerValue]) => {
|
|
969
|
+
headers[headerKey] = (headers[headerKey] || []).concat(headerValue)
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
if (result && result.multiValueHeaders) {
|
|
973
|
+
entries(result.multiValueHeaders).forEach(
|
|
974
|
+
([headerKey, headerValue]) => {
|
|
975
|
+
headers[headerKey] = (headers[headerKey] || []).concat(
|
|
976
|
+
headerValue,
|
|
977
|
+
)
|
|
978
|
+
},
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
log.debug('headers', headers)
|
|
983
|
+
|
|
984
|
+
const parseCookies = (headerValue) => {
|
|
985
|
+
const cookieName = headerValue.slice(0, headerValue.indexOf('='))
|
|
986
|
+
const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1)
|
|
987
|
+
h.state(cookieName, cookieValue, {
|
|
988
|
+
encoding: 'none',
|
|
989
|
+
strictHeader: false,
|
|
990
|
+
})
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
entries(headers).forEach(([headerKey, headerValue]) => {
|
|
994
|
+
if (headerKey.toLowerCase() === 'set-cookie') {
|
|
995
|
+
headerValue.forEach(parseCookies)
|
|
996
|
+
} else {
|
|
997
|
+
headerValue.forEach((value) => {
|
|
998
|
+
// it looks like Hapi doesn't support multiple headers with the same name,
|
|
999
|
+
// appending values is the closest we can come to the AWS behavior.
|
|
1000
|
+
response.header(headerKey, value, { append: true })
|
|
1001
|
+
})
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
if (
|
|
1006
|
+
endpoint.isHttpApi &&
|
|
1007
|
+
endpoint.payload === '2.0' &&
|
|
1008
|
+
result.cookies
|
|
1009
|
+
) {
|
|
1010
|
+
result.cookies.forEach(parseCookies)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
response.header('Content-Type', 'application/json', {
|
|
1014
|
+
duplicate: false,
|
|
1015
|
+
override: false,
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
if (typeof result === 'string') {
|
|
1019
|
+
response.source = stringify(result)
|
|
1020
|
+
} else if (result && typeof result.body !== 'undefined') {
|
|
1021
|
+
if (result.isBase64Encoded) {
|
|
1022
|
+
response.encoding = 'binary'
|
|
1023
|
+
response.source = Buffer.from(result.body, 'base64')
|
|
1024
|
+
response.variety = 'buffer'
|
|
1025
|
+
} else {
|
|
1026
|
+
if (result && result.body && typeof result.body !== 'string') {
|
|
1027
|
+
return this.#reply502(
|
|
1028
|
+
response,
|
|
1029
|
+
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
|
|
1030
|
+
{},
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
response.source = result.body
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Log response
|
|
1039
|
+
let whatToLog = result
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
whatToLog = stringify(result)
|
|
1043
|
+
} catch {
|
|
1044
|
+
// nothing
|
|
1045
|
+
} finally {
|
|
1046
|
+
if (this.#options.printOutput) {
|
|
1047
|
+
log.notice(
|
|
1048
|
+
err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`,
|
|
1049
|
+
)
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Bon voyage!
|
|
1054
|
+
return response
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
this.#server.route({
|
|
1058
|
+
handler: hapiHandler,
|
|
1059
|
+
method: hapiMethod,
|
|
1060
|
+
options: hapiOptions,
|
|
1061
|
+
path: hapiPath,
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
#replyError(statusCode, response, message, error) {
|
|
1066
|
+
log.notice(message)
|
|
1067
|
+
|
|
1068
|
+
log.error(error)
|
|
1069
|
+
|
|
1070
|
+
response.header('Content-Type', 'application/json')
|
|
1071
|
+
|
|
1072
|
+
response.statusCode = statusCode
|
|
1073
|
+
response.source = {
|
|
1074
|
+
errorMessage: message,
|
|
1075
|
+
errorType: error.constructor.name,
|
|
1076
|
+
offlineInfo:
|
|
1077
|
+
'If you believe this is an issue with serverless-offline please submit it, thanks. https://github.com/dherault/serverless-offline/issues',
|
|
1078
|
+
stackTrace: this.#getArrayStackTrace(error.stack),
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return response
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Bad news
|
|
1085
|
+
#reply502(response, message, error) {
|
|
1086
|
+
// APIG replies 502 by default on failures;
|
|
1087
|
+
return this.#replyError(502, response, message, error)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
#reply400(response, message, error) {
|
|
1091
|
+
return this.#replyError(400, response, message, error)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
createResourceRoutes() {
|
|
1095
|
+
const resourceRoutesOptions = this.#options.resourceRoutes
|
|
1096
|
+
|
|
1097
|
+
if (!resourceRoutesOptions) {
|
|
1098
|
+
return
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const resourceRoutes = parseResources(this.#serverless.service.resources)
|
|
1102
|
+
|
|
1103
|
+
if (!resourceRoutes || !keys(resourceRoutes).length) {
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
log.notice()
|
|
1108
|
+
|
|
1109
|
+
log.notice()
|
|
1110
|
+
log.notice('Routes defined in resources:')
|
|
1111
|
+
|
|
1112
|
+
entries(resourceRoutes).forEach(([methodId, resourceRoutesObj]) => {
|
|
1113
|
+
const { isProxy, method, pathResource, proxyUri } = resourceRoutesObj
|
|
1114
|
+
|
|
1115
|
+
if (!isProxy) {
|
|
1116
|
+
log.warning(
|
|
1117
|
+
`Only HTTP_PROXY is supported. Path '${pathResource}' is ignored.`,
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
if (!pathResource) {
|
|
1123
|
+
log.warning(`Could not resolve path for '${methodId}'.`)
|
|
1124
|
+
|
|
1125
|
+
return
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const hapiPath = generateHapiPath(
|
|
1129
|
+
pathResource,
|
|
1130
|
+
this.#options,
|
|
1131
|
+
this.#serverless,
|
|
1132
|
+
)
|
|
1133
|
+
const proxyUriOverwrite = resourceRoutesOptions[methodId] || {}
|
|
1134
|
+
const proxyUriInUse = proxyUriOverwrite.Uri || proxyUri
|
|
1135
|
+
|
|
1136
|
+
if (!proxyUriInUse) {
|
|
1137
|
+
log.warning(`Could not load Proxy Uri for '${methodId}'`)
|
|
1138
|
+
|
|
1139
|
+
return
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const hapiMethod = method === 'ANY' ? '*' : method
|
|
1143
|
+
|
|
1144
|
+
const state = this.#options.disableCookieValidation
|
|
1145
|
+
? {
|
|
1146
|
+
failAction: 'ignore',
|
|
1147
|
+
parse: false,
|
|
1148
|
+
}
|
|
1149
|
+
: {
|
|
1150
|
+
failAction: 'error',
|
|
1151
|
+
parse: true,
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const hapiOptions = {
|
|
1155
|
+
cors: this.#options.corsConfig,
|
|
1156
|
+
state,
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
1160
|
+
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
1161
|
+
if (hapiMethod === 'HEAD') {
|
|
1162
|
+
log.notice(
|
|
1163
|
+
'HEAD method event detected. Skipping HAPI server route mapping',
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (hapiMethod !== 'GET' && hapiMethod !== 'HEAD') {
|
|
1170
|
+
hapiOptions.payload = { parse: false }
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
log.notice(`${method} ${hapiPath} -> ${proxyUriInUse}`)
|
|
1174
|
+
|
|
1175
|
+
// hapiOptions.tags = ['api']
|
|
1176
|
+
|
|
1177
|
+
const route = {
|
|
1178
|
+
handler(request, h) {
|
|
1179
|
+
const { params } = request
|
|
1180
|
+
let resultUri = proxyUriInUse
|
|
1181
|
+
|
|
1182
|
+
entries(params).forEach(([key, value]) => {
|
|
1183
|
+
resultUri = resultUri.replace(`{${key}}`, value)
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
if (request.url.search !== null) {
|
|
1187
|
+
resultUri += request.url.search // search is empty string by default
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
log.notice(
|
|
1191
|
+
`PROXY ${request.method} ${request.url.pathname} -> ${resultUri}`,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
return h.proxy({
|
|
1195
|
+
passThrough: true,
|
|
1196
|
+
uri: resultUri,
|
|
1197
|
+
})
|
|
1198
|
+
},
|
|
1199
|
+
method: hapiMethod,
|
|
1200
|
+
options: hapiOptions,
|
|
1201
|
+
path: hapiPath,
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
this.#server.route(route)
|
|
1205
|
+
})
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
create404Route() {
|
|
1209
|
+
// If a {proxy+} or $default route exists, don't conflict with it
|
|
1210
|
+
if (this.#server.match('*', '/{p*}')) {
|
|
1211
|
+
return
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const existingRoutes = this.#server
|
|
1215
|
+
.table()
|
|
1216
|
+
// Exclude this (404) route
|
|
1217
|
+
.filter((route) => route.path !== '/{p*}')
|
|
1218
|
+
// Sort by path
|
|
1219
|
+
.sort((a, b) => (a.path <= b.path ? -1 : 1))
|
|
1220
|
+
// Human-friendly result
|
|
1221
|
+
.map((route) => `${route.method} - ${route.path}`)
|
|
1222
|
+
|
|
1223
|
+
const route = {
|
|
1224
|
+
handler(request, h) {
|
|
1225
|
+
const response = h.response({
|
|
1226
|
+
currentRoute: `${request.method} - ${request.path}`,
|
|
1227
|
+
error: 'Serverless-offline: route not found.',
|
|
1228
|
+
existingRoutes,
|
|
1229
|
+
statusCode: 404,
|
|
1230
|
+
})
|
|
1231
|
+
response.statusCode = 404
|
|
1232
|
+
|
|
1233
|
+
return response
|
|
1234
|
+
},
|
|
1235
|
+
method: '*',
|
|
1236
|
+
options: {
|
|
1237
|
+
cors: this.#options.corsConfig,
|
|
1238
|
+
},
|
|
1239
|
+
path: '/{p*}',
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
this.#server.route(route)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
#getArrayStackTrace(stack) {
|
|
1246
|
+
if (!stack) return null
|
|
1247
|
+
|
|
1248
|
+
const splittedStack = stack.split('\n')
|
|
1249
|
+
|
|
1250
|
+
return splittedStack
|
|
1251
|
+
.slice(
|
|
1252
|
+
0,
|
|
1253
|
+
splittedStack.findIndex((item) =>
|
|
1254
|
+
item.match(/server.route.handler.LambdaContext/),
|
|
1255
|
+
),
|
|
1256
|
+
)
|
|
1257
|
+
.map((line) => line.trim())
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
#injectLastRequest() {
|
|
1261
|
+
if (this.#lastRequestOptions) {
|
|
1262
|
+
log.notice('Replaying HTTP last request')
|
|
1263
|
+
this.#server.inject(this.#lastRequestOptions)
|
|
1264
|
+
} else {
|
|
1265
|
+
log.notice('No last HTTP request to replay!')
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
writeRoutesTerminal() {
|
|
1270
|
+
logRoutes(this.#terminalInfo)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// TEMP FIXME quick fix to expose gateway server for testing, look for better solution
|
|
1274
|
+
getServer() {
|
|
1275
|
+
return this.#server
|
|
1276
|
+
}
|
|
1277
|
+
}
|