serverless-offline 11.5.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +11 -11
- package/src/ServerlessOffline.js +41 -3
- package/src/config/commandOptions.js +4 -0
- package/src/config/defaultOptions.js +1 -0
- package/src/events/alb/Alb.js +56 -0
- package/src/events/alb/AlbEventDefinition.js +22 -0
- package/src/events/alb/HttpServer.js +404 -0
- package/src/events/alb/index.js +1 -0
- package/src/events/alb/lambda-events/LambdaAlbRequestEvent.js +52 -0
- package/src/events/alb/lambda-events/index.js +1 -0
- package/src/events/http/createAuthScheme.js +4 -1
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +5 -6
- package/src/utils/generateHapiPath.js +29 -0
- package/src/utils/index.js +1 -1
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ By default you can send your requests to `http://localhost:3000/`. Please note t
|
|
|
262
262
|
|
|
263
263
|
### node.js
|
|
264
264
|
|
|
265
|
-
Lambda handlers for the `node.js` runtime can run in different execution modes
|
|
265
|
+
Lambda handlers with `serverless-offline` for the `node.js` runtime can run in different execution modes and have some differences with a variety of pros and cons. they are currently mutually exclusive and it's not possible to use a combination, e.g. use `in-process` for one Lambda, and `worker-threads` for another. It is planned to combine the flags into one single flag in the future and also add support for combining run modes.
|
|
266
266
|
|
|
267
267
|
#### worker-threads (default)
|
|
268
268
|
|
|
@@ -273,6 +273,10 @@ Lambda handlers for the `node.js` runtime can run in different execution modes w
|
|
|
273
273
|
- global state is not being shared across handlers
|
|
274
274
|
- easy debugging
|
|
275
275
|
|
|
276
|
+
NOTE:
|
|
277
|
+
|
|
278
|
+
- native modules need to be a Node-API addon or be declared as context-aware using NODE_MODULE_INIT(): https://nodejs.org/docs/latest/api/addons.html#worker-support
|
|
279
|
+
|
|
276
280
|
#### in-process
|
|
277
281
|
|
|
278
282
|
- handlers run in the same context (instance) as `serverless` and `serverless-offline`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dedicatedTo": "Blue, a great migrating bird.",
|
|
3
3
|
"name": "serverless-offline",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "12.0.0",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -82,46 +82,46 @@
|
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@aws-sdk/client-lambda": "^3.
|
|
85
|
+
"@aws-sdk/client-lambda": "^3.222.0",
|
|
86
86
|
"@hapi/boom": "^10.0.0",
|
|
87
87
|
"@hapi/h2o2": "^10.0.0",
|
|
88
|
-
"@hapi/hapi": "^21.
|
|
88
|
+
"@hapi/hapi": "^21.1.0",
|
|
89
89
|
"@serverless/utils": "^6.8.2",
|
|
90
90
|
"boxen": "^7.0.0",
|
|
91
91
|
"chalk": "^5.1.2",
|
|
92
92
|
"execa": "^6.1.0",
|
|
93
|
-
"fs-extra": "^
|
|
93
|
+
"fs-extra": "^11.1.0",
|
|
94
94
|
"is-wsl": "^2.2.0",
|
|
95
95
|
"java-invoke-local": "0.0.6",
|
|
96
|
-
"jose": "^4.11.
|
|
96
|
+
"jose": "^4.11.1",
|
|
97
97
|
"js-string-escape": "^1.0.1",
|
|
98
98
|
"jsonpath-plus": "^7.2.0",
|
|
99
99
|
"jsonschema": "^1.4.1",
|
|
100
100
|
"jszip": "^3.10.1",
|
|
101
|
-
"luxon": "^3.1.
|
|
101
|
+
"luxon": "^3.1.1",
|
|
102
102
|
"node-fetch": "^3.3.0",
|
|
103
103
|
"node-schedule": "^2.1.0",
|
|
104
104
|
"object.hasown": "^1.1.2",
|
|
105
105
|
"p-memoize": "^7.1.1",
|
|
106
|
-
"p-retry": "^5.1.
|
|
106
|
+
"p-retry": "^5.1.2",
|
|
107
107
|
"velocityjs": "^2.0.6",
|
|
108
108
|
"ws": "^8.11.0"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@istanbuljs/esm-loader-hook": "^0.2.0",
|
|
112
112
|
"archiver": "^5.3.1",
|
|
113
|
-
"eslint": "^8.
|
|
113
|
+
"eslint": "^8.28.0",
|
|
114
114
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
115
115
|
"eslint-config-prettier": "^8.5.0",
|
|
116
116
|
"eslint-plugin-import": "^2.25.4",
|
|
117
117
|
"eslint-plugin-prettier": "^4.2.1",
|
|
118
118
|
"git-list-updated": "^1.2.1",
|
|
119
119
|
"husky": "^8.0.2",
|
|
120
|
-
"lint-staged": "^13.0.
|
|
120
|
+
"lint-staged": "^13.0.4",
|
|
121
121
|
"mocha": "^10.1.0",
|
|
122
122
|
"nyc": "^15.1.0",
|
|
123
|
-
"prettier": "^2.
|
|
124
|
-
"serverless": "^3.
|
|
123
|
+
"prettier": "^2.8.0",
|
|
124
|
+
"serverless": "^3.25.1",
|
|
125
125
|
"standard-version": "^9.5.0"
|
|
126
126
|
},
|
|
127
127
|
"peerDependencies": {
|
package/src/ServerlessOffline.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import { gray } from './config/colors.js'
|
|
10
10
|
|
|
11
11
|
export default class ServerlessOffline {
|
|
12
|
+
#alb = null
|
|
13
|
+
|
|
12
14
|
#cliOptions = null
|
|
13
15
|
|
|
14
16
|
#http = null
|
|
@@ -62,6 +64,7 @@ export default class ServerlessOffline {
|
|
|
62
64
|
this.#mergeOptions()
|
|
63
65
|
|
|
64
66
|
const {
|
|
67
|
+
albEvents,
|
|
65
68
|
httpEvents,
|
|
66
69
|
httpApiEvents,
|
|
67
70
|
lambdas,
|
|
@@ -75,6 +78,10 @@ export default class ServerlessOffline {
|
|
|
75
78
|
|
|
76
79
|
const eventModules = []
|
|
77
80
|
|
|
81
|
+
if (albEvents.length > 0) {
|
|
82
|
+
eventModules.push(this.#createAlb(albEvents))
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
if (httpApiEvents.length > 0 || httpEvents.length > 0) {
|
|
79
86
|
eventModules.push(this.#createHttp([...httpApiEvents, ...httpEvents]))
|
|
80
87
|
}
|
|
@@ -104,6 +111,10 @@ export default class ServerlessOffline {
|
|
|
104
111
|
eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
if (this.#alb) {
|
|
115
|
+
eventModules.push(this.#alb.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
if (this.#http) {
|
|
108
119
|
eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
109
120
|
}
|
|
@@ -217,6 +228,20 @@ export default class ServerlessOffline {
|
|
|
217
228
|
await this.#webSocket.start()
|
|
218
229
|
}
|
|
219
230
|
|
|
231
|
+
async #createAlb(events, skipStart) {
|
|
232
|
+
const { default: Alb } = await import('./events/alb/index.js')
|
|
233
|
+
|
|
234
|
+
this.#alb = new Alb(this.#serverless, this.#options, this.#lambda)
|
|
235
|
+
|
|
236
|
+
await this.#alb.createServer()
|
|
237
|
+
|
|
238
|
+
this.#alb.create(events)
|
|
239
|
+
|
|
240
|
+
if (!skipStart) {
|
|
241
|
+
await this.#alb.start()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
220
245
|
#mergeOptions() {
|
|
221
246
|
const {
|
|
222
247
|
service: { custom = {}, provider },
|
|
@@ -263,6 +288,7 @@ export default class ServerlessOffline {
|
|
|
263
288
|
#getEvents() {
|
|
264
289
|
const { service } = this.#serverless
|
|
265
290
|
|
|
291
|
+
const albEvents = []
|
|
266
292
|
const httpEvents = []
|
|
267
293
|
const httpApiEvents = []
|
|
268
294
|
const lambdas = []
|
|
@@ -274,12 +300,23 @@ export default class ServerlessOffline {
|
|
|
274
300
|
functionKeys.forEach((functionKey) => {
|
|
275
301
|
const functionDefinition = service.getFunction(functionKey)
|
|
276
302
|
|
|
277
|
-
lambdas.push({
|
|
303
|
+
lambdas.push({
|
|
304
|
+
functionDefinition,
|
|
305
|
+
functionKey,
|
|
306
|
+
})
|
|
278
307
|
|
|
279
|
-
const events = service.getAllEventsInFunction(functionKey)
|
|
308
|
+
const events = service.getAllEventsInFunction(functionKey) ?? []
|
|
280
309
|
|
|
281
310
|
events.forEach((event) => {
|
|
282
|
-
const { http, httpApi, schedule, websocket } = event
|
|
311
|
+
const { alb, http, httpApi, schedule, websocket } = event
|
|
312
|
+
|
|
313
|
+
if (alb) {
|
|
314
|
+
albEvents.push({
|
|
315
|
+
alb,
|
|
316
|
+
functionKey,
|
|
317
|
+
handler: functionDefinition.handler,
|
|
318
|
+
})
|
|
319
|
+
}
|
|
283
320
|
|
|
284
321
|
if (http && functionDefinition.handler) {
|
|
285
322
|
const httpEvent = {
|
|
@@ -369,6 +406,7 @@ export default class ServerlessOffline {
|
|
|
369
406
|
})
|
|
370
407
|
|
|
371
408
|
return {
|
|
409
|
+
albEvents,
|
|
372
410
|
httpApiEvents,
|
|
373
411
|
httpEvents,
|
|
374
412
|
lambdas,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { log } from '@serverless/utils/log.js'
|
|
2
|
+
import AlbEventDefinition from './AlbEventDefinition.js'
|
|
3
|
+
import HttpServer from './HttpServer.js'
|
|
4
|
+
|
|
5
|
+
export default class Alb {
|
|
6
|
+
#httpServer = null
|
|
7
|
+
|
|
8
|
+
#lambda = null
|
|
9
|
+
|
|
10
|
+
#options = null
|
|
11
|
+
|
|
12
|
+
#serverless = null
|
|
13
|
+
|
|
14
|
+
constructor(serverless, options, lambda) {
|
|
15
|
+
this.#lambda = lambda
|
|
16
|
+
this.#options = options
|
|
17
|
+
this.#serverless = serverless
|
|
18
|
+
|
|
19
|
+
log.warning(`
|
|
20
|
+
Application Load Balancer (ALB) support in serverless-offline is experimental.
|
|
21
|
+
Please file an issue for any bugs, missing features or other feedback: https://github.com/dherault/serverless-offline/issues
|
|
22
|
+
`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
start() {
|
|
26
|
+
return this.#httpServer.start()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
stop(timeout) {
|
|
30
|
+
return this.#httpServer.stop(timeout)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async createServer() {
|
|
34
|
+
this.#httpServer = new HttpServer(
|
|
35
|
+
this.#serverless,
|
|
36
|
+
this.#options,
|
|
37
|
+
this.#lambda,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
await this.#httpServer.createServer()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#createEvent(functionKey, rawAlbEventDefinition) {
|
|
44
|
+
const albEvent = new AlbEventDefinition(rawAlbEventDefinition)
|
|
45
|
+
|
|
46
|
+
this.#httpServer.createRoutes(functionKey, albEvent)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
create(events) {
|
|
50
|
+
events.forEach(({ functionKey, alb }) => {
|
|
51
|
+
this.#createEvent(functionKey, alb)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
this.#httpServer.writeRoutesTerminal()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { assign } = Object
|
|
2
|
+
|
|
3
|
+
export default class AlbEventDefinition {
|
|
4
|
+
constructor(rawAlbEventDefinition) {
|
|
5
|
+
let listenerArn
|
|
6
|
+
let priority
|
|
7
|
+
let conditions
|
|
8
|
+
let rest
|
|
9
|
+
|
|
10
|
+
if (typeof rawAlbEventDefinition === 'string') {
|
|
11
|
+
;[listenerArn, priority, conditions] = rawAlbEventDefinition.split(' ')
|
|
12
|
+
} else {
|
|
13
|
+
;({ listenerArn, priority, conditions, ...rest } = rawAlbEventDefinition)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.listenerArn = listenerArn
|
|
17
|
+
this.priority = priority
|
|
18
|
+
this.conditions = conditions
|
|
19
|
+
|
|
20
|
+
assign(this, rest)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { exit } from 'node:process'
|
|
3
|
+
import { Server } from '@hapi/hapi'
|
|
4
|
+
import { log } from '@serverless/utils/log.js'
|
|
5
|
+
import {
|
|
6
|
+
detectEncoding,
|
|
7
|
+
generateAlbHapiPath,
|
|
8
|
+
getHttpApiCorsConfig,
|
|
9
|
+
} from '../../utils/index.js'
|
|
10
|
+
import LambdaAlbRequestEvent from './lambda-events/LambdaAlbRequestEvent.js'
|
|
11
|
+
import logRoutes from '../../utils/logRoutes.js'
|
|
12
|
+
|
|
13
|
+
const { stringify } = JSON
|
|
14
|
+
const { entries } = Object
|
|
15
|
+
|
|
16
|
+
export default class HttpServer {
|
|
17
|
+
#lambda = null
|
|
18
|
+
|
|
19
|
+
#options = null
|
|
20
|
+
|
|
21
|
+
#serverless = null
|
|
22
|
+
|
|
23
|
+
#server = null
|
|
24
|
+
|
|
25
|
+
#lastRequestOptions = null
|
|
26
|
+
|
|
27
|
+
#terminalInfo = []
|
|
28
|
+
|
|
29
|
+
constructor(serverless, options, lambda) {
|
|
30
|
+
this.#serverless = serverless
|
|
31
|
+
this.#options = options
|
|
32
|
+
this.#lambda = lambda
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createServer() {
|
|
36
|
+
const { host, albPort } = this.#options
|
|
37
|
+
|
|
38
|
+
const serverOptions = {
|
|
39
|
+
host,
|
|
40
|
+
port: albPort,
|
|
41
|
+
router: {
|
|
42
|
+
// allows for paths with trailing slashes to be the same as without
|
|
43
|
+
// e.g. : /my-path is the same as /my-path/
|
|
44
|
+
stripTrailingSlash: true,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.#server = new Server(serverOptions)
|
|
49
|
+
|
|
50
|
+
this.#server.ext('onPreResponse', (request, h) => {
|
|
51
|
+
if (request.headers.origin) {
|
|
52
|
+
const response = request.response.isBoom
|
|
53
|
+
? request.response.output
|
|
54
|
+
: request.response
|
|
55
|
+
|
|
56
|
+
const explicitlySetHeaders = {
|
|
57
|
+
...response.headers,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
this.#serverless.service.provider.httpApi &&
|
|
62
|
+
this.#serverless.service.provider.httpApi.cors
|
|
63
|
+
) {
|
|
64
|
+
const httpApiCors = getHttpApiCorsConfig(
|
|
65
|
+
this.#serverless.service.provider.httpApi.cors,
|
|
66
|
+
this,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if (request.method === 'options') {
|
|
70
|
+
response.statusCode = 204
|
|
71
|
+
const allowAllOrigins =
|
|
72
|
+
httpApiCors.allowedOrigins.length === 1 &&
|
|
73
|
+
httpApiCors.allowedOrigins[0] === '*'
|
|
74
|
+
if (
|
|
75
|
+
!allowAllOrigins &&
|
|
76
|
+
!httpApiCors.allowedOrigins.includes(request.headers.origin)
|
|
77
|
+
) {
|
|
78
|
+
return h.continue
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
response.headers['access-control-allow-origin'] =
|
|
83
|
+
request.headers.origin
|
|
84
|
+
if (httpApiCors.allowCredentials) {
|
|
85
|
+
response.headers['access-control-allow-credentials'] = 'true'
|
|
86
|
+
}
|
|
87
|
+
if (httpApiCors.maxAge) {
|
|
88
|
+
response.headers['access-control-max-age'] = httpApiCors.maxAge
|
|
89
|
+
}
|
|
90
|
+
if (httpApiCors.exposedResponseHeaders) {
|
|
91
|
+
response.headers['access-control-expose-headers'] =
|
|
92
|
+
httpApiCors.exposedResponseHeaders.join(',')
|
|
93
|
+
}
|
|
94
|
+
if (httpApiCors.allowedMethods) {
|
|
95
|
+
response.headers['access-control-allow-methods'] =
|
|
96
|
+
httpApiCors.allowedMethods.join(',')
|
|
97
|
+
}
|
|
98
|
+
if (httpApiCors.allowedHeaders) {
|
|
99
|
+
response.headers['access-control-allow-headers'] =
|
|
100
|
+
httpApiCors.allowedHeaders.join(',')
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
response.headers['access-control-allow-origin'] =
|
|
104
|
+
request.headers.origin
|
|
105
|
+
response.headers['access-control-allow-credentials'] = 'true'
|
|
106
|
+
|
|
107
|
+
if (request.method === 'options') {
|
|
108
|
+
response.statusCode = 200
|
|
109
|
+
|
|
110
|
+
if (request.headers['access-control-expose-headers']) {
|
|
111
|
+
response.headers['access-control-expose-headers'] =
|
|
112
|
+
request.headers['access-control-expose-headers']
|
|
113
|
+
} else {
|
|
114
|
+
response.headers['access-control-expose-headers'] =
|
|
115
|
+
'content-type, content-length, etag'
|
|
116
|
+
}
|
|
117
|
+
response.headers['access-control-max-age'] = 60 * 10
|
|
118
|
+
|
|
119
|
+
if (request.headers['access-control-request-headers']) {
|
|
120
|
+
response.headers['access-control-allow-headers'] =
|
|
121
|
+
request.headers['access-control-request-headers']
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (request.headers['access-control-request-method']) {
|
|
125
|
+
response.headers['access-control-allow-methods'] =
|
|
126
|
+
request.headers['access-control-request-method']
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Override default headers with headers that have been explicitly set
|
|
131
|
+
entries(explicitlySetHeaders).forEach(([key, value]) => {
|
|
132
|
+
if (value) {
|
|
133
|
+
response.headers[key] = value
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return h.continue
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async start() {
|
|
143
|
+
const { host, albPort, httpsProtocol } = this.#options
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await this.#server.start()
|
|
147
|
+
} catch (err) {
|
|
148
|
+
log.error(
|
|
149
|
+
`Unexpected error while starting serverless-offline alb server on port ${albPort}:`,
|
|
150
|
+
err,
|
|
151
|
+
)
|
|
152
|
+
exit(1)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// TODO move the following block
|
|
156
|
+
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${albPort}`
|
|
157
|
+
|
|
158
|
+
log.notice(`ALB Server ready: ${server} 🚀`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stop(timeout) {
|
|
162
|
+
return this.#server.stop({
|
|
163
|
+
timeout,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get server() {
|
|
168
|
+
return this.#server.listener
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#createHapiHandler(params) {
|
|
172
|
+
const { functionKey, method, stage } = params
|
|
173
|
+
|
|
174
|
+
return async (request, h) => {
|
|
175
|
+
this.#lastRequestOptions = {
|
|
176
|
+
headers: request.headers,
|
|
177
|
+
method: request.method,
|
|
178
|
+
payload: request.payload,
|
|
179
|
+
url: request.url.href,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const requestPath = this.#options.noPrependStageInUrl
|
|
183
|
+
? request.path
|
|
184
|
+
: request.path.substr(`/${stage}`.length)
|
|
185
|
+
|
|
186
|
+
// Payload processing
|
|
187
|
+
const encoding = detectEncoding(request)
|
|
188
|
+
|
|
189
|
+
request.payload = request.payload && request.payload.toString(encoding)
|
|
190
|
+
request.rawPayload = request.payload
|
|
191
|
+
|
|
192
|
+
// Incoming request message
|
|
193
|
+
log.notice()
|
|
194
|
+
|
|
195
|
+
log.notice()
|
|
196
|
+
log.notice(`${method} ${request.path} (λ: ${functionKey})`)
|
|
197
|
+
|
|
198
|
+
const response = h.response()
|
|
199
|
+
|
|
200
|
+
let event = {}
|
|
201
|
+
try {
|
|
202
|
+
event = new LambdaAlbRequestEvent(request, stage, requestPath).create()
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return this.#reply502(response, ``, err)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
log.debug('event:', event)
|
|
208
|
+
|
|
209
|
+
const lambdaFunction = this.#lambda.get(functionKey)
|
|
210
|
+
|
|
211
|
+
lambdaFunction.setEvent(event)
|
|
212
|
+
|
|
213
|
+
let result
|
|
214
|
+
let err
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
result = await lambdaFunction.runHandler()
|
|
218
|
+
} catch (_err) {
|
|
219
|
+
err = _err
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
log.debug('_____ HANDLER RESOLVED _____')
|
|
223
|
+
|
|
224
|
+
// Failure handling
|
|
225
|
+
let errorStatusCode = '502'
|
|
226
|
+
|
|
227
|
+
if (err) {
|
|
228
|
+
const errorMessage = (err.message || err).toString()
|
|
229
|
+
|
|
230
|
+
const found = errorMessage.match(/\[(\d{3})]/)
|
|
231
|
+
|
|
232
|
+
if (found && found.length > 1) {
|
|
233
|
+
;[, errorStatusCode] = found
|
|
234
|
+
} else {
|
|
235
|
+
errorStatusCode = '502'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Mocks Lambda errors
|
|
239
|
+
result = {
|
|
240
|
+
errorMessage,
|
|
241
|
+
errorType: err.constructor.name,
|
|
242
|
+
stackTrace: this.#getArrayStackTrace(err.stack),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
log.error(errorMessage)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let statusCode = 200
|
|
249
|
+
|
|
250
|
+
if (err) {
|
|
251
|
+
statusCode = errorStatusCode
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (result && !result.errorType) {
|
|
255
|
+
statusCode = result.statusCode || 200
|
|
256
|
+
} else {
|
|
257
|
+
statusCode = 502
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
response.statusCode = statusCode
|
|
261
|
+
|
|
262
|
+
const headers = {}
|
|
263
|
+
|
|
264
|
+
if (result && result.headers) {
|
|
265
|
+
entries(result.headers).forEach(([headerKey, headerValue]) => {
|
|
266
|
+
headers[headerKey] = (headers[headerKey] || []).concat(headerValue)
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (result && result.multiValueHeaders) {
|
|
271
|
+
entries(result.multiValueHeaders).forEach(
|
|
272
|
+
([headerKey, headerValue]) => {
|
|
273
|
+
headers[headerKey] = (headers[headerKey] || []).concat(headerValue)
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
log.debug('headers:', headers)
|
|
279
|
+
|
|
280
|
+
response.header('Content-Type', 'application/json', {
|
|
281
|
+
duplicate: false,
|
|
282
|
+
override: false,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
if (typeof result === 'string') {
|
|
286
|
+
response.source = stringify(result)
|
|
287
|
+
} else if (result && result.body !== undefined) {
|
|
288
|
+
if (result.isBase64Encoded) {
|
|
289
|
+
response.encoding = 'binary'
|
|
290
|
+
response.source = Buffer.from(result.body, 'base64')
|
|
291
|
+
response.variety = 'buffer'
|
|
292
|
+
} else {
|
|
293
|
+
if (result && result.body && typeof result.body !== 'string') {
|
|
294
|
+
// FIXME TODO we should probably just write to console instead of returning a payload
|
|
295
|
+
return this.#reply502(
|
|
296
|
+
response,
|
|
297
|
+
'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',
|
|
298
|
+
{},
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
response.source = result.body
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return response
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
createRoutes(functionKey, albEvent) {
|
|
310
|
+
const method = albEvent.conditions.method[0].toUpperCase()
|
|
311
|
+
const path = albEvent.conditions.path[0]
|
|
312
|
+
const hapiPath = generateAlbHapiPath(path, this.#options, this.#serverless)
|
|
313
|
+
|
|
314
|
+
const stage = this.#options.stage || this.#serverless.service.provider.stage
|
|
315
|
+
const { host, albPort, httpsProtocol } = this.#options
|
|
316
|
+
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${albPort}`
|
|
317
|
+
|
|
318
|
+
this.#terminalInfo.push({
|
|
319
|
+
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
|
|
320
|
+
method,
|
|
321
|
+
path: hapiPath,
|
|
322
|
+
server,
|
|
323
|
+
stage: this.#options.noPrependStageInUrl ? null : stage,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const hapiMethod = method === 'ANY' ? '*' : method
|
|
327
|
+
const hapiOptions = {}
|
|
328
|
+
|
|
329
|
+
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
330
|
+
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
331
|
+
if (hapiMethod === 'HEAD') {
|
|
332
|
+
log.notice(
|
|
333
|
+
'HEAD method event detected. Skipping HAPI server route mapping',
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
|
|
340
|
+
// maxBytes: Increase request size from 1MB default limit to 10MB.
|
|
341
|
+
// Cf AWS API GW payload limits.
|
|
342
|
+
hapiOptions.payload = {
|
|
343
|
+
maxBytes: 1024 * 1024 * 10,
|
|
344
|
+
parse: false,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const hapiHandler = this.#createHapiHandler({
|
|
349
|
+
functionKey,
|
|
350
|
+
method,
|
|
351
|
+
stage,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
this.#server.route({
|
|
355
|
+
handler: hapiHandler,
|
|
356
|
+
method: hapiMethod,
|
|
357
|
+
options: hapiOptions,
|
|
358
|
+
path: hapiPath,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#replyError(statusCode, response, message, error) {
|
|
363
|
+
log.notice(message)
|
|
364
|
+
|
|
365
|
+
log.error(error)
|
|
366
|
+
|
|
367
|
+
response.header('Content-Type', 'application/json')
|
|
368
|
+
|
|
369
|
+
response.statusCode = statusCode
|
|
370
|
+
response.source = {
|
|
371
|
+
errorMessage: message,
|
|
372
|
+
errorType: error.constructor.name,
|
|
373
|
+
offlineInfo:
|
|
374
|
+
'If you believe this is an issue with serverless-offline please submit it, thanks. https://github.com/dherault/serverless-offline/issues',
|
|
375
|
+
stackTrace: this.#getArrayStackTrace(error.stack),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return response
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#reply502(response, message, error) {
|
|
382
|
+
// APIG replies 502 by default on failures;
|
|
383
|
+
return this.#replyError(502, response, message, error)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#getArrayStackTrace(stack) {
|
|
387
|
+
if (!stack) return null
|
|
388
|
+
|
|
389
|
+
const splittedStack = stack.split('\n')
|
|
390
|
+
|
|
391
|
+
return splittedStack
|
|
392
|
+
.slice(
|
|
393
|
+
0,
|
|
394
|
+
splittedStack.findIndex((item) =>
|
|
395
|
+
item.match(/server.route.handler.LambdaContext/),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
.map((line) => line.trim())
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
writeRoutesTerminal() {
|
|
402
|
+
logRoutes(this.#terminalInfo)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Alb.js'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMultiValueHeaders,
|
|
3
|
+
parseMultiValueQueryStringParameters,
|
|
4
|
+
} from '../../../utils/index.js'
|
|
5
|
+
|
|
6
|
+
const { fromEntries } = Object
|
|
7
|
+
|
|
8
|
+
export default class LambdaAlbRequestEvent {
|
|
9
|
+
#path = null
|
|
10
|
+
|
|
11
|
+
#request = null
|
|
12
|
+
|
|
13
|
+
#stage = null
|
|
14
|
+
|
|
15
|
+
constructor(request, stage, path) {
|
|
16
|
+
this.#path = path
|
|
17
|
+
this.#request = request
|
|
18
|
+
this.#stage = stage
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
create() {
|
|
22
|
+
const { method } = this.#request
|
|
23
|
+
const { rawHeaders, url } = this.#request.raw.req
|
|
24
|
+
const httpMethod = method.toUpperCase()
|
|
25
|
+
|
|
26
|
+
const queryStringParameters = this.#request.url.search
|
|
27
|
+
? fromEntries(Array.from(this.#request.url.searchParams))
|
|
28
|
+
: null
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
body: this.#request.payload,
|
|
32
|
+
headers: this.#request.headers,
|
|
33
|
+
httpMethod,
|
|
34
|
+
isBase64Encoded: false,
|
|
35
|
+
multiValueHeaders: parseMultiValueHeaders(
|
|
36
|
+
// NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject())
|
|
37
|
+
rawHeaders || [],
|
|
38
|
+
),
|
|
39
|
+
multiValueQueryStringParameters:
|
|
40
|
+
parseMultiValueQueryStringParameters(url),
|
|
41
|
+
path: this.#path,
|
|
42
|
+
queryStringParameters,
|
|
43
|
+
requestContext: {
|
|
44
|
+
elb: {
|
|
45
|
+
targetGroupArn:
|
|
46
|
+
// TODO: probably replace this
|
|
47
|
+
'arn:aws:elasticloadbalancing:us-east-1:550213415212:targetgroup/5811b5d6aff964cd50efa8596604c4e0/b49d49c443aa999f',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './LambdaAlbRequestEvent.js'
|
|
@@ -78,9 +78,12 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (authorization === undefined) {
|
|
81
|
-
|
|
81
|
+
log.error(
|
|
82
82
|
`Identity Source is null for ${identitySourceType} ${identitySourceField} (λ: ${authFunName})`,
|
|
83
83
|
)
|
|
84
|
+
return Boom.unauthorized(
|
|
85
|
+
'User is not authorized to access this resource',
|
|
86
|
+
)
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
const identityValidationExpression = new RegExp(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { EOL, platform } from 'node:os'
|
|
3
3
|
import { delimiter, dirname, join, relative, resolve } from 'node:path'
|
|
4
|
-
import process, { cwd } from 'node:process'
|
|
4
|
+
import process, { cwd, nextTick } from 'node:process'
|
|
5
5
|
import { createInterface } from 'node:readline'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { log } from '@serverless/utils/log.js'
|
|
@@ -99,7 +99,7 @@ export default class PythonRunner {
|
|
|
99
99
|
// invoke.py, based on:
|
|
100
100
|
// https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/invoke.py
|
|
101
101
|
async run(event, context) {
|
|
102
|
-
return new Promise((
|
|
102
|
+
return new Promise((res, rej) => {
|
|
103
103
|
const input = stringify({
|
|
104
104
|
context,
|
|
105
105
|
event,
|
|
@@ -117,18 +117,17 @@ export default class PythonRunner {
|
|
|
117
117
|
if (parsed) {
|
|
118
118
|
this.#handlerProcess.stdout.readline.removeListener('line', onLine)
|
|
119
119
|
this.#handlerProcess.stderr.removeListener('data', onErr)
|
|
120
|
-
|
|
120
|
+
res(parsed)
|
|
121
121
|
}
|
|
122
|
-
return null
|
|
123
122
|
} catch (err) {
|
|
124
|
-
|
|
123
|
+
rej(err)
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
this.#handlerProcess.stdout.readline.on('line', onLine)
|
|
129
128
|
this.#handlerProcess.stderr.on('data', onErr)
|
|
130
129
|
|
|
131
|
-
|
|
130
|
+
nextTick(() => {
|
|
132
131
|
this.#handlerProcess.stdin.write(input)
|
|
133
132
|
this.#handlerProcess.stdin.write('\n')
|
|
134
133
|
})
|
|
@@ -19,3 +19,32 @@ export default function generateHapiPath(path, options, serverless) {
|
|
|
19
19
|
|
|
20
20
|
return hapiPath
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
export function generateAlbHapiPath(path, options, serverless) {
|
|
24
|
+
// path must start with '/'
|
|
25
|
+
let hapiPath = path.startsWith('/') ? path : `/${path}`
|
|
26
|
+
|
|
27
|
+
if (!options.noPrependStageInUrl) {
|
|
28
|
+
const stage = options.stage || serverless.service.provider.stage
|
|
29
|
+
// prepend the stage to path
|
|
30
|
+
hapiPath = `/${stage}${hapiPath}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.prefix) {
|
|
34
|
+
hapiPath = `/${options.prefix}${hapiPath}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
hapiPath !== '/' &&
|
|
39
|
+
hapiPath.endsWith('/') &&
|
|
40
|
+
!options.noStripTrailingSlashInUrl
|
|
41
|
+
) {
|
|
42
|
+
hapiPath = hapiPath.slice(0, -1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; hapiPath.includes('*'); i += 1) {
|
|
46
|
+
hapiPath = hapiPath.replace('*', `{${i}}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return hapiPath
|
|
50
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { default as parseMultiValueQueryStringParameters } from './parseMultiVal
|
|
|
16
16
|
export { default as parseQueryStringParameters } from './parseQueryStringParameters.js'
|
|
17
17
|
export { default as parseQueryStringParametersForPayloadV2 } from './parseQueryStringParametersForPayloadV2.js'
|
|
18
18
|
export { default as splitHandlerPathAndName } from './splitHandlerPathAndName.js'
|
|
19
|
-
|
|
19
|
+
export { generateAlbHapiPath } from './generateHapiPath.js'
|
|
20
20
|
// export { default as baseImage } from './baseImage.js'
|
|
21
21
|
|
|
22
22
|
const { isArray } = Array
|