1 | const express = require('express')
|
2 | const bodyParser = require('body-parser')
|
3 | const expressLogging = require('express-logging')
|
4 | const queryString = require('querystring')
|
5 | const chokidar = require('chokidar')
|
6 | const jwtDecode = require('jwt-decode')
|
7 | const {
|
8 | NETLIFYDEVLOG,
|
9 |
|
10 | NETLIFYDEVERR
|
11 | } = require('./logo')
|
12 | const { getFunctions } = require('./get-functions')
|
13 |
|
14 | function handleErr(err, response) {
|
15 | response.statusCode = 500
|
16 | response.write(`${NETLIFYDEVERR} Function invocation failed: ` + err.toString())
|
17 | response.end()
|
18 | console.log(`${NETLIFYDEVERR} Error during invocation: `, err)
|
19 | }
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | function buildClientContext(headers) {
|
29 |
|
30 | if (!headers.authorization) return
|
31 |
|
32 | const parts = headers.authorization.split(' ')
|
33 | if (parts.length !== 2 || parts[0] !== 'Bearer') return
|
34 |
|
35 | try {
|
36 | return {
|
37 | identity: {
|
38 | url: 'https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity',
|
39 | token:
|
40 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI'
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | },
|
48 | user: jwtDecode(parts[1])
|
49 | }
|
50 | } catch (_) {
|
51 |
|
52 | }
|
53 | }
|
54 |
|
55 | function createHandler(dir) {
|
56 | const functions = getFunctions(dir)
|
57 |
|
58 | const clearCache = action => path => {
|
59 | console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`)
|
60 | Object.keys(require.cache).forEach(k => {
|
61 | delete require.cache[k]
|
62 | })
|
63 | }
|
64 | const watcher = chokidar.watch(dir, { ignored: /node_modules/ })
|
65 | watcher.on('change', clearCache('modified')).on('unlink', clearCache('deleted'))
|
66 |
|
67 | return function(request, response) {
|
68 |
|
69 | const cleanPath = request.path.replace(/^\/.netlify\/functions/, '')
|
70 |
|
71 | const func = cleanPath.split('/').filter(function(e) {
|
72 | return e
|
73 | })[0]
|
74 | if (!functions[func]) {
|
75 | response.statusCode = 404
|
76 | response.end('Function not found...')
|
77 | return
|
78 | }
|
79 | const { functionPath, moduleDir } = functions[func]
|
80 | let handler
|
81 | let before = module.paths
|
82 | try {
|
83 | module.paths = [moduleDir]
|
84 | handler = require(functionPath)
|
85 | if (typeof handler.handler !== 'function') {
|
86 | throw new Error(`function ${functionPath} must export a function named handler`)
|
87 | }
|
88 | module.paths = before
|
89 | } catch (error) {
|
90 | module.paths = before
|
91 | handleErr(error, response)
|
92 | return
|
93 | }
|
94 |
|
95 | const body = request.body.toString()
|
96 | var isBase64Encoded = Buffer.from(body, 'base64').toString('base64') === body
|
97 |
|
98 | let remoteAddress =
|
99 | request.headers['x-forwarded-for'] || request.headers['X-Forwarded-for'] || request.connection.remoteAddress || ''
|
100 | remoteAddress = remoteAddress
|
101 | .split(remoteAddress.includes('.') ? ':' : ',')
|
102 | .pop()
|
103 | .trim()
|
104 |
|
105 | const lambdaRequest = {
|
106 | path: request.path,
|
107 | httpMethod: request.method,
|
108 | queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]),
|
109 | headers: Object.assign({}, request.headers, { 'client-ip': remoteAddress }),
|
110 | body: body,
|
111 | isBase64Encoded: isBase64Encoded
|
112 | }
|
113 |
|
114 | let callbackWasCalled = false
|
115 | const callback = createCallback(response)
|
116 |
|
117 | const promise = handler.handler(
|
118 | lambdaRequest,
|
119 | { clientContext: buildClientContext(request.headers) || {} },
|
120 | callback
|
121 | )
|
122 |
|
123 | if (callbackWasCalled && promise && typeof promise.then === 'function') {
|
124 | throw new Error(
|
125 | 'Error: your function seems to be using both a callback and returning a promise (aka async function). This is invalid, pick one. (Hint: async!)'
|
126 | )
|
127 | } else {
|
128 |
|
129 | promiseCallback(promise, callback)
|
130 | }
|
131 |
|
132 |
|
133 | function createCallback(response) {
|
134 | return function(err, lambdaResponse) {
|
135 | callbackWasCalled = true
|
136 | if (err) {
|
137 | return handleErr(err, response)
|
138 | }
|
139 | if (lambdaResponse === undefined) {
|
140 | return handleErr('lambda response was undefined. check your function code again.', response)
|
141 | }
|
142 | if (!Number(lambdaResponse.statusCode)) {
|
143 | console.log(
|
144 | `${NETLIFYDEVERR} Your function response must have a numerical statusCode. You gave: $`,
|
145 | lambdaResponse.statusCode
|
146 | )
|
147 | return handleErr('Incorrect function response statusCode', response)
|
148 | }
|
149 | if (typeof lambdaResponse.body !== 'string') {
|
150 | console.log(`${NETLIFYDEVERR} Your function response must have a string body. You gave:`, lambdaResponse.body)
|
151 | return handleErr('Incorrect function response body', response)
|
152 | }
|
153 |
|
154 | response.statusCode = lambdaResponse.statusCode
|
155 |
|
156 | for (const key in lambdaResponse.headers) {
|
157 | response.setHeader(key, lambdaResponse.headers[key])
|
158 | }
|
159 | for (const key in lambdaResponse.multiValueHeaders) {
|
160 | const items = lambdaResponse.multiValueHeaders[key]
|
161 | response.setHeader(key, items)
|
162 | }
|
163 | response.write(
|
164 | lambdaResponse.isBase64Encoded ? Buffer.from(lambdaResponse.body, 'base64') : lambdaResponse.body
|
165 | )
|
166 | response.end()
|
167 | }
|
168 | }
|
169 | }
|
170 | }
|
171 |
|
172 | function promiseCallback(promise, callback) {
|
173 | if (!promise) return
|
174 | if (typeof promise.then !== 'function') return
|
175 | if (typeof callback !== 'function') return
|
176 |
|
177 | promise.then(
|
178 | function(data) {
|
179 | callback(null, data)
|
180 | },
|
181 | function(err) {
|
182 | callback(err, null)
|
183 | }
|
184 | )
|
185 | }
|
186 |
|
187 | async function serveFunctions(settings) {
|
188 | const app = express()
|
189 | const dir = settings.functionsDir
|
190 |
|
191 | app.use(
|
192 | bodyParser.text({
|
193 | limit: '6mb',
|
194 | type: ['text/*', 'application/json', 'multipart/form-data']
|
195 | })
|
196 | )
|
197 | app.use(bodyParser.raw({ limit: '6mb', type: '*/*' }))
|
198 | app.use(
|
199 | expressLogging(console, {
|
200 | blacklist: ['/favicon.ico']
|
201 | })
|
202 | )
|
203 |
|
204 | app.get('/favicon.ico', function(req, res) {
|
205 | res.status(204).end()
|
206 | })
|
207 | app.all('*', createHandler(dir))
|
208 |
|
209 | app.listen(settings.functionsPort, function(err) {
|
210 | if (err) {
|
211 | console.error(`${NETLIFYDEVERR} Unable to start lambda server: `, err)
|
212 | process.exit(1)
|
213 | }
|
214 |
|
215 |
|
216 | console.log(`\n${NETLIFYDEVLOG} Lambda server is listening on ${settings.functionsPort}`)
|
217 | })
|
218 |
|
219 | return Promise.resolve()
|
220 | }
|
221 |
|
222 | module.exports = { serveFunctions }
|