Created
May 31, 2018 14:08
-
-
Save akc42/5eb4c1558757f717f996e33fdd05ec74 to your computer and use it in GitHub Desktop.
Server basics
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
'use strict'; | |
const path = require('path'); | |
const fs = require('fs'); | |
const enableDestroy = require('server-destroy'); | |
const UAParser = require('ua_parser'); | |
const url = require('url'); | |
const etag = require('etag'); | |
const logger = require('./logger'); | |
const debug = require('debug')('pas:web'); | |
const map = { | |
'.ico': 'image/x-icon', | |
'.html': 'text/html', | |
'.js': 'text/javascript', | |
'.mjs': 'text/javascript', | |
'.json': 'application/json', | |
'.css': 'text/css', | |
'.png': 'image/png', | |
'.jpg': 'image/jpeg', | |
'.wav': 'audio/wav', | |
'.mp3': 'audio/mpeg', | |
'.svg': 'image/svg+xml', | |
'.pdf': 'application/pdf', | |
'.doc': 'application/msword' | |
}; | |
class Web { | |
constructor(manager, Router) { | |
this.server = false; | |
this.manager = manager; //store so we can shut it down when asked | |
const router = Router(); | |
debug('Ask Manager to set up routes'); | |
manager.setRoutes(router, this.transformNeeded); | |
debug('Set up to serve static with possible transform'); | |
//set up our static route | |
router.use('/', this.serveStatic()); | |
this._start(router); | |
} | |
async _start(router) { | |
const self = this; | |
const keyPromise = new Promise((resolve,reject) => { | |
fs.readFile(path.resolve(__dirname, 'assets/key.pem'),(err,data) => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(data); | |
} | |
}); | |
}); | |
const certPromise = new Promise((resolve,reject) => { | |
fs.readFile(path.resolve(__dirname, 'assets/certificate.pem'), (err,data) => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(data); | |
} | |
}); | |
}); | |
const key = await keyPromise; | |
const cert = await certPromise; | |
const certs = { | |
key: key, | |
cert: cert, | |
allowHttp1: true | |
}; | |
debug('Have certificates, about to start server'); | |
this.server = require('http2').createSecureServer(certs,async (req,res) => { | |
req.on('error', err => { | |
if(err.code !== 'ENOENT') { | |
logger('error', `Request ${reqURL} had error of ${err.message}`); | |
} else { | |
debug('File not existing error triggered on request'); | |
} | |
}); | |
res.on('error', err => { | |
if(err.code !== 'ENOENT') { | |
logger('error', `Response error ${err.message} during request ${reqURL}`); | |
} else { | |
debug('File not existing error triggered on response'); | |
} | |
}); | |
req.on('aborted', () => { | |
logger('url', 'request aborted'); | |
}); | |
/* | |
Temporarily disabling this because its happening all the time with respondWithFile | |
res.on('close', () => { | |
logger('url', `Stream closed before Response End with Request of ${reqURL}`); | |
}); | |
*/ | |
let reqURL = url.parse(req.url).pathname; | |
debug('request received for url ', reqURL); | |
self.request = req; //keep a copy for unhandlerd rejections to use | |
function doneWrapper(err) { | |
if (err) { | |
logger('url','Request Error ' + (err.stack || err.toString())); | |
} else { | |
let ip = req.remoteAddress; | |
let hostname = req.headers.host; | |
logger('url','Request for ' + reqURL + ' from ' + hostname + ' not found',ip); | |
} | |
//could not find so send a 404 | |
res.statusCode = 404; | |
res.end(); | |
} | |
router(req, res, err => { | |
if (err) { | |
doneWrapper(err); | |
} else { | |
let reqURL = url.parse(req.url).pathname; | |
debug('parse url to get request pathname = %s', reqURL); | |
/* | |
The following regex includes anything that is likely should have already been handled by | |
a server route or static files. | |
*/ | |
//eslint-disable-next-line no-useless-escape | |
if (/^([^\/]|\/(api|log|\/|.*(\.|\/\/)))/.test(reqURL)) { | |
/* | |
If we get here it means that it is not a route created by the client, and so we should | |
respond with a 404. | |
*/ | |
debug('reqURL is one which should normally have been handled already, so now its a 404'); | |
doneWrapper(); | |
} else { | |
/* | |
This is a url that should not have been handled by known server routes. This means that | |
it is likely that its a client generated url and so we should respond with index.html | |
*/ | |
debug('app route of %s called sending index.html',reqURL); | |
self.serveFile('/index.html', req, res, doneWrapper); | |
} | |
} | |
}); | |
}); | |
this.server.on('timeout', () => { | |
logger('url','socket timeout occured'); | |
}); | |
this.server.on('sessionError', err => { | |
logger('error', 'server received a sessionError ' + err.message); | |
}); | |
this.server.listen(parseInt(process.env.PAS_HTTPS_PORT,10),'0.0.0.0'); | |
enableDestroy(this.server); | |
logger('app', 'Application Web Server Operational Running on Port:' + | |
process.env.PAS_HTTPS_PORT + ' with Node Version: ' + process.version); | |
process.on('unhandledRejection', reason => { | |
if( reason.code === 'ERR_HTTP2_STREAM_CLOSED') { | |
logger('url', 'Stream closed whilst request in progress, full error ' + reason.stack ); | |
return; | |
} | |
if (self.request !== undefined) { | |
logger('error', `Unhandled Rejection: ${reason.stack} | |
Request in Progress At time of Rejection: | |
url:${ url.parse(self.request.url).pathname} | |
body:${typeof self.request.body === 'object' ? JSON.stringify(self.request.body) : self.request.body}`); | |
if (self.done !== undefined) self.done(); //make a reply of we can | |
} else { | |
logger('error', `Unhandled Rejection: ${reason.stack} | |
No request has yet been received`); | |
} | |
//give us some time to make the reply | |
setTimeout(() => { | |
logger('app', 'Shutting down after failure with node version: ' + process.version); | |
process.exit(1); | |
},10000); | |
debug('starting close after unhandled exception'); | |
self.close(); //start (at least) server shutdown | |
}); | |
} | |
async close() { | |
logger('app', 'Starting Shutdown'); | |
if (this.server) { | |
debug('destroying server on close'); | |
this.server.destroy(); | |
} | |
debug('return promise of manager closing'); | |
await this.manager.close(); | |
logger('app', 'Application Server Shut Down with node version: ' + process.version); | |
} | |
serveFile(url, req, res, next) { | |
//helper for static files | |
let forwardError = false; | |
let match = ''; | |
//if we have an if-modified-since header we can maybe not send the fule so create timestamp from it | |
if (req.headers['if-none-match']) { | |
debug('we had a if-none-match header for url ', url); | |
match = req.headers['if-none-match']; | |
} | |
function statCheck(stat, headers) { | |
debug('in stat check for file ', url); | |
const tag = etag(stat); | |
if (match.length > 0) { | |
debug('if-none-since = \'', match, '\' etag = \'', tag, '\''); | |
if (tag === match) { | |
debug('we\'ve not modified the file since last cache so sending 304'); | |
res.statusCode = 304; | |
res.end(); | |
return false; //do not continue with sending the file | |
} | |
} | |
headers['etag'] = tag; | |
debug('set etag header up ', tag); | |
forwardError = true; | |
return true; //tell it to continue | |
} | |
//find out where file is | |
let isES5 = false; | |
if (process.env.PAS_CLIENT_ES5 !== process.env.PAS_CLIENT_ES6) { | |
//only different if production | |
isES5 = this.transformNeeded(req); | |
} | |
let clientPath; | |
let filePath; | |
//might just have 'node_modules' directory in url | |
const nodeIndex = url.indexOf('node_modules'); | |
if (nodeIndex >= 0) { | |
filePath = url.substring(nodeIndex + 13); | |
debug('using resolved modules'); | |
//we need to use the modules that had their imports resolved to relative urls. | |
clientPath = isES5 ? process.env.PAS_RESOLVED_MODULES_ES5 : process.env.PAS_RESOLVED_MODULES_ES6; | |
} else { | |
filePath = url.charAt(0) === '/' ? url.substring(1) : url; | |
clientPath = isES5? process.env.PAS_CLIENT_ES5 : process.env.PAS_CLIENT_ES6; | |
} | |
const filename = path.resolve( | |
__dirname, | |
clientPath, | |
filePath | |
); | |
const ext = path.extname(filename); | |
debug(`send static file ${filename}`); | |
function onError(err) { | |
debug('Respond with file error ', err); | |
if (forwardError || !(err.code === 'ENOENT')) { | |
next(err); | |
} else { | |
//this was probably file not found, in which case we just go to next middleware function. | |
next(); | |
} | |
} | |
res.stream.respondWithFile( | |
filename, | |
{ 'content-type': map[ext] || 'text/plain', | |
'cache-control': 'no-cache' }, | |
{ statCheck, onError }); | |
} | |
transformNeeded(req) { | |
const ua = UAParser.userAgent(req.headers['user-agent']); | |
const browser = ua.browser; | |
const majorVersion = parseInt(ua.browser.version.major,10); | |
const minorVersion = parseInt(ua.browser.version.minor,10); | |
const supportsES2015 = (browser.chrome && majorVersion >= 49) || | |
(browser.android && majorVersion >= 49) || | |
(browser.opera && majorVersion >= 36) || | |
(browser.iphone && majorVersion >= 10) || | |
(browser.safari && majorVersion >= 10) || | |
// Note: The Edge user agent uses the EdgeHTML version, not the main | |
// release version (e.g. EdgeHTML 15 corresponds to Edge 40). See | |
// https://en.wikipedia.org/wiki/Microsoft_Edge#Release_history. | |
// | |
// Versions before 15.15063 may contain a JIT bug affecting ES6 | |
// constructors (see #161). | |
(browser.edge && | |
(majorVersion > 15 || (majorVersion === 15 && minorVersion >= 15063))) || | |
(browser.firefox && majorVersion >= 51); | |
if (supportsES2015) { | |
debug( | |
'ES2015 supported by browser: %s version %d,%d', | |
browser.name, | |
majorVersion, | |
minorVersion | |
); | |
} else { | |
logger('client','ES2015 not supported in browser: ' + browser.name + | |
' version ' + majorVersion + '.' + minorVersion ,req.remoteAddress); | |
} | |
return !supportsES2015; | |
} | |
serveStatic() { | |
const self = this; | |
return (req, res, next) => { | |
if (req.method !== 'GET' && req.method !== 'HEAD') { | |
next(); | |
return; | |
} | |
let reqURL = url.parse(req.url).pathname; | |
if (reqURL === '/') reqURL = '/index.html'; | |
self.serveFile(reqURL,req, res, next); | |
}; | |
} | |
} | |
module.exports = Web; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment