serverless-offline 8.8.1 → 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.
Files changed (235) hide show
  1. package/README.md +3 -3
  2. package/package.json +33 -57
  3. package/src/ServerlessOffline.js +412 -0
  4. package/src/config/commandOptions.js +155 -0
  5. package/src/config/constants.js +22 -0
  6. package/{dist → src}/config/defaultOptions.js +8 -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 +1277 -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 +308 -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 +72 -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 +417 -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 +166 -0
  78. package/src/lambda/handler-runner/go-runner/index.js +1 -0
  79. package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +125 -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/resolveJoins.js +29 -0
  128. package/src/utils/splitHandlerPathAndName.js +31 -0
  129. package/src/utils/unflatten.js +11 -0
  130. package/CHANGELOG.md +0 -78
  131. package/dist/ServerlessOffline.js +0 -508
  132. package/dist/config/commandOptions.js +0 -149
  133. package/dist/config/constants.js +0 -30
  134. package/dist/config/index.js +0 -55
  135. package/dist/config/supportedRuntimes.js +0 -40
  136. package/dist/debugLog.js +0 -12
  137. package/dist/events/authCanExecuteResource.js +0 -35
  138. package/dist/events/authFunctionNameExtractor.js +0 -87
  139. package/dist/events/authMatchPolicyResource.js +0 -62
  140. package/dist/events/authValidateContext.js +0 -53
  141. package/dist/events/http/Endpoint.js +0 -173
  142. package/dist/events/http/Http.js +0 -77
  143. package/dist/events/http/HttpEventDefinition.js +0 -36
  144. package/dist/events/http/HttpServer.js +0 -1370
  145. package/dist/events/http/OfflineEndpoint.js +0 -38
  146. package/dist/events/http/authJWTSettingsExtractor.js +0 -76
  147. package/dist/events/http/createAuthScheme.js +0 -184
  148. package/dist/events/http/createJWTAuthScheme.js +0 -159
  149. package/dist/events/http/index.js +0 -15
  150. package/dist/events/http/javaHelpers.js +0 -99
  151. package/dist/events/http/lambda-events/LambdaIntegrationEvent.js +0 -87
  152. package/dist/events/http/lambda-events/LambdaProxyIntegrationEvent.js +0 -246
  153. package/dist/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +0 -225
  154. package/dist/events/http/lambda-events/VelocityContext.js +0 -170
  155. package/dist/events/http/lambda-events/index.js +0 -39
  156. package/dist/events/http/lambda-events/renderVelocityTemplateObject.js +0 -111
  157. package/dist/events/http/payloadSchemaValidator.js +0 -13
  158. package/dist/events/schedule/Schedule.js +0 -183
  159. package/dist/events/schedule/ScheduleEvent.js +0 -27
  160. package/dist/events/schedule/ScheduleEventDefinition.js +0 -36
  161. package/dist/events/schedule/index.js +0 -15
  162. package/dist/events/websocket/HttpServer.js +0 -114
  163. package/dist/events/websocket/WebSocket.js +0 -78
  164. package/dist/events/websocket/WebSocketClients.js +0 -577
  165. package/dist/events/websocket/WebSocketEventDefinition.js +0 -32
  166. package/dist/events/websocket/WebSocketServer.js +0 -139
  167. package/dist/events/websocket/http-routes/_catchAll/catchAllRoute.js +0 -33
  168. package/dist/events/websocket/http-routes/_catchAll/index.js +0 -15
  169. package/dist/events/websocket/http-routes/connections/ConnectionsController.js +0 -45
  170. package/dist/events/websocket/http-routes/connections/connectionsRoutes.js +0 -95
  171. package/dist/events/websocket/http-routes/connections/index.js +0 -15
  172. package/dist/events/websocket/http-routes/index.js +0 -23
  173. package/dist/events/websocket/index.js +0 -15
  174. package/dist/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +0 -99
  175. package/dist/events/websocket/lambda-events/WebSocketConnectEvent.js +0 -101
  176. package/dist/events/websocket/lambda-events/WebSocketDisconnectEvent.js +0 -47
  177. package/dist/events/websocket/lambda-events/WebSocketEvent.js +0 -54
  178. package/dist/events/websocket/lambda-events/WebSocketRequestContext.js +0 -98
  179. package/dist/events/websocket/lambda-events/index.js +0 -39
  180. package/dist/index.js +0 -15
  181. package/dist/lambda/HttpServer.js +0 -124
  182. package/dist/lambda/Lambda.js +0 -117
  183. package/dist/lambda/LambdaContext.js +0 -53
  184. package/dist/lambda/LambdaFunction.js +0 -390
  185. package/dist/lambda/LambdaFunctionPool.js +0 -127
  186. package/dist/lambda/handler-runner/HandlerRunner.js +0 -195
  187. package/dist/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +0 -124
  188. package/dist/lambda/handler-runner/child-process-runner/childProcessHelper.js +0 -49
  189. package/dist/lambda/handler-runner/child-process-runner/index.js +0 -15
  190. package/dist/lambda/handler-runner/docker-runner/DockerContainer.js +0 -515
  191. package/dist/lambda/handler-runner/docker-runner/DockerImage.js +0 -67
  192. package/dist/lambda/handler-runner/docker-runner/DockerRunner.js +0 -74
  193. package/dist/lambda/handler-runner/docker-runner/index.js +0 -15
  194. package/dist/lambda/handler-runner/go-runner/GoRunner.js +0 -230
  195. package/dist/lambda/handler-runner/go-runner/index.js +0 -15
  196. package/dist/lambda/handler-runner/in-process-runner/InProcessRunner.js +0 -228
  197. package/dist/lambda/handler-runner/in-process-runner/index.js +0 -15
  198. package/dist/lambda/handler-runner/index.js +0 -15
  199. package/dist/lambda/handler-runner/java-runner/JavaRunner.js +0 -153
  200. package/dist/lambda/handler-runner/java-runner/index.js +0 -15
  201. package/dist/lambda/handler-runner/python-runner/PythonRunner.js +0 -185
  202. package/dist/lambda/handler-runner/python-runner/index.js +0 -15
  203. package/dist/lambda/handler-runner/ruby-runner/RubyRunner.js +0 -147
  204. package/dist/lambda/handler-runner/ruby-runner/index.js +0 -15
  205. package/dist/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +0 -92
  206. package/dist/lambda/handler-runner/worker-thread-runner/index.js +0 -15
  207. package/dist/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +0 -31
  208. package/dist/lambda/index.js +0 -15
  209. package/dist/lambda/routes/index.js +0 -23
  210. package/dist/lambda/routes/invocations/InvocationsController.js +0 -142
  211. package/dist/lambda/routes/invocations/index.js +0 -15
  212. package/dist/lambda/routes/invocations/invocationsRoute.js +0 -90
  213. package/dist/lambda/routes/invoke-async/InvokeAsyncController.js +0 -38
  214. package/dist/lambda/routes/invoke-async/index.js +0 -15
  215. package/dist/lambda/routes/invoke-async/invokeAsyncRoute.js +0 -43
  216. package/dist/main.js +0 -11
  217. package/dist/serverlessLog.js +0 -91
  218. package/dist/utils/checkDockerDaemon.js +0 -27
  219. package/dist/utils/checkGoVersion.js +0 -27
  220. package/dist/utils/createApiKey.js +0 -12
  221. package/dist/utils/createUniqueId.js +0 -14
  222. package/dist/utils/detectExecutable.js +0 -21
  223. package/dist/utils/generateHapiPath.js +0 -28
  224. package/dist/utils/getHttpApiCorsConfig.js +0 -40
  225. package/dist/utils/index.js +0 -165
  226. package/dist/utils/jsonPath.js +0 -21
  227. package/dist/utils/lowerCaseKeys.js +0 -14
  228. package/dist/utils/parseHeaders.js +0 -23
  229. package/dist/utils/parseMultiValueHeaders.js +0 -36
  230. package/dist/utils/parseMultiValueQueryStringParameters.js +0 -40
  231. package/dist/utils/parseQueryStringParameters.js +0 -26
  232. package/dist/utils/resolveJoins.js +0 -36
  233. package/dist/utils/satisfiesVersionRange.js +0 -20
  234. package/dist/utils/splitHandlerPathAndName.js +0 -37
  235. 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
+ }