scf-framework
Version:
Support Node.js framework for SCF
203 lines (172 loc) • 5.08 kB
JavaScript
const http = require('http')
const url = require('url')
const isType = require('type-is')
const binaryMimeTypes = [
'application/octet-stream',
'font/eot',
'font/opentype',
'font/otf',
'image/jpeg',
'image/png',
'image/svg+xml'
]
function getPathWithQueryStringParams(event) {
return url.format({
pathname: event.path,
query: event.queryStringParameters
})
}
function getEventBody(event) {
return Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
}
function clone(json) {
return JSON.parse(JSON.stringify(json))
}
function getContentType(params) {
// only compare mime type; ignore encoding part
return params.contentTypeHeader ? params.contentTypeHeader.split(';')[0] : ''
}
function isContentTypeBinaryMimeType(contentType) {
return isType.is(contentType, binaryMimeTypes)
}
function mapEventToHttpRequest(event, context, socketPath) {
const headers = Object.assign({}, event.headers)
// 计算Content-Length并设置
if (event.body && !headers['Content-Length']) {
const body = getEventBody(event)
headers['Content-Length'] = Buffer.byteLength(body)
}
const clonedEventWithoutBody = clone(event)
delete clonedEventWithoutBody.body
headers['x-apigateway-event'] = encodeURIComponent(
JSON.stringify(clonedEventWithoutBody)
)
headers['x-apigateway-context'] = encodeURIComponent(JSON.stringify(context))
return {
method: event.httpMethod,
path: getPathWithQueryStringParams(event),
headers,
socketPath
}
}
function forwardResponse(response, resolver) {
let buf = []
response
.on('data', chunk => buf.push(chunk))
.on('end', () => {
const bodyBuffer = Buffer.concat(buf)
const statusCode = response.statusCode
const headers = response.headers
// apigateway暂时不支持chunked transfer
if (headers['transfer-encoding'] === 'chunked') {
delete headers['transfer-encoding']
}
//如果头部传入数组,后续要特殊处理(比如,set-cookie暂时不支持多个)
Object.keys(headers).forEach(h => {
if (Array.isArray(headers[h])) {
headers[h] = headers[h].join(',')
}
})
const contentType = getContentType({
contentTypeHeader: headers['content-type']
})
const isBase64Encoded = isContentTypeBinaryMimeType(contentType)
const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8')
const successResponse = { statusCode, body, headers, isBase64Encoded }
resolver(successResponse)
})
}
//连接错误
function forwardConnectionErrorResponse(error, resolver) {
console.log('ERROR: connection error')
console.error(error)
const errorResponse = {
statusCode: 502,
body: '',
headers: {}
}
resolver(errorResponse)
}
function forwardLibraryErrorResponse(error, resolver) {
console.log('ERROR: system error')
console.error(error)
const errorResponse = {
statusCode: 500,
body: '',
headers: {}
}
resolver(errorResponse)
}
function forwardRequest(server, event, context, resolve, reject) {
try {
const requestOptions = mapEventToHttpRequest(
event,
context,
getSocketPath(server._socketPathSuffix)
)
const req = http.request(requestOptions, response =>
forwardResponse(response, resolve)
)
if (event.body) {
const body = getEventBody(event)
req.write(body)
}
req
.on('error', error => forwardConnectionErrorResponse(error, reject))
.end()
} catch (error) {
forwardLibraryErrorResponse(error, reject)
return server
}
}
function startServer(server) {
return server.listen(getSocketPath(server._socketPathSuffix))
}
function getSocketPath(socketPathSuffix) {
return `/tmp/server-${socketPathSuffix}.sock`
}
function getRandomString() {
return Math.random()
.toString(36)
.substring(2, 15)
}
function createServer(app) {
const server = http.createServer(app.callback ? app.callback() : app)
server._socketPathSuffix = getRandomString()
server.on('listening', () => {
server._isListening = true
})
server
.on('close', () => {
server._isListening = false
})
.on('error', error => {
if (error.code === 'EADDRINUSE') {
console.warn(
`WARNING: Attempting to listen on socket ${getSocketPath(
server._socketPathSuffix
)}, but it is already in use.`
)
server._socketPathSuffix = getRandomString()
return server.close(() => startServer(server))
} else {
console.log('ERROR: server error')
console.error(error)
}
})
return server
}
function proxy(server, event, context) {
return new Promise((resolve, reject) => {
if (server._isListening) {
forwardRequest(server, event, context, resolve, reject)
} else {
startServer(server).on('listening', () =>
forwardRequest(server, event, context, resolve, reject)
)
}
})
}
exports.createServer = createServer
exports.proxy = proxy