@srfnstack/spliffy 0.5.5 → 0.8.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/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/package.json +14 -6
- package/src/content-types.mjs +75 -0
- package/src/content.mjs +95 -0
- package/src/decorator.mjs +205 -0
- package/src/handler.mjs +240 -0
- package/src/httpStatusCodes.mjs +103 -0
- package/src/{index.js → index.mjs} +14 -18
- package/src/log.mjs +59 -0
- package/src/middleware.mjs +90 -0
- package/src/nodeModuleHandler.mjs +61 -0
- package/src/routes.mjs +180 -0
- package/src/server.mjs +126 -0
- package/src/serverConfig.mjs +84 -0
- package/src/start.mjs +25 -0
- package/src/staticHandler.mjs +74 -0
- package/src/url.mjs +24 -0
- package/src/content-types.js +0 -75
- package/src/content.js +0 -56
- package/src/dispatcher.js +0 -190
- package/src/expressShim.js +0 -120
- package/src/log.js +0 -43
- package/src/middleware.js +0 -121
- package/src/nodeModuleHandler.js +0 -54
- package/src/parseUrl.js +0 -43
- package/src/routeUtil.js +0 -67
- package/src/routes.js +0 -191
- package/src/secure.js +0 -53
- package/src/serverConfig.js +0 -70
- package/src/start.js +0 -39
- package/src/staticHandler.js +0 -115
package/LICENSE.txt
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srfnstack/spliffy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"author": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"author": "snowbldr",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/narcolepticsnowman/spliffy",
|
|
7
7
|
"license": "MIT",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git@github.com:narcolepticsnowman/spliffy.git"
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "npm run lint && jest",
|
|
20
|
+
"lint": "standard --env jest src ./*js && standard --env jest --env browser --global Prism docs example",
|
|
21
|
+
"lint:fix": "standard --env jest --fix src ./*js && standard --env jest --env browser --global Prism --fix docs example"
|
|
22
|
+
},
|
|
18
23
|
"keywords": [
|
|
19
24
|
"node",
|
|
20
25
|
"http",
|
|
@@ -24,12 +29,15 @@
|
|
|
24
29
|
"rest"
|
|
25
30
|
],
|
|
26
31
|
"dependencies": {
|
|
27
|
-
"cookie": "^0.4.
|
|
32
|
+
"cookie": "^0.4.1",
|
|
28
33
|
"etag": "^1.8.1",
|
|
29
|
-
"uWebSockets.js": "uNetworking/uWebSockets.js#
|
|
30
|
-
"uuid": "^
|
|
34
|
+
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.4.0",
|
|
35
|
+
"uuid": "^8.3.2"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
|
-
"
|
|
38
|
+
"standard": "^16.0.4",
|
|
39
|
+
"helmet": "^4.6.0",
|
|
40
|
+
"jest": "^27.3.1",
|
|
41
|
+
"node-fetch": "^2.6.6"
|
|
34
42
|
}
|
|
35
43
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'.aac': 'audio/aac',
|
|
3
|
+
'.abw': 'application/x-abiword',
|
|
4
|
+
'.arc': 'application/x-freearc',
|
|
5
|
+
'.avi': 'video/x-msvideo',
|
|
6
|
+
'.azw': 'application/vnd.amazon.ebook',
|
|
7
|
+
'.bin': 'application/octet-stream',
|
|
8
|
+
'.bmp': 'image/bmp',
|
|
9
|
+
'.bz': 'application/x-bzip',
|
|
10
|
+
'.bz2': 'application/x-bzip2',
|
|
11
|
+
'.csh': 'application/x-csh',
|
|
12
|
+
'.css': 'text/css',
|
|
13
|
+
'.csv': 'text/csv',
|
|
14
|
+
'.doc': 'application/msword',
|
|
15
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
16
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
17
|
+
'.epub': 'application/epub+zip',
|
|
18
|
+
'.gif': 'image/gif',
|
|
19
|
+
'.htm': 'text/html',
|
|
20
|
+
'.html': 'text/html',
|
|
21
|
+
'.ico': 'image/vnd.microsoft.icon',
|
|
22
|
+
'.ics': 'text/calendar',
|
|
23
|
+
'.jar': 'application/java-archive',
|
|
24
|
+
'.jpeg': 'image/jpeg',
|
|
25
|
+
'.jpg': 'image/jpeg',
|
|
26
|
+
'.js': 'text/javascript',
|
|
27
|
+
'.json': 'application/json',
|
|
28
|
+
'.jsonld': 'application/ld+json',
|
|
29
|
+
'.mid': 'audio/midi',
|
|
30
|
+
'.midi': 'audio/x-midi',
|
|
31
|
+
'.mjs': 'text/javascript',
|
|
32
|
+
'.mp3': 'audio/mpeg',
|
|
33
|
+
'.mpeg': 'video/mpeg',
|
|
34
|
+
'.mpkg': 'application/vnd.apple.installer+xml',
|
|
35
|
+
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
|
36
|
+
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
|
37
|
+
'.odt': 'application/vnd.oasis.opendocument.text',
|
|
38
|
+
'.oga': 'audio/ogg',
|
|
39
|
+
'.ogv': 'video/ogg',
|
|
40
|
+
'.ogg': 'application/ogg',
|
|
41
|
+
'.ogx': 'application/ogg',
|
|
42
|
+
'.otf': 'font/otf',
|
|
43
|
+
'.png': 'image/png',
|
|
44
|
+
'.pdf': 'application/pdf',
|
|
45
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
46
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
47
|
+
'.rar': 'application/x-rar-compressed',
|
|
48
|
+
'.rtf': 'application/rtf',
|
|
49
|
+
'.sh': 'application/x-sh',
|
|
50
|
+
'.svg': 'image/svg+xml',
|
|
51
|
+
'.swf': 'application/x-shockwave-flash',
|
|
52
|
+
'.tar': 'application/x-tar',
|
|
53
|
+
'.tif': 'image/tiff',
|
|
54
|
+
'.tiff': 'font/ttf',
|
|
55
|
+
'.ts': 'video/mp2t',
|
|
56
|
+
'.ttf': 'font/ttf ',
|
|
57
|
+
'.txt': 'text/plain',
|
|
58
|
+
'.vsd': 'application/vnd.visio',
|
|
59
|
+
'.wav': 'audio/wav',
|
|
60
|
+
'.weba': 'audio/webm',
|
|
61
|
+
'.webm': 'video/webm',
|
|
62
|
+
'.webp': 'image/webp',
|
|
63
|
+
'.woff': 'font/woff',
|
|
64
|
+
'.woff2': 'font/woff2',
|
|
65
|
+
'.xhtml': 'application/xhtml+xml',
|
|
66
|
+
'.xls': 'application/vnd.ms-excel',
|
|
67
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
68
|
+
'.xml': 'application/xml',
|
|
69
|
+
'.xul': 'application/vnd.mozilla.xul+xml',
|
|
70
|
+
'.zip': 'application/zip',
|
|
71
|
+
'.3gp': 'video/3gpp',
|
|
72
|
+
'.3g2': 'video/3gpp2',
|
|
73
|
+
'.7z': 'application/x-7z-compressed ',
|
|
74
|
+
default: 'application/octet-stream'
|
|
75
|
+
}
|
package/src/content.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import contentTypes from './content-types.mjs'
|
|
2
|
+
import { parseQuery } from './url.mjs'
|
|
3
|
+
|
|
4
|
+
const defaultHandler = {
|
|
5
|
+
deserialize: o => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(o && o.toString())
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return o
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
serialize: o => {
|
|
13
|
+
if (typeof o === 'string') {
|
|
14
|
+
return {
|
|
15
|
+
contentType: 'text/plain',
|
|
16
|
+
data: o
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (o instanceof Buffer) {
|
|
20
|
+
return {
|
|
21
|
+
contentType: 'application/octet-stream',
|
|
22
|
+
data: o
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
contentType: 'application/json',
|
|
27
|
+
data: JSON.stringify(o)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const toFormData = (key, value) => {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map(toFormData).flat()
|
|
35
|
+
} else if (typeof value === 'object') {
|
|
36
|
+
return Object.keys(value).map(k => toFormData(`${key}.${k}`, value[k])).flat()
|
|
37
|
+
} else {
|
|
38
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const contentHandlers = {
|
|
43
|
+
'application/json': {
|
|
44
|
+
deserialize: s => JSON.parse(s && s.toString()),
|
|
45
|
+
serialize: o => JSON.stringify(o)
|
|
46
|
+
},
|
|
47
|
+
'text/plain': {
|
|
48
|
+
deserialize: s => s && s.toString(),
|
|
49
|
+
serialize: o => o && o.toString()
|
|
50
|
+
},
|
|
51
|
+
'application/octet-stream': defaultHandler,
|
|
52
|
+
'application/x-www-form-urlencoded': {
|
|
53
|
+
deserialize: s => s && parseQuery(s.toString(), true),
|
|
54
|
+
serialize: o => Object.keys(o).map(toFormData).flat().join('&')
|
|
55
|
+
},
|
|
56
|
+
'*/*': defaultHandler
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getHandler (contentType, acceptsDefault) {
|
|
60
|
+
if (!contentType) return contentHandlers[acceptsDefault]
|
|
61
|
+
// content-type is singular https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.17
|
|
62
|
+
let handler = contentHandlers[contentType]
|
|
63
|
+
if (!handler && contentType.indexOf(';') > -1) {
|
|
64
|
+
handler = contentHandlers[contentType.split(';')[0].trim()]
|
|
65
|
+
}
|
|
66
|
+
if (handler && typeof handler) {
|
|
67
|
+
if (typeof handler.serialize !== 'function') {
|
|
68
|
+
throw new Error(`Content handlers must provide a serialize function. ${handler}`)
|
|
69
|
+
}
|
|
70
|
+
if (typeof handler.deserialize !== 'function') {
|
|
71
|
+
throw new Error(`Content handlers must provide a deserialize function. ${handler}`)
|
|
72
|
+
}
|
|
73
|
+
return handler
|
|
74
|
+
}
|
|
75
|
+
return contentHandlers[acceptsDefault]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getContentTypeByExtension (name, staticContentTypes) {
|
|
79
|
+
const extension = name.indexOf('.') > -1 ? name.slice(name.lastIndexOf('.')).toLowerCase() : 'default'
|
|
80
|
+
const contentType = staticContentTypes?.[extension] || null
|
|
81
|
+
|
|
82
|
+
return contentType || contentTypes[extension]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function serializeBody (content, contentType, acceptsDefault) {
|
|
86
|
+
return getHandler(contentType && contentType.toLowerCase(), acceptsDefault).serialize(content)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function deserializeBody (content, contentType, acceptsDefault) {
|
|
90
|
+
return getHandler(contentType && contentType.toLowerCase(), acceptsDefault).deserialize(content)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function initContentHandlers (handlers) {
|
|
94
|
+
return Object.assign({}, contentHandlers, handlers)
|
|
95
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import cookie from 'cookie'
|
|
2
|
+
import http from 'http'
|
|
3
|
+
import { parseQuery, setMultiValueKey } from './url.mjs'
|
|
4
|
+
import log from './log.mjs'
|
|
5
|
+
import { v4 as uuid } from 'uuid'
|
|
6
|
+
import stream from 'stream'
|
|
7
|
+
import httpStatusCodes, { defaultStatusMessages } from './httpStatusCodes.mjs'
|
|
8
|
+
const { Writable } = stream
|
|
9
|
+
|
|
10
|
+
const addressArrayBufferToString = addrBuf => String.fromCharCode.apply(null, new Int8Array(addrBuf))
|
|
11
|
+
const excludedMessageProps = {
|
|
12
|
+
setTimeout: true,
|
|
13
|
+
_read: true,
|
|
14
|
+
destroy: true,
|
|
15
|
+
_addHeaderLines: true,
|
|
16
|
+
_addHeaderLine: true,
|
|
17
|
+
_dump: true,
|
|
18
|
+
__proto__: true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizeHeader = header => header.toLowerCase()
|
|
22
|
+
|
|
23
|
+
const reqProtoProps = () => Object.keys(http.IncomingMessage.prototype).filter(p => !excludedMessageProps[p])
|
|
24
|
+
|
|
25
|
+
export const setCookie = (res) => function () {
|
|
26
|
+
return res.setHeader('Set-Cookie', [...(res.getHeader('Set-Cookie') || []), cookie.serialize(...arguments)])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decorateRequest (uwsReq, pathParameters, res, { decodeQueryParameters, decodePathParameters, parseCookie } = {}) {
|
|
30
|
+
// uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
|
|
31
|
+
const req = {}
|
|
32
|
+
// frameworks like passport like to modify the message prototype
|
|
33
|
+
// Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
|
|
34
|
+
for (const p of reqProtoProps()) {
|
|
35
|
+
if (!req[p]) req[p] = http.IncomingMessage.prototype[p]
|
|
36
|
+
}
|
|
37
|
+
const query = uwsReq.getQuery()
|
|
38
|
+
req.path = uwsReq.getUrl()
|
|
39
|
+
req.url = `${req.path}${query ? '?' + query : ''}`
|
|
40
|
+
req.spliffyUrl = {
|
|
41
|
+
path: uwsReq.getUrl(),
|
|
42
|
+
query: parseQuery(uwsReq.getQuery(), decodeQueryParameters)
|
|
43
|
+
}
|
|
44
|
+
req.spliffyUrl.pathParameters = {}
|
|
45
|
+
if (pathParameters && pathParameters.length > 0) {
|
|
46
|
+
for (const i in pathParameters) {
|
|
47
|
+
req.spliffyUrl.pathParameters[pathParameters[i]] = decodePathParameters
|
|
48
|
+
? decodeURIComponent(uwsReq.getParameter(i))
|
|
49
|
+
: uwsReq.getParameter(i)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
req.params = req.spliffyUrl.pathParameters
|
|
53
|
+
req.headers = {}
|
|
54
|
+
req.method = uwsReq.getMethod().toUpperCase()
|
|
55
|
+
req.remoteAddress = addressArrayBufferToString(res.getRemoteAddressAsText())
|
|
56
|
+
req.proxiedRemoteAddress = addressArrayBufferToString(res.getProxiedRemoteAddressAsText())
|
|
57
|
+
uwsReq.forEach((header, value) => setMultiValueKey(req.headers, normalizeHeader(header), value))
|
|
58
|
+
req.get = header => req.headers[normalizeHeader(header)]
|
|
59
|
+
if (parseCookie && req.headers.cookie) {
|
|
60
|
+
req.cookies = cookie.parse(req.headers.cookie) || {}
|
|
61
|
+
}
|
|
62
|
+
return req
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decorateResponse (res, req, finalizeResponse, errorTransformer, endError, { acceptsDefault }) {
|
|
66
|
+
res.onAborted(() => {
|
|
67
|
+
res.ended = true
|
|
68
|
+
res.writableEnded = true
|
|
69
|
+
res.finalized = true
|
|
70
|
+
log.error(`Request to ${req.url} was aborted`)
|
|
71
|
+
})
|
|
72
|
+
res.acceptsDefault = acceptsDefault
|
|
73
|
+
res.headers = {}
|
|
74
|
+
res.headersSent = false
|
|
75
|
+
res.setHeader = (header, value) => {
|
|
76
|
+
res.headers[normalizeHeader(header)] = value
|
|
77
|
+
}
|
|
78
|
+
res.removeHeader = header => {
|
|
79
|
+
delete res.headers[normalizeHeader(header)]
|
|
80
|
+
}
|
|
81
|
+
res.flushHeaders = () => {
|
|
82
|
+
if (res.headersSent) return
|
|
83
|
+
if (!res.statusCode) res.statusCode = httpStatusCodes.OK
|
|
84
|
+
if (!res.statusMessage) res.statusMessage = defaultStatusMessages[res.statusCode]
|
|
85
|
+
res.headersSent = true
|
|
86
|
+
res.writeStatus(`${res.statusCode} ${res.statusMessage}`)
|
|
87
|
+
if (typeof res.onFlushHeaders === 'function') {
|
|
88
|
+
res.onFlushHeaders(res)
|
|
89
|
+
}
|
|
90
|
+
for (const header of Object.keys(res.headers)) {
|
|
91
|
+
if (Array.isArray(res.headers[header])) {
|
|
92
|
+
for (const multiple of res.headers[header]) {
|
|
93
|
+
res.writeHeader(header, multiple.toString())
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
res.writeHeader(header, res.headers[header].toString())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
res.writeHead = (status, headers) => {
|
|
101
|
+
res.statusCode = status
|
|
102
|
+
res.assignHeaders(headers)
|
|
103
|
+
}
|
|
104
|
+
res.assignHeaders = headers => {
|
|
105
|
+
for (const header of Object.keys(headers)) {
|
|
106
|
+
res.headers[normalizeHeader(header)] = headers[header]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
res.getHeader = header => {
|
|
110
|
+
return res.headers[normalizeHeader(header)]
|
|
111
|
+
}
|
|
112
|
+
res.status = (code) => {
|
|
113
|
+
res.statusCode = code
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toArrayBuffer (buffer) {
|
|
118
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let outStream
|
|
122
|
+
res.getWritable = () => {
|
|
123
|
+
if (!outStream) {
|
|
124
|
+
res.streaming = true
|
|
125
|
+
outStream = new Writable({
|
|
126
|
+
write: (chunk, encoding, cb) => {
|
|
127
|
+
try {
|
|
128
|
+
res.flushHeaders()
|
|
129
|
+
const result = res.write(chunk)
|
|
130
|
+
if (typeof cb === 'function') {
|
|
131
|
+
cb()
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (typeof cb === 'function') {
|
|
136
|
+
cb(e)
|
|
137
|
+
} else {
|
|
138
|
+
throw e
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.on('finish', res.end)
|
|
144
|
+
.on('end', res.end)
|
|
145
|
+
.on('error', e => {
|
|
146
|
+
try {
|
|
147
|
+
outStream.destroy()
|
|
148
|
+
} finally {
|
|
149
|
+
endError(res, e, uuid(), errorTransformer)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return outStream
|
|
154
|
+
}
|
|
155
|
+
res.writeArrayBuffer = res.write
|
|
156
|
+
res.write = chunk => {
|
|
157
|
+
res.streaming = true
|
|
158
|
+
res.flushHeaders()
|
|
159
|
+
if (chunk instanceof Buffer) {
|
|
160
|
+
return res.writeArrayBuffer(toArrayBuffer(chunk))
|
|
161
|
+
} else if (typeof chunk === 'string') {
|
|
162
|
+
return res.writeArrayBuffer(toArrayBuffer(Buffer.from(chunk, 'utf8')))
|
|
163
|
+
} else {
|
|
164
|
+
return res.writeArrayBuffer(toArrayBuffer(Buffer.from(JSON.stringify(chunk), 'utf8')))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const uwsEnd = res.end
|
|
169
|
+
res.ended = false
|
|
170
|
+
res.end = body => {
|
|
171
|
+
if (res.ended) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
// provide writableEnded like node does, with slightly different behavior
|
|
175
|
+
if (!res.writableEnded) {
|
|
176
|
+
res.flushHeaders()
|
|
177
|
+
uwsEnd.call(res, body)
|
|
178
|
+
res.writableEnded = true
|
|
179
|
+
res.ended = true
|
|
180
|
+
}
|
|
181
|
+
if (typeof res.onEnd === 'function') {
|
|
182
|
+
res.onEnd()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
res.redirect = function (code, location) {
|
|
187
|
+
if (arguments.length === 1) {
|
|
188
|
+
location = code
|
|
189
|
+
code = httpStatusCodes.MOVED_PERMANENTLY
|
|
190
|
+
}
|
|
191
|
+
return finalizeResponse(req, res, {
|
|
192
|
+
statusCode: code,
|
|
193
|
+
headers: {
|
|
194
|
+
location: location
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
res.send = (body) => {
|
|
199
|
+
finalizeResponse(req, res, body)
|
|
200
|
+
}
|
|
201
|
+
res.json = res.send
|
|
202
|
+
res.setCookie = setCookie(res)
|
|
203
|
+
res.cookie = res.setCookie
|
|
204
|
+
return res
|
|
205
|
+
}
|
package/src/handler.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import log from './log.mjs'
|
|
2
|
+
import { deserializeBody, serializeBody } from './content.mjs'
|
|
3
|
+
import { invokeMiddleware } from './middleware.mjs'
|
|
4
|
+
import { decorateResponse, decorateRequest } from './decorator.mjs'
|
|
5
|
+
import { v4 as uuid } from 'uuid'
|
|
6
|
+
import stream from 'stream'
|
|
7
|
+
const { Readable } = stream
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute the handler
|
|
11
|
+
* @param url The url being requested
|
|
12
|
+
* @param res The uws response object
|
|
13
|
+
* @param req The uws request object
|
|
14
|
+
* @param body The request body
|
|
15
|
+
* @param handler The handler function for the route
|
|
16
|
+
* @param middleware The middleware that applies to this request
|
|
17
|
+
* @param errorTransformer An errorTransformer to convert error objects into response data
|
|
18
|
+
*/
|
|
19
|
+
const executeHandler = async (url, res, req, body, handler, middleware, errorTransformer) => {
|
|
20
|
+
try {
|
|
21
|
+
if (body) body = deserializeBody(body, req.headers['content-type'], res.acceptsDefault)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
log.error('Failed to parse request.', e)
|
|
24
|
+
end(res, 400, handler.statusCodeOverride)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const handled = await handler({ url, body, headers: req.headers, req, res })
|
|
30
|
+
finalizeResponse(req, res, handled, handler.statusCodeOverride)
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const refId = uuid()
|
|
33
|
+
log.error('handler failed', e, refId)
|
|
34
|
+
await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
|
|
35
|
+
endError(res, e, refId, errorTransformer)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const endError = (res, e, refId, errorTransformer) => {
|
|
40
|
+
if (e.body && typeof e.body !== 'string') {
|
|
41
|
+
e.body = JSON.stringify(e.body)
|
|
42
|
+
}
|
|
43
|
+
if (typeof errorTransformer === 'function') {
|
|
44
|
+
e = errorTransformer(e, refId)
|
|
45
|
+
}
|
|
46
|
+
res.headers['x-ref-id'] = refId
|
|
47
|
+
end(res, e.statusCode || 500, null, e.body || '')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const end = (res, defaultStatusCode, statusCodeOverride, body) => {
|
|
51
|
+
// status set directly on res wins
|
|
52
|
+
res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
|
|
53
|
+
if (body instanceof Readable || res.streaming) {
|
|
54
|
+
res.streaming = true
|
|
55
|
+
if (body instanceof Readable) {
|
|
56
|
+
res.flushHeaders()
|
|
57
|
+
pipeResponse(res, body)
|
|
58
|
+
}
|
|
59
|
+
// handler is responsible for ending the response if they are streaming
|
|
60
|
+
} else {
|
|
61
|
+
res.end(doSerializeBody(body, res) || '')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const writeAccess = function (req, res) {
|
|
66
|
+
const start = new Date().getTime()
|
|
67
|
+
return () => {
|
|
68
|
+
log.access(req.remoteAddress, res.proxiedRemoteAddress || '', res.statusCode, req.method, req.url, new Date().getTime() - start + 'ms')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalizeResponse = (req, res, handled, statusCodeOverride) => {
|
|
73
|
+
if (!res.finalized) {
|
|
74
|
+
if (!handled) {
|
|
75
|
+
// if no error was thrown, assume everything is fine. Otherwise each handler must return truthy which is un-necessary for methods that don't need to return anything
|
|
76
|
+
end(res, 200, statusCodeOverride)
|
|
77
|
+
} else {
|
|
78
|
+
// if the returned object has known fields, treat it as a response object instead of the body
|
|
79
|
+
if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
|
|
80
|
+
if (handled.headers) {
|
|
81
|
+
res.assignHeaders(handled.headers)
|
|
82
|
+
}
|
|
83
|
+
res.statusMessage = handled.statusMessage || res.statusMessage
|
|
84
|
+
end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
|
|
85
|
+
} else {
|
|
86
|
+
end(res, 200, statusCodeOverride, handled)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
res.finalized = true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pipeResponse = (res, readStream, errorTransformer) => {
|
|
94
|
+
readStream.on('data', res.write)
|
|
95
|
+
.on('end', res.end)
|
|
96
|
+
.on('error', e => {
|
|
97
|
+
try {
|
|
98
|
+
readStream.destroy()
|
|
99
|
+
} finally {
|
|
100
|
+
endError(res, e, uuid(), errorTransformer)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const doSerializeBody = (body, res) => {
|
|
106
|
+
const contentType = res.getHeader('content-type')
|
|
107
|
+
if (typeof body === 'string') {
|
|
108
|
+
if (!contentType) {
|
|
109
|
+
res.headers['content-type'] = 'text/plain'
|
|
110
|
+
}
|
|
111
|
+
return body
|
|
112
|
+
} else if (body instanceof Readable) {
|
|
113
|
+
return body
|
|
114
|
+
}
|
|
115
|
+
const serialized = serializeBody(body, contentType, res.acceptsDefault)
|
|
116
|
+
|
|
117
|
+
if (serialized?.contentType && !contentType) {
|
|
118
|
+
res.headers['content-type'] = serialized.contentType
|
|
119
|
+
}
|
|
120
|
+
return serialized?.data || ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function executeMiddleware (middleware, req, res, errorTransformer, refId, e) {
|
|
124
|
+
if (!middleware) return
|
|
125
|
+
|
|
126
|
+
let applicableMiddleware = middleware[req.method]
|
|
127
|
+
if (middleware.ALL) {
|
|
128
|
+
if (applicableMiddleware) applicableMiddleware = middleware.ALL.concat(applicableMiddleware)
|
|
129
|
+
else applicableMiddleware = middleware.ALL
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!applicableMiddleware || applicableMiddleware.length === 0) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (e) {
|
|
136
|
+
await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 4), req, res, e)
|
|
137
|
+
} else {
|
|
138
|
+
await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 3), req, res)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let currentDate = new Date().toUTCString()
|
|
143
|
+
setInterval(() => { currentDate = new Date().toUTCString() }, 1000)
|
|
144
|
+
|
|
145
|
+
const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
|
|
146
|
+
res.headers.date = currentDate
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
let reqBody
|
|
150
|
+
if (!handler.streamRequestBody) {
|
|
151
|
+
let buffer
|
|
152
|
+
reqBody = await new Promise(
|
|
153
|
+
resolve =>
|
|
154
|
+
res.onData(async (data, isLast) => {
|
|
155
|
+
if (isLast) {
|
|
156
|
+
buffer = data.byteLength > 0 ? Buffer.concat([buffer, Buffer.from(data)].filter(b => b)) : buffer
|
|
157
|
+
resolve(buffer)
|
|
158
|
+
}
|
|
159
|
+
buffer = Buffer.concat([buffer, Buffer.from(data)].filter(b => b))
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
} else {
|
|
163
|
+
reqBody = new Readable({
|
|
164
|
+
read: () => {
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
res.onData(async (data, isLast) => {
|
|
168
|
+
if (data.byteLength === 0 && !isLast) return
|
|
169
|
+
// data must be copied so it isn't lost
|
|
170
|
+
reqBody.push(Buffer.concat([Buffer.from(data)]))
|
|
171
|
+
if (isLast) {
|
|
172
|
+
reqBody.push(null)
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
await executeMiddleware(middleware, req, res, errorTransformer)
|
|
177
|
+
if (!res.writableEnded && !res.ended) {
|
|
178
|
+
await executeHandler(req.spliffyUrl, res, req, reqBody, handler, middleware, errorTransformer)
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
const refId = uuid()
|
|
182
|
+
log.error('Handling request failed', e, refId)
|
|
183
|
+
await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
|
|
184
|
+
if (!res.writableEnded) { endError(res, e, refId, errorTransformer) }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE']
|
|
189
|
+
|
|
190
|
+
export const createHandler = (handler, middleware, pathParameters, config) => function (res, req) {
|
|
191
|
+
try {
|
|
192
|
+
req = decorateRequest(req, pathParameters, res, config)
|
|
193
|
+
res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
|
|
194
|
+
|
|
195
|
+
if (config.logAccess) {
|
|
196
|
+
res.onEnd = writeAccess(req, res)
|
|
197
|
+
}
|
|
198
|
+
handleRequest(req, res, handler, middleware, config.errorTransformer)
|
|
199
|
+
.catch(e => log.error('Failed handling request', e))
|
|
200
|
+
} catch (e) {
|
|
201
|
+
log.error('Failed handling request', e)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const createNotFoundHandler = config => {
|
|
206
|
+
const handler = config.defaultRouteHandler || config.notFoundRouteHandler
|
|
207
|
+
const params = handler?.pathParameters || []
|
|
208
|
+
return (res, req) => {
|
|
209
|
+
try {
|
|
210
|
+
req = decorateRequest(req, params, res, config)
|
|
211
|
+
res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
|
|
212
|
+
if (config.logAccess) {
|
|
213
|
+
res.onEnd = writeAccess(req, res)
|
|
214
|
+
}
|
|
215
|
+
if (handler && typeof handler === 'object') {
|
|
216
|
+
if (handler.handlers && typeof handler.handlers[req.method] === 'function') {
|
|
217
|
+
if ('statusCodeOverride' in handler) {
|
|
218
|
+
handler.handlers[req.method].statusCodeOverride = handler.statusCodeOverride
|
|
219
|
+
}
|
|
220
|
+
handleRequest(req, res,
|
|
221
|
+
handler.handlers[req.method],
|
|
222
|
+
handler.middleware,
|
|
223
|
+
config.errorTransformer
|
|
224
|
+
).catch((e) => {
|
|
225
|
+
log.error('Unexpected exception during request handling', e)
|
|
226
|
+
res.statusCode = 500
|
|
227
|
+
})
|
|
228
|
+
} else {
|
|
229
|
+
res.statusCode = 405
|
|
230
|
+
res.end()
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
res.statusCode = 404
|
|
234
|
+
res.end()
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
log.error('Failed handling request', e)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|