var http = require("http"); var urllib = require("url"); var fs = require("fs"); var res404 = "404 not found: {{method}} {{pathname}}"; var res403 = "403 forbidden: {{method}} {{pathname}}"; function template(tpml, args) { for (var i in args) { tpml = tpml.split("{{"+i+"}}").join(args[i]); } return tpml; } function resJson(obj) { this.writeHead(200, { "content-type": "application/json" }); this.end(JSON.stringify(obj)); } function methodsMatch(route, req) { if (route.method === "ALL") return true; if (route.method === "GET" && req.method === "HEAD") return true; return route.method === req.method; } class App { constructor(options) { options = options || {}; // 403 and 404 response this.res404 = options.res404 || res404; this.res403 = options.res403 || res403; // Script to be served as /webframe.js this.clientScript = ""; // Create server if (options.server !== undefined) { this.server = options.server; } else { this.server = http.createServer(); } // Listen if (options.listen !== false) { var port = options.port || process.env.PORT || 8080; var host = options.host || process.env.HOSTNAME || "127.0.0.1"; this.server.listen(port, host); this.info("Listening on "+host+":"+port); this.port = port; this.host = host; } this._routeMap = {}; this._routes = []; this._transforms = {}; // Add scripts if (options.client_utils) { this.addScript("client_utils", fs.readFileSync(__dirname+"/client/utils.js", "utf8")); } // Serve /webframe.js this.get("/webframe.js", (req, res) => { res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(this.clientScript); }); // Listen for requests this.server.on("request", (req, res) => { res.json = resJson; var url = urllib.parse(req.url); req.urlobj = url; var route = null; // With HEAD requests, we don't want to write anything if (req.method === "HEAD") { res.write = function() {}; var end = res.end; res.end = function() { end.call(res); } } // If the route is in the hash map, use that var r = this._routeMap[url.pathname]; if (r && (methodsMatch(r, req))) { route = r; // Search through the routes list and look for matching routes } else { for (var i in this._routes) { var r = this._routes[i]; if (!methodsMatch(r, req)) continue; if (r.pattern.test(req.urlobj.pathname)) { route = r; break; } } } // If we still have no route, 404 if (route === null) { res.writeHead(404); res.end(template(this.res404, { method: req.method, pathname: req.urlobj.pathname })); return; } // Run all the middleware stuff if applicable var self = this; if (route.middleware && route.middleware.length > 0) { var cbs = route.middleware.length; function cb() { if (--cbs === 0) route.func(req, res, self); else route.middleware.shift()(req, res, cb); } route.middleware.shift()(req, res, cb); // Just run the function if there's no middleware } else { route.func(req, res, this); } }); } /* * Add route. * Args: * method: "GET", "POST", "PUT", "DELETE", "ALL" * path: path, * middleware: middleware array (optional) * func: function(request, response) */ route(method, path, middleware, func) { if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE" && method !== "ALL") { throw new Error("Invalid method."); } // Middleware is optional if (func === undefined) { func = middleware; middleware = undefined; } // All necessary arguments must exist if (typeof path !== "string") throw new TypeError("Path must be a string."); if (typeof func !== "function") throw new TypeError("Func must be a function."); // All middlewares must exist if (middleware) { for (var i in middleware) { if (typeof middleware[i] !== "function") { this.panic( "Middleware "+i+" for "+method+" "+path+ " is "+(typeof middleware[i])+", expected function."); return; } } } // Add to routes array or route map if (path[0] === "^") { var pat = new RegExp(path); this._routes.push({ method: method, func: func, pattern: pat, middleware: middleware }); } else { this._routeMap[path] = { method: method, func: func, middleware: middleware }; } } /* * Remove a route. * Args: * path: path */ unroute(path) { if (path[0] === "^") { path = "/"+path+"/"; for (var i in this._routes) { var str = this._routes[i].pattern.toString(); if (path === str) delete this._routes[i]; } } else { delete this._routeMap[path]; } } /* * Add a transform. * Args: * ext: What extension to transform * mime: The mime-type the transformed code should be sent as * func: function(path, writeStream) * * writeStream: { * status: status code (should only be modified before write() or end()) * headers: HTTP headers (should only be modified before write() or end()) * write: function(data) - write data * end: function(data) - write data and end response * error: function(err) - end response and log error * } */ transform(ext, mime, func) { if (typeof ext !== "string") throw new Error("Expected ext to be string, got "+(typeof ext)+"."); if (typeof func !== "function") throw new Error("Expected func to be function, got "+(typeof func)+"."); if (this._transforms[ext]) throw new Error("Extension "+ext+" already has a transform."); this._transforms[ext] = { func: func, mime: mime }; } /* * Add code to be served as /webframe.js */ addScript(name, str) { var start = "(function "+name+"() {\n"; var end = "\n})();\n" str = str.trim(); this.clientScript += start + str + end; } /* * Template string */ template(tmpl, args) { return template(tmpl, args); } /* * Utility methods for GET/POST/PUT/DELETE/ALL */ get(path, middleware, func) { this.route("GET", path, middleware, func); } post(path, middleware, func) { this.route("POST", path, middleware, func); } put(path, middleware, func) { this.route("PUT", path, middleware, func); } delete(path, middleware, func) { this.route("DELETE", path, middleware, func); } all(path, middleware, func) { this.route("ALL", path, middleware, func); } /* * Logging */ info(str) { console.log("Info: "+str); } notice(str) { console.log("Notice: "+str); } warning(str) { console.log("Warning: "+str); } panic(str) { console.log("PANIC: "+str); process.exit(1); } } exports.static = require("./js/static"); exports.middleware = require("./js/middleware");; exports.App = App;