serverless-offline 8.7.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +91 -95
  2. package/package.json +41 -69
  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/dist/ServerlessOffline.js +0 -507
  131. package/dist/checkEngine.js +0 -21
  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 -10
  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/http/Endpoint.js +0 -171
  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 -1363
  144. package/dist/events/http/OfflineEndpoint.js +0 -40
  145. package/dist/events/http/authJWTSettingsExtractor.js +0 -76
  146. package/dist/events/http/authValidateContext.js +0 -48
  147. package/dist/events/http/createAuthScheme.js +0 -184
  148. package/dist/events/http/createJWTAuthScheme.js +0 -155
  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 -85
  152. package/dist/events/http/lambda-events/LambdaProxyIntegrationEvent.js +0 -244
  153. package/dist/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +0 -221
  154. package/dist/events/http/lambda-events/VelocityContext.js +0 -168
  155. package/dist/events/http/lambda-events/index.js +0 -39
  156. package/dist/events/http/lambda-events/renderVelocityTemplateObject.js +0 -108
  157. package/dist/events/http/payloadSchemaValidator.js +0 -13
  158. package/dist/events/schedule/Schedule.js +0 -182
  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 -112
  163. package/dist/events/websocket/WebSocket.js +0 -78
  164. package/dist/events/websocket/WebSocketClients.js +0 -550
  165. package/dist/events/websocket/WebSocketEventDefinition.js +0 -32
  166. package/dist/events/websocket/WebSocketServer.js +0 -140
  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 -19
  181. package/dist/lambda/HttpServer.js +0 -122
  182. package/dist/lambda/Lambda.js +0 -113
  183. package/dist/lambda/LambdaContext.js +0 -53
  184. package/dist/lambda/LambdaFunction.js +0 -391
  185. package/dist/lambda/LambdaFunctionPool.js +0 -127
  186. package/dist/lambda/handler-runner/HandlerRunner.js +0 -223
  187. package/dist/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +0 -132
  188. package/dist/lambda/handler-runner/child-process-runner/childProcessHelper.js +0 -40
  189. package/dist/lambda/handler-runner/child-process-runner/index.js +0 -15
  190. package/dist/lambda/handler-runner/docker-runner/DockerContainer.js +0 -517
  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 -211
  195. package/dist/lambda/handler-runner/go-runner/index.js +0 -15
  196. package/dist/lambda/handler-runner/in-process-runner/InProcessRunner.js +0 -234
  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 -151
  200. package/dist/lambda/handler-runner/java-runner/index.js +0 -15
  201. package/dist/lambda/handler-runner/python-runner/PythonRunner.js +0 -180
  202. package/dist/lambda/handler-runner/python-runner/index.js +0 -15
  203. package/dist/lambda/handler-runner/ruby-runner/RubyRunner.js +0 -148
  204. package/dist/lambda/handler-runner/ruby-runner/index.js +0 -15
  205. package/dist/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +0 -94
  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 -30
  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 -44
  225. package/dist/utils/index.js +0 -158
  226. package/dist/utils/jsonPath.js +0 -21
  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 -34
  232. package/dist/utils/satisfiesVersionRange.js +0 -20
  233. package/dist/utils/splitHandlerPathAndName.js +0 -41
  234. package/dist/utils/unflatten.js +0 -18
