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