serverless-offline 8.8.1 → 9.1.1

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