@@ -0,0 +1,462 @@
1
+ import { WebSocket } from 'ws'
2
+ import { isBoom } from '@hapi/boom'
3
+ import { log } from '@serverless/utils/log.js'
4
+ import {
5
+ WebSocketAuthorizerEvent,
6
+ WebSocketConnectEvent,
7
+ WebSocketDisconnectEvent,
8
+ WebSocketEvent,
9
+ } from './lambda-events/index.js'
10
+ import authCanExecuteResource from '../authCanExecuteResource.js'
11
+ import authFunctionNameExtractor from '../authFunctionNameExtractor.js'
12
+ import authValidateContext from '../authValidateContext.js'
13
+ import {
14
+ DEFAULT_WEBSOCKETS_API_ROUTE_SELECTION_EXPRESSION,
15
+ DEFAULT_WEBSOCKETS_ROUTE,
16
+ } from '../../config/index.js'
17
+ import { jsonPath } from '../../utils/index.js'
18
+
19
+ const { parse, stringify } = JSON
20
+
21
+ export default class WebSocketClients {
22
+ #clients = new Map()
23
+
24
+ #hardTimeouts = new WeakMap()
25
+
26
+ #idleTimeouts = new WeakMap()
27
+
28
+ #lambda = null
29
+
30
+ #options = null
31
+
32
+ #serverless = null
33
+
34
+ #webSocketAuthorizers = new Map()
35
+
36
+ #webSocketAuthorizersCache = new Map()
37
+
38
+ #webSocketRoutes = new Map()
39
+
40
+ #websocketsApiRouteSelectionExpression = null
41
+
42
+ constructor(serverless, options, lambda) {
43
+ this.#lambda = lambda
44
+ this.#options = options
45
+ this.#serverless = serverless
46
+ this.#websocketsApiRouteSelectionExpression =
47
+ serverless.service.provider.websocketsApiRouteSelectionExpression ||
48
+ DEFAULT_WEBSOCKETS_API_ROUTE_SELECTION_EXPRESSION
49
+ }
50
+
51
+ #addWebSocketClient(client, connectionId) {
52
+ this.#clients.set(client, connectionId)
53
+ this.#clients.set(connectionId, client)
54
+ this.#onWebSocketUsed(connectionId)
55
+ this.#addHardTimeout(client, connectionId)
56
+ }
57
+
58
+ #removeWebSocketClient(client) {
59
+ const connectionId = this.#clients.get(client)
60
+
61
+ this.#clients.delete(client)
62
+ this.#clients.delete(connectionId)
63
+
64
+ return connectionId
65
+ }
66
+
67
+ #getWebSocketClient(connectionId) {
68
+ return this.#clients.get(connectionId)
69
+ }
70
+
71
+ #addHardTimeout(client, connectionId) {
72
+ const timeoutId = setTimeout(() => {
73
+ log.debug(`timeout:hard:${connectionId}`)
74
+
75
+ client.close(1001, 'Going away')
76
+ }, this.#options.webSocketHardTimeout * 1000)
77
+
78
+ this.#hardTimeouts.set(client, timeoutId)
79
+ }
80
+
81
+ #clearHardTimeout(client) {
82
+ const timeoutId = this.#hardTimeouts.get(client)
83
+ clearTimeout(timeoutId)
84
+ }
85
+
86
+ #onWebSocketUsed(connectionId) {
87
+ const client = this.#getWebSocketClient(connectionId)
88
+ this.#clearIdleTimeout(client)
89
+
90
+ log.debug(`timeout:idle:${connectionId}:reset`)
91
+
92
+ const timeoutId = setTimeout(() => {
93
+ log.debug(`timeout:idle:${connectionId}:trigger`)
94
+ client.close(1001, 'Going away')
95
+ }, this.#options.webSocketIdleTimeout * 1000)
96
+ this.#idleTimeouts.set(client, timeoutId)
97
+ }
98
+
99
+ #clearIdleTimeout(client) {
100
+ const timeoutId = this.#idleTimeouts.get(client)
101
+ clearTimeout(timeoutId)
102
+ }
103
+
104
+ async verifyClient(connectionId, request) {
105
+ const routeName = '$connect'
106
+ const route = this.#webSocketRoutes.get(routeName)
107
+ if (!route) {
108
+ return {
109
+ statusCode: 502,
110
+ verified: false,
111
+ }
112
+ }
113
+
114
+ const connectEvent = new WebSocketConnectEvent(
115
+ connectionId,
116
+ request,
117
+ this.#options,
118
+ ).create()
119
+
120
+ const authFunName = this.#webSocketAuthorizers.get(routeName)
121
+
122
+ if (authFunName) {
123
+ const authorizerFunction = this.#lambda.get(authFunName)
124
+ const authorizeEvent = new WebSocketAuthorizerEvent(
125
+ connectionId,
126
+ request,
127
+ this.#serverless.service.provider,
128
+ this.#options,
129
+ ).create()
130
+
131
+ authorizerFunction.setEvent(authorizeEvent)
132
+
133
+ log.notice()
134
+ log.notice(
135
+ `Running Authorization function for ${routeName} (λ: ${authFunName})`,
136
+ )
137
+
138
+ try {
139
+ const result = await authorizerFunction.runHandler()
140
+ if (result === 'Unauthorized') {
141
+ return {
142
+ statusCode: 401,
143
+ verified: false,
144
+ }
145
+ }
146
+
147
+ const policy = result
148
+
149
+ // Validate that the policy document has the principalId set
150
+ if (!policy.principalId) {
151
+ log.notice(
152
+ `Authorization response did not include a principalId: (λ: ${authFunName})`,
153
+ )
154
+
155
+ return {
156
+ statusCode: 403,
157
+ verified: false,
158
+ }
159
+ }
160
+
161
+ if (
162
+ !authCanExecuteResource(
163
+ policy.policyDocument,
164
+ authorizeEvent.methodArn,
165
+ )
166
+ ) {
167
+ log.notice(
168
+ `Authorization response didn't authorize user to access resource: (λ: ${authFunName})`,
169
+ )
170
+
171
+ return {
172
+ statusCode: 403,
173
+ verified: false,
174
+ }
175
+ }
176
+
177
+ log.notice(
178
+ `Authorization function returned a successful response: (λ: ${authFunName})`,
179
+ )
180
+
181
+ if (policy.context) {
182
+ const validatedContext = authValidateContext(
183
+ policy.context,
184
+ authorizerFunction,
185
+ )
186
+ if (validatedContext instanceof Error) throw validatedContext
187
+ policy.context = validatedContext
188
+ }
189
+
190
+ this.#webSocketAuthorizersCache.set(connectionId, {
191
+ authorizer: {
192
+ integrationLatency: '42',
193
+ principalId: policy.principalId,
194
+ ...policy.context,
195
+ },
196
+ identity: {
197
+ apiKey: policy.usageIdentifierKey,
198
+ sourceIp: authorizeEvent.requestContext.sourceIp,
199
+ userAgent: authorizeEvent.headers['user-agent'] || '',
200
+ },
201
+ })
202
+ } catch (err) {
203
+ log.debug(`Error in route handler '${routeName}' authorizer`, err)
204
+
205
+ let headers = []
206
+ let message
207
+
208
+ if (isBoom(err)) {
209
+ headers = err.output.headers
210
+ message = err.output.payload.message
211
+ }
212
+
213
+ return {
214
+ headers,
215
+ message,
216
+ statusCode: 500,
217
+ verified: false,
218
+ }
219
+ }
220
+ }
221
+
222
+ const authorizerData = this.#webSocketAuthorizersCache.get(connectionId)
223
+ if (authorizerData) {
224
+ connectEvent.requestContext.identity = authorizerData.identity
225
+ connectEvent.requestContext.authorizer = authorizerData.authorizer
226
+ }
227
+
228
+ const lambdaFunction = this.#lambda.get(route.functionKey)
229
+ lambdaFunction.setEvent(connectEvent)
230
+
231
+ try {
232
+ const { statusCode } = await lambdaFunction.runHandler()
233
+ const verified = statusCode >= 200 && statusCode < 300
234
+
235
+ return {
236
+ statusCode,
237
+ verified,
238
+ }
239
+ } catch (err) {
240
+ this.#webSocketAuthorizersCache.delete(connectionId)
241
+
242
+ log.debug(`Error in route handler '${route.functionKey}'`, err)
243
+
244
+ return {
245
+ statusCode: 502,
246
+ verified: false,
247
+ }
248
+ }
249
+ }
250
+
251
+ async #processEvent(websocketClient, connectionId, routeKey, event) {
252
+ let route = this.#webSocketRoutes.get(routeKey)
253
+
254
+ if (!route && routeKey !== '$disconnect') {
255
+ route = this.#webSocketRoutes.get('$default')
256
+ }
257
+
258
+ if (!route) {
259
+ return
260
+ }
261
+
262
+ const sendError = (err) => {
263
+ if (websocketClient.readyState === WebSocket.OPEN) {
264
+ websocketClient.send(
265
+ stringify({
266
+ connectionId,
267
+ message: 'Internal server error',
268
+ requestId: '1234567890',
269
+ }),
270
+ )
271
+ }
272
+
273
+ log.debug(`Error in route handler '${route.functionKey}'`, err)
274
+ }
275
+
276
+ const authorizerData = this.#webSocketAuthorizersCache.get(connectionId)
277
+ let authorizedEvent
278
+
279
+ if (authorizerData) {
280
+ authorizedEvent = event
281
+ authorizedEvent.requestContext.identity = authorizerData.identity
282
+ authorizedEvent.requestContext.authorizer = authorizerData.authorizer
283
+ }
284
+
285
+ const lambdaFunction = this.#lambda.get(route.functionKey)
286
+ lambdaFunction.setEvent(authorizedEvent || event)
287
+
288
+ try {
289
+ const { body } = await lambdaFunction.runHandler()
290
+ if (
291
+ body &&
292
+ routeKey !== '$disconnect' &&
293
+ route.definition.routeResponseSelectionExpression === '$default'
294
+ ) {
295
+ // https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html#apigateway-websocket-api-route-response-selection-expressions
296
+ // TODO: Once API gateway supports RouteResponses, this will need to change to support that functionality
297
+ // For now, send body back to the client
298
+ this.send(connectionId, body)
299
+ }
300
+ } catch (err) {
301
+ log.error(err)
302
+
303
+ sendError(err)
304
+ }
305
+ }
306
+
307
+ #getRoute(value) {
308
+ let json
309
+
310
+ try {
311
+ json = parse(value)
312
+ } catch {
313
+ return DEFAULT_WEBSOCKETS_ROUTE
314
+ }
315
+
316
+ const routeSelectionExpression =
317
+ this.#websocketsApiRouteSelectionExpression.replace('request.body', '')
318
+
319
+ const route = jsonPath(json, routeSelectionExpression)
320
+
321
+ if (typeof route !== 'string') {
322
+ return DEFAULT_WEBSOCKETS_ROUTE
323
+ }
324
+
325
+ return route || DEFAULT_WEBSOCKETS_ROUTE
326
+ }
327
+
328
+ addClient(webSocketClient, connectionId) {
329
+ this.#addWebSocketClient(webSocketClient, connectionId)
330
+
331
+ webSocketClient.on('close', () => {
332
+ log.debug(`disconnect:${connectionId}`)
333
+
334
+ this.#removeWebSocketClient(webSocketClient)
335
+
336
+ const disconnectEvent = new WebSocketDisconnectEvent(
337
+ connectionId,
338
+ ).create()
339
+
340
+ this.#clearHardTimeout(webSocketClient)
341
+ this.#clearIdleTimeout(webSocketClient)
342
+
343
+ const authorizerData = this.#webSocketAuthorizersCache.get(connectionId)
344
+ if (authorizerData) {
345
+ disconnectEvent.requestContext.identity = authorizerData.identity
346
+ disconnectEvent.requestContext.authorizer = authorizerData.authorizer
347
+ }
348
+
349
+ this.#processEvent(
350
+ webSocketClient,
351
+ connectionId,
352
+ '$disconnect',
353
+ disconnectEvent,
354
+ ).finally(() => this.#webSocketAuthorizersCache.delete(connectionId))
355
+ })
356
+
357
+ webSocketClient.on('message', (data, isBinary) => {
358
+ const message = isBinary ? data : String(data)
359
+
360
+ log.debug(`message:${message}`)
361
+
362
+ const route = this.#getRoute(message)
363
+
364
+ log.debug(`route:${route} on connection=${connectionId}`)
365
+
366
+ const event = new WebSocketEvent(connectionId, route, message).create()
367
+ const authorizerData = this.#webSocketAuthorizersCache.get(connectionId)
368
+ if (authorizerData) {
369
+ event.requestContext.identity = authorizerData.identity
370
+ event.requestContext.authorizer = authorizerData.authorizer
371
+ }
372
+ this.#onWebSocketUsed(connectionId)
373
+
374
+ this.#processEvent(webSocketClient, connectionId, route, event)
375
+ })
376
+ }
377
+
378
+ #extractAuthFunctionName(endpoint) {
379
+ if (
380
+ typeof endpoint.authorizer === 'object' &&
381
+ endpoint.authorizer.type &&
382
+ endpoint.authorizer.type.toUpperCase() === 'TOKEN'
383
+ ) {
384
+ log.debug(`Websockets does not support the TOKEN authorization type`)
385
+
386
+ return null
387
+ }
388
+
389
+ const result = authFunctionNameExtractor(endpoint)
390
+
391
+ return result.unsupportedAuth ? null : result.authorizerName
392
+ }
393
+
394
+ #configureAuthorization(endpoint, functionKey) {
395
+ if (!endpoint.authorizer) {
396
+ return
397
+ }
398
+
399
+ if (endpoint.route === '$connect') {
400
+ const authFunctionName = this.#extractAuthFunctionName(endpoint)
401
+
402
+ if (!authFunctionName) {
403
+ return
404
+ }
405
+
406
+ log.notice(
407
+ `Configuring Authorization: ${functionKey} ${authFunctionName}`,
408
+ )
409
+
410
+ const authFunction =
411
+ this.#serverless.service.getFunction(authFunctionName)
412
+
413
+ if (!authFunction) {
414
+ log.error(`Authorization function ${authFunctionName} does not exist`)
415
+
416
+ return
417
+ }
418
+
419
+ this.#webSocketAuthorizers.set(endpoint.route, authFunctionName)
420
+ return
421
+ }
422
+
423
+ log.notice(`Configuring Authorization is supported only on $connect route`)
424
+ }
425
+
426
+ addRoute(functionKey, definition) {
427
+ // set the route name
428
+ this.#webSocketRoutes.set(definition.route, {
429
+ definition,
430
+ functionKey,
431
+ })
432
+
433
+ if (!this.#options.noAuth) {
434
+ this.#configureAuthorization(definition, functionKey)
435
+ }
436
+
437
+ log.notice(`route '${definition.route} (λ: ${functionKey})'`)
438
+ }
439
+
440
+ close(connectionId) {
441
+ const client = this.#getWebSocketClient(connectionId)
442
+
443
+ if (client) {
444
+ client.close()
445
+ return true
446
+ }
447
+
448
+ return false
449
+ }
450
+
451
+ send(connectionId, payload) {
452
+ const client = this.#getWebSocketClient(connectionId)
453
+
454
+ if (client) {
455
+ this.#onWebSocketUsed(connectionId)
456
+ client.send(payload)
457
+ return true
458
+ }
459
+
460
+ return false
461
+ }
462
+ }
@@ -0,0 +1,18 @@
1
+ const { assign } = Object
2
+
3
+ export default class WebSocketEventDefinition {
4
+ constructor(rawWebSocketEventDefinition) {
5
+ let rest
6
+ let route
7
+
8
+ if (typeof rawWebSocketEventDefinition === 'string') {
9
+ route = rawWebSocketEventDefinition
10
+ } else {
11
+ ;({ route, ...rest } = rawWebSocketEventDefinition)
12
+ }
13
+
14
+ this.route = route
15
+
16
+ assign(this, rest)
17
+ }
18
+ }
@@ -0,0 +1,73 @@
1
+ import { log } from '@serverless/utils/log.js'
2
+ import { WebSocketServer as WsWebSocketServer } from 'ws'
3
+ import { createUniqueId } from '../../utils/index.js'
4
+
5
+ export default class WebSocketServer {
6
+ #connectionIds = new Map()
7
+
8
+ #options = null
9
+
10
+ #webSocketClients = null
11
+
12
+ constructor(options, webSocketClients, sharedServer) {
13
+ this.#options = options
14
+ this.#webSocketClients = webSocketClients
15
+
16
+ const server = new WsWebSocketServer({
17
+ server: sharedServer,
18
+ verifyClient: async ({ req }, cb) => {
19
+ const connectionId = createUniqueId()
20
+ const key = req.headers['sec-websocket-key']
21
+
22
+ log.debug(`verifyClient:${key} ${connectionId}`)
23
+
24
+ // use the websocket key to correlate connection IDs
25
+ this.#connectionIds.set(key, connectionId)
26
+
27
+ const { headers, message, statusCode, verified } =
28
+ await this.#webSocketClients.verifyClient(connectionId, req)
29
+
30
+ try {
31
+ if (!verified) {
32
+ cb(false, statusCode, message, headers)
33
+ return
34
+ }
35
+ cb(true)
36
+ } catch (err) {
37
+ log.debug(`Error verifying`, err)
38
+ cb(false)
39
+ }
40
+ },
41
+ })
42
+
43
+ server.on('connection', (webSocketClient, request) => {
44
+ log.notice('received connection')
45
+
46
+ const { headers } = request
47
+ const key = headers['sec-websocket-key']
48
+
49
+ const connectionId = this.#connectionIds.get(key)
50
+
51
+ log.debug(`connect:${connectionId}`)
52
+
53
+ this.#webSocketClients.addClient(webSocketClient, connectionId)
54
+ })
55
+ }
56
+
57
+ async start() {
58
+ const { host, httpsProtocol, websocketPort } = this.#options
59
+
60
+ log.notice(
61
+ `Offline [websocket] listening on ws${
62
+ httpsProtocol ? 's' : ''
63
+ }://${host}:${websocketPort}`,
64
+ )
65
+ }
66
+
67
+ // no-op, we're re-using the http server
68
+ stop() {}
69
+
70
+ addRoute(functionKey, webSocketEvent) {
71
+ this.#webSocketClients.addRoute(functionKey, webSocketEvent)
72
+ }
73
+ }
@@ -0,0 +1,16 @@
1
+ import { log } from '@serverless/utils/log.js'
2
+
3
+ export default function catchAllRoute() {
4
+ return {
5
+ handler(request, h) {
6
+ const { url } = request
7
+
8
+ log.debug(`got GET to ${url}`)
9
+
10
+ return h.response(null).code(426)
11
+ },
12
+
13
+ method: 'GET',
14
+ path: '/{path*}',
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export { default } from './catchAllRoute.js'
@@ -0,0 +1,28 @@
1
+ export default class ConnectionsController {
2
+ #webSocketClients = null
3
+
4
+ constructor(webSocketClients) {
5
+ this.#webSocketClients = webSocketClients
6
+ }
7
+
8
+ send(connectionId, payload) {
9
+ // TODO, is this correct?
10
+ if (!payload) {
11
+ return null
12
+ }
13
+
14
+ const clientExisted = this.#webSocketClients.send(
15
+ connectionId,
16
+ // payload is a Buffer
17
+ payload.toString('utf-8'),
18
+ )
19
+
20
+ return clientExisted
21
+ }
22
+
23
+ remove(connectionId) {
24
+ const clientExisted = this.#webSocketClients.close(connectionId)
25
+
26
+ return clientExisted
27
+ }
28
+ }
@@ -0,0 +1,70 @@
1
+ import { log } from '@serverless/utils/log.js'
2
+ import ConnectionsController from './ConnectionsController.js'
3
+
4
+ export default function connectionsRoutes(webSocketClients) {
5
+ const connectionsController = new ConnectionsController(webSocketClients)
6
+
7
+ return [
8
+ {
9
+ async handler(request, h) {
10
+ const {
11
+ params: { connectionId },
12
+ payload,
13
+ url,
14
+ } = request
15
+
16
+ log.debug(`got POST to ${url}`)
17
+
18
+ const clientExisted = await connectionsController.send(
19
+ connectionId,
20
+ payload,
21
+ )
22
+
23
+ if (!clientExisted) {
24
+ return h.response(null).code(410)
25
+ }
26
+
27
+ log.debug(`sent data to connection:${connectionId}`)
28
+
29
+ return null
30
+ },
31
+
32
+ method: 'POST',
33
+ options: {
34
+ payload: {
35
+ parse: false,
36
+ },
37
+ },
38
+ path: '/@connections/{connectionId}',
39
+ },
40
+
41
+ {
42
+ handler(request, h) {
43
+ const {
44
+ params: { connectionId },
45
+ url,
46
+ } = request
47
+
48
+ log.debug(`got DELETE to ${url}`)
49
+
50
+ const clientExisted = connectionsController.remove(connectionId)
51
+
52
+ if (!clientExisted) {
53
+ return h.response(null).code(410)
54
+ }
55
+
56
+ log.debug(`closed connection:${connectionId}`)
57
+
58
+ return h.response(null).code(204)
59
+ },
60
+
61
+ method: 'DELETE',
62
+ options: {
63
+ payload: {
64
+ parse: false,
65
+ },
66
+ },
67
+ path: '/@connections/{connectionId}',
68
+ },
69
+ ]
70
+ }
@@ -0,0 +1 @@
1
+ export { default } from './connectionsRoutes.js'
@@ -0,0 +1,2 @@
1
+ export { default as catchAllRoute } from './_catchAll/index.js'
2
+ export { default as connectionsRoutes } from './connections/index.js'
@@ -0,0 +1 @@
1
+ export { default } from './WebSocket.js'