Skip to content

Instantly share code, notes, and snippets.

@FranciscoG
Last active October 18, 2017 22:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FranciscoG/fbe3a415f5a62d6eb5c01292c7a0cbdc to your computer and use it in GitHub Desktop.
Save FranciscoG/fbe3a415f5a62d6eb5c01292c7a0cbdc to your computer and use it in GitHub Desktop.
Work in Progress: A very very simplified Node server in the style of Express. Not to be used in production, meant for experimenting and testing
const SimpleServer = require('./server.js');
var app = new SimpleServer(3004);
// the server will always look for index.html when the route is "/"
// so you do not need to define a GET "/" route unless you want
// do more than just serve up an index.html file
app.get('/test', function(req,res){
// this will return as text/html
res.send('<h1>Page 2, hi</h1>');
});
app.get('/movies', function(req,res){
// this will return as JSON
res.send(['star wars', 'star trek', 'care bears']);
});
app.post('/submit', function(body, req, res){
res.send('<h1>thank you for your response</h1>');
})
app.listen()
.then(function(port){
console.log(`server is listening on localhost:${port}`);
})
.catch((err)=>{
console.log(err)
});
/**
* Simple Node Server
*
* When you need a quick and dirty node server to do some experimenting/testing with
* but you don't want to install a whole Framework and its many many dependencies
* and you want something easy to extend
*
* WARNING: This is not meant to be used in production!
* I created this partly for learning node and partly for prototyping
*
* based on info gathered from:
* https://developer.mozilla.org/en-US/docs/Learn/Server-side/Node_server_without_framework
* https://stackoverflow.com/a/41521743/395414
* and inspired by ExpressJS
*/
const http = require("http");
const fs = require("fs");
const path = require("path");
const qs = require('querystring');
const util = require('util');
var settings;
var defaults = {
port: 3004,
static : process.cwd()
}
// handle events and stuff
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
console.error('whoops! there was an error');
});
var GET_ROUTES = {};
var POST_ROUTES = {};
/**
* Extract the POST body from a request and return it as an object
*
* @param {stream} request
* @returns {Promise}
*/
function parseBody(request){
return new Promise((resolve,reject)=>{
const chunks = [];
request.on('data', chunk => chunks.push(chunk));
request.on('end', () => {
const data = Buffer.concat(chunks);
let body = qs.parse(data.toString());
resolve(body)
})
request.on('error', err => reject(err));
})
}
/**
* Chunk and stream mp4 videos
* TODO: make it work with other streamable media
*
* @param {any} filePath
* @param {string} filePath
* @param {stream} req
* @param {object} res
*/
function streamMP4(filePath, req, res){
// source: https://gist.github.com/paolorossi/1993068
var stat = fs.statSync(filePath);
var total = stat.size;
if (req.headers['range']) {
var range = req.headers.range;
var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : total-1;
var chunksize = (end-start)+1;
console.log('RANGE: ' + start + ' - ' + end + ' = ' + chunksize);
var file = fs.createReadStream(filePath, {start: start, end: end});
res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4' });
file.pipe(res);
} else {
console.log('ALL: ' + total);
res.writeHead(200, { 'Content-Length': total, 'Content-Type': 'video/mp4' });
fs.createReadStream(filePath).pipe(res);
}
}
/**
* Serve up the right static content files requested
*
* @param {string} extname
* @param {string} filePath
* @param {stream} request
* @param {object} response
*/
function handleFiles(extname, filePath, request, response) {
// remove beginning slash
filePath = filePath.replace(/^\//, '');
// resolve using user defined static asset path (or default to cwd)
let finalPath = path.resolve(settings.static, filePath);
if (extname === '.mp4') {
streamMP4(finalPath, request, response);
return;
}
var contentType = "text/html";
var mimeTypes = {
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpg",
".gif": "image/gif",
".wav": "audio/wav",
".woff": "application/font-woff",
".ttf": "application/font-ttf",
".eot": "application/vnd.ms-fontobject",
".otf": "application/font-otf",
".svg": "application/image/svg+xml",
".ico": "image/x-icon"
};
contentType = mimeTypes[extname] || "application/octet-stream";
fs.readFile(finalPath, function(error, content) {
if (error) {
if (error.code == "ENOENT") {
notfound(response)
} else {
myEmitter.emit('error', new Error(error))
internalError(error, response)
}
return;
}
response.writeHead(200, { "Content-Type": contentType });
response.end(content, "utf-8");
});
}
/**
* All unhandled non-resource/content requests get caught here
*
* @param {object} response
*/
function notfound(response){
response.writeHead(404);
response.end("resource not found");
response.end();
}
/**
* handle internal errors here
*
* @param {object} error
* @param {object} response
*/
function internalError(error, response,){
response.writeHead(500);
response.end(`internal error occured: ${error.code}`);
response.end();
}
/**
* Function to help make it easier to respond to requests
* with proper mime types
*
* @param {string} content
*/
function send(content, encoding){
if (typeof content === 'string') {
this.writeHead(200, { 'Content-Type': 'text/html' });
this.end(content, encoding || 'utf-8');
return
}
if (typeof content === 'object') {
this.writeHead(200, { 'Content-Type': 'application/json' });
this.end(JSON.stringify(content), 'utf-8');
return
}
}
function processMatch(url, route){
var obj = {};
for (let i=0; i < route.length; i++){
let key = route[i].replace(/^:/, '');
let val = url[i]
obj[key] = val;
}
return obj;
}
function getRouteMatching(url) {
var func = function(){};
var data = null;
Object.keys(GET_ROUTES).forEach(function(r){
if (url === r) {
func = GET_ROUTES[r];
return
}
let _url = url.split('/');
let _r = r.split('/');
if (_r[0] === _url[0]) {
func = GET_ROUTES[r];
data = processMatch(_url, _r);
}
});
return {
callback : func,
data : data
};
}
/**
* Handle for all requests coming in and out of the server
*
* @param {stream} request
* @param {object} response
* @returns
*/
function requestHandler(request, response) {
console.log(request.method, ":", request.url);
// handle all files by checking if extension exists
var filePath = request.url;
var extname = String(path.extname(filePath)).toLowerCase();
if (extname) {
return handleFiles(extname, filePath, request, response);
}
// special case for home route which works without the index.html
if (filePath == "/") {
handleFiles('.html', '/index.html', request, response);
// return early if a user did not define a "/" route handler
if (!GET_ROUTES[request.url]) { return; }
}
response.send = send;
response.send.bind(response);
if (request.method === "GET") {
// do other GET related things here
let route = getRouteMatching(request.url)
if (route) {
response.data = route.data;
route.callback(request, response);
return
}
}
if (request.method === "POST") {
// do other POST related things here
if (POST_ROUTES[request.url]) {
parseBody(request)
.then((body)=>{
POST_ROUTES[request.url](body, request, response);
})
.catch((error)=>{
myEmitter.emit('error', new Error(error))
internalError(error, response)
})
return;
}
}
// if we are then no routes have been found
notfound(response);
}
/**
* The Server class
*
* @class Server
*/
class Server {
constructor(config) {
settings = Object.assign(defaults, config||{}, {});
// server setup
this.app = http.createServer(requestHandler);
}
/**
* Define a simple GET request route and its callback
*
* @param {string} route the route
* @param {function} cb callback function
* @memberof Server
*/
get(route, cb) {
GET_ROUTES[route] = cb;
}
/**
* Define a POST request route and its callback
*
* @param {string} route
* @param {function} cb callback function
* @memberof Server
*/
post(route, cb) {
POST_ROUTES[route] = cb;
}
/**
* pass-through to the eventEmitter on function
*
* @param {any} eventName
* @param {any} cb
* @memberof Server
*/
on(eventName, cb) {
myEmitter.on(eventName, function(){
cb.apply(this, arguments)
})
}
/**
* Begins the actual listening for requests by the server
*
* @returns {Promise}
* @memberof Server
*/
listen() {
return new Promise((resolve, reject) => {
this.app.listen(settings.port, err => {
if (err) {
reject(err);
}
resolve(settings);
});
});
}
}
module.exports = Server;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment