Skip to content

Instantly share code, notes, and snippets.

@akc42
Created May 31, 2018 14:08
Show Gist options
  • Save akc42/5eb4c1558757f717f996e33fdd05ec74 to your computer and use it in GitHub Desktop.
Save akc42/5eb4c1558757f717f996e33fdd05ec74 to your computer and use it in GitHub Desktop.
Server basics
(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