Last active
October 18, 2017 22:12
-
-
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
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
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) | |
}); |
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
/** | |
* 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