UNPKG

node-http-server

Version:

A very simple and fast http server for node, bash, and spawnable from C, Python etc. It is lightweight and great for embedded solutions as well as everyday development or public facing apps.

524 lines (447 loc) 13.3 kB
'use strict'; const http = require('http'), https = require('https'), url = require('url'), path = require('path'), fs = require('fs'), querystring=require('querystring'), Config = require(`${__dirname}/Config.js`); const passedArgs = process.argv.splice(2), argCount = passedArgs.length, args = {}; for(let i=0; i<argCount; i++){ const data=passedArgs[i].split('='); args[data[0]]=data[1]; } if(args.launch=='now'){ const server=new Server; server.deploy(); } function deploy(userConfig, readyCallback){ Object.defineProperty( this, 'server', { value:http.createServer( requestRecieved.bind(this) ), writable:false, enumerable:true } ); this.config=new this.Config(userConfig); if(this.config.https && this.config.https.privateKey && this.config.https.certificate){ if(!this.config.https.port){ this.config.https.port=443; } this.config.httpsOptions = { key: fs.readFileSync(this.config.https.privateKey), cert: fs.readFileSync(this.config.https.certificate) }; if(this.config.https.ca){ this.config.httpsOptions.ca=fs.readFileSync(this.config.https.ca); } Object.defineProperty( this, 'secureServer', { value:https.createServer( this.config.httpsOptions, requestRecieved.bind(this) ), writable:false, enumerable:true } ); }else{ this.config.https={ only:false }; } this.config.logID=`### ${this.config.domain} server`; if(this.config.verbose){ console.log( `${this.config.logID} configured with ###\n\n`,this.config); } this.server.timeout=this.config.server.timeout; if(!this.config.https.only){ this.server.listen( this.config.port, function() { if(this.config.verbose){ console.log(`${this.config.logID} listening on port ${this.config.port} ###\n\n`); } if (readyCallback){ //passes the current Server class instance for refrence. //this allows multiple servers to use the same ready callback if desired readyCallback(this); } }.bind(this) ); } if(this.config.httpsOptions){ this.secureServer.listen( this.config.https.port, function() { if(this.config.verbose){ console.log(`HTTPS: ${this.config.logID} listening on port ${this.config.https.port} ###\n\n`); } if (readyCallback){ //passes the current Server class instance for refrence. //this allows multiple servers to use the same ready callback if desired readyCallback(this); } }.bind(this) ); } } function setHeaders(response,headers){ const keys=Object.keys(headers); for(const i in keys){ response.setHeader( keys[i], headers[keys[i]] ); } } function serveFile(filename,exists,request,response) { if(!exists) { if(this.config.verbose){ console.log(`${this.config.logID} 404 ###\n\n`); } if(!response){ return false; } response.statusCode=404; setHeaders(response, this.config.errors.headers); this.serve( request, response, this.config.errors['404'] ); return; } const contentType = path.extname(filename).slice(1); //Only serve specified file types if(!this.config.contentType[contentType]){ if(this.config.verbose){ console.log(`${this.config.logID} 415 ###\n\n`); } response.statusCode=415; setHeaders(response, this.config.errors.headers); this.serve( request, response, this.config.errors['415'] ); return; } //default if ( fs.statSync(filename).isDirectory() ){ filename+=`/${this.config.server.index}`; } //Do not allow access to restricted file types if ( this.config.restrictedType[contentType] ){ if(this.config.verbose){ console.log(`${this.config.logID} 403 ###\n\n`); } response.statusCode=403; setHeaders(response, this.config.errors.headers); this.serve( request, response, this.config.errors['403'] ); return; } fs.readFile( filename, 'binary', function(err, file) { if(err) { if(this.config.verbose){ console.log(`${this.config.logID} 500 ###\n\n`,err,'\n\n'); } response.statusCode=500; setHeaders(response, this.config.errors.headers); this.serve( request, response, this.config.errors['500'].replace(/\{\{err\}\}/g,err) ); return; } response.setHeader( 'Content-Type', this.config.contentType[contentType] ); if(this.config.server.noCache){ response.setHeader( 'Cache-Control', 'no-cache, no-store, must-revalidate' ); } response.statusCode=200; this.serve( request, response, file, 'binary' ); if(this.config.verbose){ console.log(`${this.config.logID} 200 ###\n\n`); } return; }.bind(this) ); } function serve(request,response,body,encoding){ //defaults to 200 if(!response.statusCode){ response.statusCode=200; } //defaults to text/plain if(!response.getHeader('Content-Type')){ response.setHeader( 'Content-Type', 'text/plain' ); if(this.config.verbose){ console.log(`${this.config.logID} response content-type header not specified ###\n\nContent-Type set to: text/plain\n\n`); } } //defaults to utf8 if(!encoding){ encoding='utf8'; if(this.config.verbose){ console.log(`${this.config.logID} encoding not specified ###\nencoding set to:\n`,encoding,'\n\n'); } } const refBody=new RefString; const refEncoding=new RefString; refBody.value=body; refEncoding.value=encoding; //return any value to force delayed serving if( this.beforeServe( request, response, refBody, refEncoding, completeServing.bind(this) ) ){ return; }; completeServing.bind(this)(request,response,refBody,encoding); return; } function completeServing(request,response,refBody,refEncoding){ if(!(refBody instanceof RefString)){ refBody=new RefString(refBody); } if(!(refEncoding instanceof RefString)){ refEncoding=new RefString(refEncoding||'binary'); } if(response.finished){ this.afterServe(request); return; } response.end( refBody.value, refEncoding.value, this.afterServe.bind(this,request) ); } function RefString(value){ Object.defineProperties( this, { value:{ value:value||'', enumerable:true, writable:true } } ); } function requestRecieved(request,response){ if(this.config.log){ const logData={ method : request.method, url : request.url, headers : request.headers }; this.config.logFunction( logData ); } let uri = url.parse(request.url); uri.protocol='http'; uri.host=uri.hostname=request.headers.host; uri.port=80; uri.query=querystring.parse(uri.query); if(request.connection.encrypted){ uri.protocol='https'; uri.port=443; } ( function(){ if(!uri.host){ return; } const host=uri.host.split(':'); if(!host[1]){ return; } uri.host=uri.hostname=host[0]; uri.port=host[1]; } )(); for(let key in uri){ if(uri[key]!==null){ continue; } uri[key]=''; } request.uri=uri; //return any value to force delayed serving if( this.onRawRequest( request, response, completeServing.bind(this) ) ){ return; }; uri=uri.pathname; if (uri=='/'){ uri=`/${this.config.server.index}`; } let hostname= []; if (request.headers.host !== undefined){ hostname = request.headers.host.split(':'); } let root = this.config.root; if(this.config.verbose){ console.log(`${this.config.logID} REQUEST ###\n\n`, request.headers,'\n', uri,'\n\n', hostname,'\n\n' ); } if(this.config.domain!='0.0.0.0' && hostname.length > 0 && hostname[0]!=this.config.domain){ if(!this.config.domains[hostname[0]]){ if(this.config.verbose){ console.log(`${this.config.logID} INVALID HOST ###\n\n`); } this.serveFile(hostname[0],false,response); return; } root=this.config.domains[hostname[0]]; } if(this.config.verbose){ console.log(`${this.config.logID} USING ROOT : ${root}###\n\n`); } if(uri.slice(-1)=='/'){ uri+=this.config.server.index; } request.url=uri; request.serverRoot=root; //return any value to force delayed serving if( this.onRequest( request, response, completeServing.bind(this) ) ){ return; }; const filename = path.join( request.serverRoot, request.url ); fs.exists( filename, function fileExists(exists){ this.serveFile(filename,exists,request,response); }.bind(this) ); } /********************* * SERVER CLASS *********************/ class Server{ constructor(){ Object.defineProperties( this, { deploy:{ value:deploy, writable:false, enumerable:true }, serveFile:{ value:serveFile, writable:false, enumerable:false }, //executed before any processing of request. If returns true response serving will be delayed. onRawRequest:{ value:function(request,response,serve){}, writable:true, enumerable:true }, //executed just after request recieved allowing user to modify if needed. If returns true response serving will be delayed. onRequest:{ value:function(request,response,serve){}, writable:true, enumerable:true }, //executed just before response sent allowing user to modify if needed. If returns true response serving will be delayed. beforeServe:{ value:function beforeServe(request,response,body,encoding,serve){}, writable:true, enumerable:true }, //executed after each full response completely sent afterServe:{ value:function afterServe(request){}, writable:true, enumerable:true }, serve:{ value:serve, writable:false, enumerable:false }, //kept for backwards compatibility configTemplate :{ value:function configTemplate(config){ return new Config(config); }, writable:false, //not visible because this is just for backwards compatibility enumerable:false }, Config:{ value:Config, writable:false, enumerable:true }, Server:{ value:Server, writable:false, enumerable:true } } ); } } module.exports=new Server;