@srfnstack/spliffy 0.7.0 → 0.9.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.
@@ -0,0 +1,96 @@
1
+ import { initContentHandlers } from './content.mjs'
2
+ import { validateMiddleware } from './middleware.mjs'
3
+ import log from './log.mjs'
4
+
5
+ const defaultHeaders = {
6
+ acceptsDefault: '*/*',
7
+ defaultContentType: '*/*'
8
+ }
9
+ // this is mainly for performance reasons
10
+ const nonsense = [
11
+ 'I\'m toasted',
12
+ 'that hurt',
13
+ 'your interwebs!',
14
+ 'I see a light...',
15
+ 'totally zooted',
16
+ 'misplaced my bits',
17
+ 'maybe reboot?',
18
+ 'what was I doing again?',
19
+ 'my cabbages!!!',
20
+ 'Leeerrroooyyy Jeeenkins',
21
+ 'at least I have chicken'
22
+ ]
23
+
24
+ export const randomNonsense = () => `[OH NO, ${nonsense[Math.floor(Math.random() * nonsense.length)]}]`
25
+
26
+ export async function initConfig (userConfig) {
27
+ const config = Object.assign({}, userConfig)
28
+
29
+ if (!('decodePathParameters' in config)) {
30
+ config.decodePathParameters = false
31
+ }
32
+
33
+ if (!('decodeQueryParams' in config)) {
34
+ config.decodeQueryParams = false
35
+ }
36
+
37
+ if (!('extendIncomingMessage' in config)) {
38
+ config.extendIncomingMessage = false
39
+ }
40
+
41
+ if (!('parseCookie' in config)) {
42
+ config.parseCookie = false
43
+ }
44
+
45
+ if (!('writeDateHeader' in config)) {
46
+ config.writeDateHeader = false
47
+ }
48
+
49
+ config.acceptsDefault = config.acceptsDefault || defaultHeaders.acceptsDefault
50
+ config.defaultContentType = config.defaultContentType || defaultHeaders.defaultContentType
51
+
52
+ config.contentHandlers = initContentHandlers(config.contentHandlers || {})
53
+ config.resolveWithoutExtension = config.resolveWithoutExtension || []
54
+ if (!Array.isArray(config.resolveWithoutExtension)) {
55
+ config.resolveWithoutExtension = [config.resolveWithoutExtension]
56
+ }
57
+
58
+ if (config.resolveWithoutExtension.indexOf('.htm') === -1) {
59
+ config.resolveWithoutExtension.push('.htm')
60
+ }
61
+ if (config.resolveWithoutExtension.indexOf('.html') === -1) {
62
+ config.resolveWithoutExtension.push('.html')
63
+ }
64
+
65
+ if (config.middleware) {
66
+ validateMiddleware(config.middleware)
67
+ }
68
+
69
+ if (!('logAccess' in config)) {
70
+ config.logAccess = false
71
+ }
72
+ if ('logLevel' in config) {
73
+ log.setLogLevel(config.logLevel)
74
+ }
75
+ if (!('ignoreFilesMatching' in config)) {
76
+ config.ignoreFilesMatching = []
77
+ } else if (!Array.isArray(config.ignoreFilesMatching)) {
78
+ config.ignoreFilesMatching = [config.ignoreFilesMatching]
79
+ }
80
+ if (!('allowTestFileRoutes' in config)) {
81
+ config.allowTestFileRoutes = false
82
+ }
83
+ config.port = config.port || 10420
84
+ if (!config.httpPort) {
85
+ config.httpPort = config.port - 1
86
+ }
87
+
88
+ if (config.logger) {
89
+ log.setLogger(config.logger)
90
+ }
91
+
92
+ if ((config.httpsKeyFile && !config.httpsCertFile) || (!config.httpsKeyFile && config.httpsCertFile)) {
93
+ throw new Error('You must provide both httpsKeyFile and httpsCertFile')
94
+ }
95
+ return config
96
+ }
package/src/start.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { initConfig, randomNonsense } from './serverConfig.mjs'
2
+ import log from './log.mjs'
3
+ import { startServer } from './server.mjs'
4
+
5
+ export default async function (config) {
6
+ if (!config || !config.routeDir) {
7
+ throw new Error('You must supply a config object with at least a routeDir property. routeDir should be a full path.')
8
+ }
9
+ process
10
+ .on('unhandledRejection', (reason, p) => {
11
+ log.error(randomNonsense(), reason, 'Unhandled Rejection at Promise', p)
12
+ })
13
+ .on('uncaughtException', (err, origin) => {
14
+ log.error(randomNonsense(), `Caught unhandled exception: ${err}\n` +
15
+ `Exception origin: ${origin}`)
16
+ })
17
+
18
+ log.gne('Starting Spliffy!')
19
+ const configWithDefaults = await initConfig(config)
20
+ return startServer(configWithDefaults).catch(e => {
21
+ log.error(randomNonsense(), 'Exception during startup:', e)
22
+ // Spliffy threw an exception, or a route handler failed to load.
23
+ process.exit(420)
24
+ })
25
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'fs'
2
+ import etag from 'etag'
3
+
4
+ const readFile = async (fullPath) => await new Promise(
5
+ (resolve, reject) =>
6
+ fs.readFile(fullPath, (err, data) => {
7
+ if (err) reject(err)
8
+ else resolve(data)
9
+ }
10
+ )
11
+ )
12
+
13
+ const writeHeaders = (req, res, tag, stat, contentType, staticCacheControl) => {
14
+ if (req.headers['if-none-match'] === tag) {
15
+ res.statusCode = 304
16
+ return
17
+ }
18
+ res.writeHead(200, {
19
+ 'Content-Type': contentType,
20
+ // content-length should not be included because transfer-encoding is chunked
21
+ // see https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 sub section 3.
22
+ // Not all clients are compliant (node-fetch) and throw instead of ignoring the header as specified
23
+ 'Cache-Control': staticCacheControl || 'max-age=600',
24
+ ETag: tag
25
+ })
26
+ }
27
+
28
+ const readStat = async path => new Promise((resolve, reject) =>
29
+ fs.stat(path, (err, stats) =>
30
+ err ? reject(err) : resolve(stats)
31
+ ))
32
+
33
+ export function createStaticHandler (fullPath, contentType, cacheStatic, staticCacheControl) {
34
+ const cache = {}
35
+ return {
36
+ GET: async ({ req, res }) => {
37
+ if (cacheStatic) {
38
+ if (!cache.exists || !cache.stat) {
39
+ cache.exists = fs.existsSync(fullPath)
40
+ if (cache.exists) {
41
+ cache.stat = await readStat(fullPath)
42
+ cache.content = await readFile(fullPath)
43
+ cache.etag = etag(cache.content)
44
+ }
45
+ }
46
+ if (!cache.exists) {
47
+ return {
48
+ statusCode: 404
49
+ }
50
+ }
51
+ writeHeaders(req, res, cache.etag, cache.stat, contentType, staticCacheControl)
52
+ if (res.statusCode === 304) {
53
+ return
54
+ }
55
+ return cache.content
56
+ } else {
57
+ if (!fs.existsSync(fullPath)) {
58
+ return {
59
+ statusCode: 404
60
+ }
61
+ }
62
+ const stat = await readStat(fullPath)
63
+ writeHeaders(req, res, etag(stat), stat, contentType, staticCacheControl)
64
+ if (res.statusCode === 304) {
65
+ return
66
+ }
67
+ if (stat.size === 0) {
68
+ return ''
69
+ }
70
+ return fs.createReadStream(fullPath)
71
+ }
72
+ }
73
+ }
74
+ }
package/src/url.mjs ADDED
@@ -0,0 +1,24 @@
1
+ export function parseQuery (query, decodeQueryParams) {
2
+ const parsed = {}
3
+ if (query) {
4
+ if (decodeQueryParams) {
5
+ query = decodeURIComponent(query.replace(/\+/g, '%20'))
6
+ }
7
+ for (const param of query.split('&')) {
8
+ const eq = param.indexOf('=')
9
+ setMultiValueKey(parsed, param.substr(0, eq), param.substr(eq + 1))
10
+ }
11
+ }
12
+ return parsed
13
+ }
14
+
15
+ export function setMultiValueKey (obj, key, value) {
16
+ if (key in obj) {
17
+ if (!Array.isArray(obj[key])) {
18
+ obj[key] = [obj[key]]
19
+ }
20
+ obj[key].push(value)
21
+ } else {
22
+ obj[key] = value
23
+ }
24
+ }
@@ -1,75 +0,0 @@
1
- module.exports = {
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.js DELETED
@@ -1,98 +0,0 @@
1
- const contentTypes = require( './content-types.js' )
2
- const { parseQuery } = require( "./url" );
3
- let defaultHandler = {
4
- deserialize: o => {
5
- try {
6
- return JSON.parse( o && o.toString() )
7
- } catch( e ) {
8
- return o
9
- }
10
- },
11
- serialize: o => {
12
- if( typeof o === 'string' ) {
13
- return {
14
- contentType: 'text/plain',
15
- data: o
16
- }
17
- }
18
- if( o instanceof Buffer ) {
19
- return {
20
- contentType: 'application/octet-stream',
21
- data: o
22
- }
23
- }
24
- return {
25
- contentType: 'application/json',
26
- data: JSON.stringify( o )
27
- }
28
- }
29
- };
30
-
31
- let toFormData = ( key, value ) => {
32
- if( Array.isArray( value ) ) {
33
- return value.map( toFormData ).flat()
34
- } else if( typeof value === 'object' ) {
35
- return Object.keys( value ).map( k => toFormData( `${key}.${k}`, value[k] ) ).flat()
36
- } else {
37
- return `${encodeURIComponent( key )}=${encodeURIComponent( value )}`
38
- }
39
- };
40
-
41
- const contentHandlers = {
42
- 'application/json': {
43
- deserialize: s => JSON.parse( s && s.toString() ),
44
- serialize: o => JSON.stringify( o )
45
- },
46
- 'text/plain': {
47
- deserialize: s => s && s.toString(),
48
- serialize: o => o && o.toString()
49
- },
50
- 'application/octet-stream': defaultHandler,
51
- 'application/x-www-form-urlencoded': {
52
- deserialize: s => s && parseQuery( s.toString(), true ),
53
- serialize: o => Object.keys( o ).map( toFormData ).flat().join( '&' )
54
- },
55
- '*/*': defaultHandler,
56
- }
57
-
58
- let _acceptsDefault = '*/*'
59
-
60
- function getHandler( contentType ) {
61
- if( !contentType ) return contentHandlers[_acceptsDefault]
62
- //content-type is singular https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.17
63
- let handler = contentHandlers[contentType];
64
- if( !handler && contentType.indexOf( ';' ) > -1 ) {
65
- handler = contentHandlers[contentType.split( ';' )[0].trim()]
66
- }
67
- if( handler && typeof handler ) {
68
- if( typeof handler.serialize !== 'function' ) {
69
- throw new Error( `Content handlers must provide a serialize function. ${handler}` )
70
- }
71
- if( typeof handler.deserialize !== 'function' ) {
72
- throw new Error( `Content handlers must provide a deserialize function. ${handler}` )
73
- }
74
- return handler
75
- }
76
- return contentHandlers[_acceptsDefault]
77
- }
78
-
79
- function getContentTypeByExtension( name, staticContentTypes ) {
80
- const extension = name.indexOf( '.' ) > -1 ? name.slice( name.lastIndexOf( '.' ) ).toLowerCase() : 'default'
81
- let contentType = staticContentTypes && staticContentTypes[extension] || null
82
-
83
- return contentType ? contentType : contentTypes[extension]
84
- }
85
-
86
- module.exports = {
87
- serialize( content, contentType ) {
88
- return getHandler( contentType && contentType.toLowerCase() ).serialize( content )
89
- },
90
- deserialize( content, contentType ) {
91
- return getHandler( contentType && contentType.toLowerCase() ).deserialize( content )
92
- },
93
- initContentHandlers( handlers, acceptsDefault ) {
94
- Object.assign( handlers, contentHandlers )
95
- _acceptsDefault = acceptsDefault
96
- },
97
- getContentTypeByExtension
98
- }
package/src/decorator.js DELETED
@@ -1,202 +0,0 @@
1
- const cookie = require( 'cookie' )
2
- const http = require( 'http' )
3
- const { parseQuery, setMultiValueKey } = require( './url' )
4
- const log = require( './log' )
5
- const uuid = require( "uuid" ).v4;
6
- const { Writable } = require( 'stream' )
7
-
8
- const setCookie = ( res ) => function() {
9
- return res.setHeader( 'Set-Cookie', [...( res.getHeader( 'Set-Cookie' ) || [] ), cookie.serialize( ...arguments )] )
10
- }
11
- const addressArrayBufferToString = addrBuf => String.fromCharCode.apply( null, new Int8Array( addrBuf ) )
12
- const excludedMessageProps = {
13
- setTimeout: true,
14
- _read: true,
15
- destroy: true,
16
- _addHeaderLines: true,
17
- _addHeaderLine: true,
18
- _dump: true,
19
- __proto__: true
20
- }
21
-
22
- const normalizeHeader = header => header.toLowerCase()
23
-
24
- const reqProtoProps = () => Object.keys( http.IncomingMessage.prototype ).filter( p => !excludedMessageProps[p] )
25
-
26
- /**
27
- * Provide a minimal set of shims to make most middleware, like passport, work
28
- * @type {{decorateResponse(*=, *=, *): *, decorateRequest(*): *}}
29
- */
30
- module.exports = {
31
- setCookie,
32
- decorateRequest( uwsReq, pathParameters, res, {decodeQueryParameters, decodePathParameters, parseCookie} = {} ) {
33
- //uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
34
- let req = {}
35
- //frameworks like passport like to modify the message prototype
36
- //Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
37
- for( let p of reqProtoProps() ) {
38
- if( !req[p] ) req[p] = http.IncomingMessage.prototype[p]
39
- }
40
- let query = uwsReq.getQuery()
41
- req.path = uwsReq.getUrl()
42
- req.url = `${req.path}${query ? '?' + query : ''}`
43
- req.spliffyUrl = {
44
- path: uwsReq.getUrl(),
45
- query: parseQuery( uwsReq.getQuery(), decodeQueryParameters )
46
- }
47
- req.spliffyUrl.pathParameters = {}
48
- if( pathParameters && pathParameters.length > 0 ) {
49
- for( let i in pathParameters ) {
50
- req.spliffyUrl.pathParameters[pathParameters[i]] = decodePathParameters
51
- ? decodeURIComponent( uwsReq.getParameter( i ) )
52
- : uwsReq.getParameter( i )
53
- }
54
- }
55
- req.params = req.spliffyUrl.pathParameters
56
- req.headers = {}
57
- req.method = uwsReq.getMethod().toUpperCase()
58
- req.remoteAddress = addressArrayBufferToString( res.getRemoteAddressAsText() )
59
- req.proxiedRemoteAddress = addressArrayBufferToString( res.getProxiedRemoteAddressAsText() )
60
- uwsReq.forEach( ( header, value ) => setMultiValueKey( req.headers, normalizeHeader( header ), value ) )
61
- req.get = header => req.headers[normalizeHeader( header )]
62
- if( parseCookie && req.headers.cookie ) {
63
- req.cookies = cookie.parse( req.headers.cookie ) || {}
64
- }
65
- return req
66
- },
67
- decorateResponse( res, req, finalizeResponse, errorTransformer, endError ) {
68
- res.onAborted( () => {
69
- res.ended = true
70
- res.writableEnded = true
71
- res.finalized = true
72
- log.error( `Request to ${req.url} was aborted` )
73
- } )
74
-
75
- res.headers = {}
76
- res.headersSent = false
77
- res.setHeader = ( header, value ) => {
78
- res.headers[normalizeHeader( header )] = value
79
- }
80
- res.removeHeader = header => {
81
- delete res.headers[normalizeHeader( header )]
82
- }
83
- res.flushHeaders = () => {
84
- if( res.headersSent ) return
85
- if( !res.statusCode ) res.statusCode = 200
86
- if( !res.statusMessage ) res.statusMessage = 'OK'
87
- res.headersSent = true
88
- res.writeStatus( `${res.statusCode} ${res.statusMessage}` )
89
- if( typeof res.onFlushHeaders === 'function' ) {
90
- res.onFlushHeaders( res )
91
- }
92
- for( let header of Object.keys( res.headers ) ) {
93
- if( Array.isArray( res.headers[header] ) ) {
94
- for( let multiple of res.headers[header] ) {
95
- res.writeHeader( header, multiple.toString() )
96
- }
97
- } else {
98
- res.writeHeader( header, res.headers[header].toString() )
99
- }
100
- }
101
- }
102
- res.writeHead = ( status, headers ) => {
103
- this.statusCode = status
104
- res.assignHeaders( headers )
105
- }
106
- res.assignHeaders = headers => {
107
- for( let header of Object.keys( headers ) ) {
108
- res.headers[normalizeHeader( header )] = headers[header]
109
- }
110
- }
111
- res.getHeader = header => {
112
- return res.headers[normalizeHeader( header )]
113
- }
114
- res.status = ( code ) => {
115
- this.statusCode = code
116
- return this
117
- }
118
-
119
- function toArrayBuffer( buffer ) {
120
- return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength );
121
- }
122
-
123
- let outStream
124
- res.getWritable = () => {
125
- if( !outStream ) {
126
- res.streaming = true
127
- outStream = new Writable( {
128
- write: (chunk, encoding, cb) => {
129
- try{
130
- res.flushHeaders()
131
- res.write( chunk )
132
- cb()
133
- } catch (e) {
134
- cb(e)
135
- }
136
- }
137
- } )
138
- .on( 'finish', res.end )
139
- .on( 'end', res.end )
140
- .on( 'error', e => {
141
- try {
142
- outStream.destroy()
143
- } finally {
144
- endError( res, e, uuid(), errorTransformer )
145
- }
146
- } )
147
- }
148
- return outStream
149
- }
150
- res.writeArrayBuffer = res.write
151
- res.write = chunk => {
152
- res.streaming = true
153
- res.flushHeaders()
154
- if( chunk instanceof Buffer ) {
155
- res.writeArrayBuffer( toArrayBuffer( chunk ) )
156
- } else if( typeof chunk === 'string' ) {
157
- res.writeArrayBuffer( toArrayBuffer( Buffer.from( chunk, 'utf8' ) ) )
158
- } else {
159
- res.writeArrayBuffer( toArrayBuffer( Buffer.from( JSON.stringify( chunk ), 'utf8' ) ) )
160
- }
161
- }
162
-
163
- const uwsEnd = res.end;
164
- res.ended = false
165
- res.end = body => {
166
- if( res.ended ) {
167
- return
168
- }
169
- res.ended = true
170
- //provide writableEnded like node does, with slightly different behavior
171
- if( !res.writableEnded ) {
172
- res.writableEnded = true
173
- res.flushHeaders()
174
- uwsEnd.call( res, body )
175
- }
176
- if( typeof res.onEnd === 'function' ) {
177
- res.onEnd()
178
- }
179
- }
180
-
181
- res.redirect = function( code, location ) {
182
- if( arguments.length === 1 ) {
183
- location = code
184
- code = 301
185
- }
186
- return finalizeResponse( req, res, {
187
- statusCode: code,
188
- headers: {
189
- 'location': location
190
- }
191
- } )
192
- }
193
- res.send = ( body ) => {
194
- finalizeResponse( req, res, body )
195
- }
196
- res.json = res.send
197
- res.setCookie = setCookie( res )
198
- res.cookie = res.setCookie
199
- return res
200
-
201
- }
202
- }