Browse Source

initial commit

master
mortie 7 years ago
commit
c89d92afad
6 changed files with 326 additions and 0 deletions
  1. 7
    0
      README.md
  2. 34
    0
      client/utils.js
  3. 184
    0
      index.js
  4. 37
    0
      js/middleware.js
  5. 53
    0
      js/static.js
  6. 11
    0
      package.json

+ 7
- 0
README.md View File

@@ -0,0 +1,7 @@
# Webby

Web server.

## Usage



+ 34
- 0
client/utils.js View File

@@ -0,0 +1,34 @@
function request(method, path, payload, cb) {
var xhr = new XMLHttpRequest();
xhr.open(method, path);
xhr.overrideMimeType("text/plain; charset=x-user-defined");
xhr.send(payload);

xhr.addEventListener("load", function() {
if (cb)
cb(null, xhr.responseText);
});
xhr.addEventListener("abort", function() {
if (cb)
cb(new Error("Aborted"));
});
xhr.addEventListener("error", function() {
if (cb)
cb(new Error("Error"));
});

return xhr;
}

function get(path, cb) {
return request("GET", path, null, cb);
}

function post(path, payload, cb) {
return request("POST", path, payload, cb);
}

window.$$ = document.querySelector.bind(document);
window.request = request;
window.get = get;
window.post = post;

+ 184
- 0
index.js View File

@@ -0,0 +1,184 @@
var http = require("http");
var urllib = require("url");
var fs = require("fs");

var res404 = "404 not found: {{pathname}}";
var res403 = "403 forbidden: {{pathname}}";

function template(tpml, args) {
for (var i in args) {
tpml = tpml.split("{{"+i+"}}").join(args[i]);
}
return tpml;
}

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 /webby.js
this.clientScript = "";

// Create server
if (options.server !== undefined) {
this.server = options.server;
} else {
this.server = http.createServer();

var port = options.port || process.env.PORT | 8080;
var host = options.host || "127.0.0.1";

this.server.listen(port, host);
this.info("Listening on "+host+":"+port);
}

this._routeMap = {};
this._routes = [];

// Add scripts
if (options.client_utils) {
this.addScript("client_utils",
fs.readFileSync(__dirname+"/client/utils.js", "utf8"));
}

// Serve /webby.js
this.get("/webby.js", (req, res) => {
res.writeHead(200, {
"Content-Type": "application/javascript"
});
res.end(this.clientScript);
});

// Listen for requests
this.server.on("request", (req, res) => {
var url = urllib.parse(req.url);
req.urlobj = url;

var route = null;

// If the route is in the hash map, use that
var r = this._routeMap[url.pathname];
if (r && (r.method === "ALL" || r.method === req.method)) {
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 (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,
{ pathname: req.urlobj.pathname }));
return;
}

// Run all the before stuff if applicable
var self = this;
if (route.before) {
var cbs = route.before.length;
function cb() {
if (--cbs === 0)
route.func(req, res, self);
}

for (var i in route.before) {
route.before[i](req, res, cb);
}

// Just run the function if there's no before
} else {
route.func(req, res, this);
}
});
}

/*
* Add route.
* Args:
* method: "GET", "POST", "PUT", "DELETE", "ALL"
* path: path,
* func: function
*/
route(method, path, before, func) {
if (method !== "GET" && method !== "POST" && method !== "PUT" &&
method !== "DELETE" && method !== "ALL") {
throw new Error("Invalid method.");
}

// Before is optional
if (func === undefined) {
func = before;
before = 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.");

// Add to routes array or route map
if (path[0] === "^") {
var pat = new RegExp(path);
this._routes.push({
method: method,
func: func,
pattern: pat,
before: before
});
} else {
this._routeMap[path] = {
method: method,
func: func,
before: before
};
}
}

/*
* Add code to be served as /webby.js
*/
addScript(name, str) {
var start = "(function "+name+"() {\n";
var end = "\n})();\n"

str = str.trim();
this.clientScript += start + str + end;
}

get(path, before, func) { this.route("GET", path, before, func); }
post(path, before, func) { this.route("POST", path, before, func); }
put(path, before, func) { this.route("PUT", path, before, func); }
delete(path, before, func) { this.route("DELETE", path, before, func); }
all(path, before, func) { this.route("ALL", path, before, func); }

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;

+ 37
- 0
js/middleware.js View File

@@ -0,0 +1,37 @@

/*
* Adds req.cookie
*/
function cookies(req, res, cb) {
req.cookies = {};

if (!req.headers.cookie)
return cb();

req.headers.cookie.split(/;\s+/).forEach(cookie => {
var parts = cookie.split("=");
req.cookies[parts.shift()] = parts.join("=");
});

cb();
}

/*
* Adds req.params from URL parameters
*/
function params(req, res, cb) {
req.params = {};

if (!req.urlobj.query)
return cb();

req.urlobj.query.split("&").forEach(param => {
var parts = param.split("=");
req.params[parts.shift()] = parts.join("=") || true;
});

cb();
}

exports.cookies = cookies;
exports.params = params;

+ 53
- 0
js/static.js View File

@@ -0,0 +1,53 @@
var fs = require("fs");
var pathlib = require("path");

function mimetype(path) {
}

module.exports = function(root, before) {

return function(req, res, app) {
var pn = req.urlobj.pathname;

// Send a file
function send(path) {
var rs = fs.createReadStream(path);
rs.on("error", err => {
app.notice(err);
res.end(template(app.res404, { pathname: pathname }));
});
rs.on("data", d => res.write(d));
rs.on("end", () => res.end());
}

res.responded = true;

// Prevent leaking information
if (pn.indexOf("../") !== -1 || pn.indexOf("/..") !== -1 || pn === "..") {
res.writeHead(403);
res.end(template(app.res403, { pathname: pn }));
return;
}

// Join the web root with the request's path name
var path = pathlib.join(root, pn.replace(before, ""));

fs.stat(path, (err, stat) => {

// If there's an error stat'ing, just error
if (err) {
app.notice(err);
res.writeHead(404);
res.end(template(app.res404, { pathname: pn }));
return;
}

// If it's a directory, we want the index.html file
if (stat.isDirectory())
path = pathlib.join(path, "index.html");

// Send the file
send(path, pn, res, app);
});
}
}

+ 11
- 0
package.json View File

@@ -0,0 +1,11 @@
{
"name": "webby",
"version": "1.0.0",
"description": "Web server.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)",
"license": "ISC"
}

Loading…
Cancel
Save