UNPKG

7.52 kBJavaScriptView Raw
1const express = require('express')
2const bodyParser = require('body-parser')
3const expressLogging = require('express-logging')
4const queryString = require('querystring')
5const chokidar = require('chokidar')
6const jwtDecode = require('jwt-decode')
7const {
8 NETLIFYDEVLOG,
9 // NETLIFYDEVWARN,
10 NETLIFYDEVERR
11} = require('./logo')
12const { getFunctions } = require('./get-functions')
13
14function 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) // eslint-disable-line no-console
19}
20
21// function getHandlerPath(functionPath) {
22// if (functionPath.match(/\.js$/)) {
23// return functionPath;
24// }
25// return path.join(functionPath, `${path.basename(functionPath)}.js`);
26// }
27
28function buildClientContext(headers) {
29 // inject a client context based on auth header, ported over from netlify-lambda (https://github.com/netlify/netlify-lambda/pull/57)
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 // you can decode this with https://jwt.io/
42 // just says
43 // {
44 // "source": "netlify dev",
45 // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY"
46 // }
47 },
48 user: jwtDecode(parts[1])
49 }
50 } catch (_) {
51 // Ignore errors - bearer token is not a JWT, probably not intended for us
52 }
53}
54
55function createHandler(dir) {
56 const functions = getFunctions(dir)
57
58 const clearCache = action => path => {
59 console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`) // eslint-disable-line no-console
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 // handle proxies without path re-writes (http-servr)
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 // we already checked that it exports a function named handler above
117 const promise = handler.handler(
118 lambdaRequest,
119 { clientContext: buildClientContext(request.headers) || {} },
120 callback
121 )
122 /** guard against using BOTH async and callback */
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 // it is definitely an async function with no callback called, good.
129 promiseCallback(promise, callback)
130 }
131
132 /** need to keep createCallback in scope so we can know if cb was called AND handler is async */
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 // eslint-disable-line guard-for-in
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
172function promiseCallback(promise, callback) {
173 if (!promise) return // means no handler was written
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
187async 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) // eslint-disable-line no-console
212 process.exit(1)
213 }
214
215 // add newline because this often appears alongside the client devserver's output
216 console.log(`\n${NETLIFYDEVLOG} Lambda server is listening on ${settings.functionsPort}`) // eslint-disable-line no-console
217 })
218
219 return Promise.resolve()
220}
221
222module.exports = { serveFunctions }