@srfnstack/spliffy 0.6.1 → 0.8.2

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,84 @@
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 = true
31
+ }
32
+
33
+ if (!('parseCookie' in config)) {
34
+ config.parseCookie = true
35
+ }
36
+
37
+ config.acceptsDefault = config.acceptsDefault || defaultHeaders.acceptsDefault
38
+ config.defaultContentType = config.defaultContentType || defaultHeaders.defaultContentType
39
+
40
+ config.contentHandlers = initContentHandlers(config.contentHandlers || {})
41
+ config.resolveWithoutExtension = config.resolveWithoutExtension || []
42
+ if (!Array.isArray(config.resolveWithoutExtension)) {
43
+ config.resolveWithoutExtension = [config.resolveWithoutExtension]
44
+ }
45
+
46
+ if (config.resolveWithoutExtension.indexOf('.htm') === -1) {
47
+ config.resolveWithoutExtension.push('.htm')
48
+ }
49
+ if (config.resolveWithoutExtension.indexOf('.html') === -1) {
50
+ config.resolveWithoutExtension.push('.html')
51
+ }
52
+
53
+ if (config.middleware) {
54
+ validateMiddleware(config.middleware)
55
+ }
56
+
57
+ if (!('logAccess' in config)) {
58
+ config.logAccess = true
59
+ }
60
+ if ('logLevel' in config) {
61
+ log.setLogLevel(config.logLevel)
62
+ }
63
+ if (!('ignoreFilesMatching' in config)) {
64
+ config.ignoreFilesMatching = []
65
+ } else if (!Array.isArray(config.ignoreFilesMatching)) {
66
+ config.ignoreFilesMatching = [config.ignoreFilesMatching]
67
+ }
68
+ if (!('allowTestFileRoutes' in config)) {
69
+ config.allowTestFileRoutes = false
70
+ }
71
+ config.port = config.port || 10420
72
+ if (!config.httpPort) {
73
+ config.httpPort = config.port - 1
74
+ }
75
+
76
+ if (config.logger) {
77
+ log.setLogger(config.logger)
78
+ }
79
+
80
+ if ((config.httpsKeyFile && !config.httpsCertFile) || (!config.httpsKeyFile && config.httpsCertFile)) {
81
+ throw new Error('You must provide both httpsKeyFile and httpsCertFile')
82
+ }
83
+ return config
84
+ }
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,82 +0,0 @@
1
- const contentTypes = require( './content-types.js' )
2
- let defaultHandler = {
3
- deserialize: o => {
4
- try {
5
- return JSON.parse( o && o.toString() )
6
- } catch( e ) {
7
- return o
8
- }
9
- },
10
- serialize: o => {
11
- if( typeof o === 'string' ) {
12
- return {
13
- contentType: 'text/plain',
14
- data: o
15
- }
16
- }
17
- if( o instanceof Buffer ) {
18
- return {
19
- contentType: 'application/octet-stream',
20
- data: o
21
- }
22
- }
23
- return {
24
- contentType: 'application/json',
25
- data: JSON.stringify( o )
26
- }
27
- }
28
- };
29
- const contentHandlers = {
30
- 'application/json': {
31
- deserialize: s => JSON.parse( s && s.toString() ),
32
- serialize: s => JSON.stringify( s )
33
- },
34
- 'text/plain': {
35
- deserialize: s => s && s.toString(),
36
- serialize: o => o && o.toString()
37
- },
38
- 'application/octet-stream': defaultHandler,
39
- '*/*': defaultHandler,
40
- }
41
-
42
- let _acceptsDefault = '*/*'
43
-
44
- function getHandler( contentType ) {
45
- if( !contentType ) return contentHandlers[_acceptsDefault]
46
- //content-type should be singular https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.17
47
- let handler = contentHandlers[contentType];
48
- if( !handler && contentType.indexOf( ';' ) > -1 ) {
49
- handler = contentHandlers[contentType.split( ';' )[0].trim()]
50
- }
51
- if( handler && typeof handler ) {
52
- if( typeof handler.serialize !== 'function' ) {
53
- throw new Error( `Content handlers must provide a serialize function. ${handler}` )
54
- }
55
- if( typeof handler.deserialize !== 'function' ) {
56
- throw new Error( `Content handlers must provide a deserialize function. ${handler}` )
57
- }
58
- return handler
59
- }
60
- return contentHandlers[_acceptsDefault]
61
- }
62
-
63
- function getContentTypeByExtension( name, staticContentTypes ) {
64
- const extension = name.indexOf( '.' ) > -1 ? name.slice( name.lastIndexOf( '.' ) ).toLowerCase() : 'default'
65
- let contentType = staticContentTypes && staticContentTypes[extension] || null
66
-
67
- return contentType ? contentType : contentTypes[extension]
68
- }
69
-
70
- module.exports = {
71
- serialize( content, contentType ) {
72
- return getHandler( contentType ).serialize( content )
73
- },
74
- deserialize( content, contentType ) {
75
- return getHandler( contentType ).deserialize( content )
76
- },
77
- initContentHandlers( handlers, acceptsDefault ) {
78
- Object.assign( handlers, contentHandlers )
79
- _acceptsDefault = acceptsDefault
80
- },
81
- getContentTypeByExtension
82
- }
@@ -1,151 +0,0 @@
1
- const cookie = require( 'cookie' )
2
- const serverConfig = require( './serverConfig' )
3
- const http = require( 'http' )
4
- const { parse, setMultiValueKey } = require( './url' )
5
- const setCookie = ( res ) => function() {
6
- return res.setHeader( 'Set-Cookie', [...( res.getHeader( 'Set-Cookie' ) || [] ), cookie.serialize( ...arguments )] )
7
- }
8
- const log = require( './log' )
9
- const addressArrayBufferToString = addrBuf => String.fromCharCode.apply( null, new Int8Array( addrBuf ) )
10
- const excludedMessageProps = {
11
- setTimeout: true,
12
- _read: true,
13
- destroy: true,
14
- _addHeaderLines: true,
15
- _addHeaderLine: true,
16
- _dump: true,
17
- __proto__: true
18
- }
19
-
20
- const normalizeHeader = header => header.toLowerCase()
21
-
22
- /**
23
- * Provide a minimal set of shims to make most middleware, like passport, work
24
- * @type {{decorateResponse(*=, *=, *): *, decorateRequest(*): *}}
25
- */
26
- module.exports = {
27
- setCookie,
28
- decorateRequest( uwsReq, pathParameters, res ) {
29
- //uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
30
- let req = {}
31
- let query = uwsReq.getQuery()
32
- req.path = uwsReq.getUrl()
33
- req.url = `${req.path}${query ? '?' + query : ''}`
34
- req.spliffyUrl = parse( uwsReq.getUrl(), uwsReq.getQuery() )
35
- req.spliffyUrl.pathParameters = {}
36
- if( pathParameters && pathParameters.length > 0 ) {
37
- for( let i in pathParameters ) {
38
- req.spliffyUrl.pathParameters[pathParameters[i]] = serverConfig.current.decodePathParameters
39
- ? decodeURIComponent( uwsReq.getParameter( i ) )
40
- : uwsReq.getParameter( i )
41
- }
42
- }
43
- req.params = req.spliffyUrl.pathParameters
44
- req.headers = {}
45
- req.method = uwsReq.getMethod().toUpperCase()
46
- req.remoteAddress = addressArrayBufferToString( res.getRemoteAddressAsText() )
47
- req.proxiedRemoteAddress = addressArrayBufferToString( res.getProxiedRemoteAddressAsText() ) || undefined
48
- uwsReq.forEach( ( header, value ) => setMultiValueKey( req.headers, header.toLowerCase(), value ) )
49
- req.get = header => req.headers[header.toLowerCase()]
50
- if( serverConfig.current.parseCookie && req.headers.cookie ) {
51
- req.cookies = cookie.parse( req.headers.cookie ) || {}
52
- }
53
- //frameworks like passport like to modify the message prototype...
54
- for( let p of Object.keys( http.IncomingMessage.prototype ) ) {
55
- if( !req[p] && !excludedMessageProps[p] ) req[p] = http.IncomingMessage.prototype[p]
56
- }
57
- return req
58
- },
59
- decorateResponse( res, req, finalizeResponse ) {
60
- res.onAborted( () => {
61
- res.ended = true
62
- res.writableEnded = true
63
- res.finalized = true
64
- log.warn( `Request to ${req.url} from ${res.remoteAddress || ''} ${res.proxiedRemoteAddress || ''} was aborted. Possible client disconnect.` )
65
- } )
66
- const initHeaders = {}
67
-
68
- res.headers = {}
69
- res.headersSent = false
70
- res.setHeader = ( header, value ) => {
71
- res.headers[normalizeHeader( header )] = value
72
- }
73
- res.removeHeader = header => {
74
- delete initHeaders[normalizeHeader( header )]
75
- delete res.headers[normalizeHeader( header )]
76
- }
77
- res.flushHeaders = () => {
78
- if( res.headersSent ) return
79
- res.headersSent = true
80
- // https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
81
- //When headers have been set with response.setHeader(), they will be merged with any headers passed to response.initHeaders(), with the headers passed to response.initHeaders() given precedence.
82
- Object.assign( res.headers, initHeaders )
83
- for( let header of Object.keys( res.headers ) ) {
84
- if( Array.isArray( res.headers[header] ) ) {
85
- for( let multiple of res.headers[header] ) {
86
- res.writeHeader( header, multiple.toString() )
87
- }
88
- } else {
89
- res.writeHeader( header, res.headers[header].toString() )
90
- }
91
- }
92
- }
93
- res.writeHead = ( status, headers ) => {
94
- this.statusCode = status
95
- res.assignHeaders( headers )
96
- }
97
- res.assignHeaders = headers => {
98
- for( let header of Object.keys( headers ) ) {
99
- initHeaders[header.toLowerCase()] = headers[header]
100
- }
101
- }
102
- res.getHeader = header => {
103
- let normalized = normalizeHeader( header );
104
- return initHeaders[normalized] || res.headers[normalized]
105
- }
106
- res.status = ( code ) => {
107
- this.statusCode = code
108
- return this
109
- }
110
-
111
- const ogEnd = res.end
112
- res.ended = false
113
- res.end = body => {
114
- if( res.ended ) {
115
- return
116
- }
117
- res.ended = true
118
- //has to be separate to allow streaming
119
- if( !res.writableEnded ) {
120
- res.writableEnded = true
121
- res.writeStatus( `${res.statusCode} ${res.statusMessage}` )
122
- res.flushHeaders()
123
- ogEnd.call( res, body )
124
- }
125
- if( typeof res.onEnd === 'function' ) {
126
- res.onEnd()
127
- }
128
- }
129
-
130
- res.redirect = function( code, location ) {
131
- if( arguments.length === 1 ) {
132
- location = code
133
- code = 301
134
- }
135
- return finalizeResponse( req, res, {
136
- statusCode: code,
137
- headers: {
138
- 'location': location
139
- }
140
- } )
141
- }
142
- res.send = ( body ) => {
143
- finalizeResponse( req, res, body )
144
- }
145
- res.json = res.send
146
- res.setCookie = setCookie( res )
147
- res.cookie = res.setCookie
148
- return res
149
-
150
- }
151
- }