mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-11-21 09:44:51 +00:00
Refactor server handling
* Introduce a new named parameter scheme for commands * Move the SimpleServer class into it's own module * Deprecate the --server command because of the unwieldy syntax * Add a new --listen command using the new syntax For example: tiddlywiki mywiki --listen host:0.0.0.0 port:8090
This commit is contained in:
@@ -94,6 +94,13 @@ Commander.prototype.executeNextCommand = function() {
|
||||
if(this.verbose) {
|
||||
this.streams.output.write("Executing command: " + commandName + " " + params.join(" ") + "\n");
|
||||
}
|
||||
// Parse named parameters if required
|
||||
if(command.info.namedParameters) {
|
||||
params = this.extractNamedParameters(params,command.info.namedParameters);
|
||||
if(typeof params === "string") {
|
||||
return this.callback(params);
|
||||
}
|
||||
}
|
||||
if(command.info.synchronous) {
|
||||
// Synchronous command
|
||||
c = new command.Command(params,this);
|
||||
@@ -122,6 +129,31 @@ Commander.prototype.executeNextCommand = function() {
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Given an array of parameter strings `params` in name:value format, and an array of mandatory parameter names in `mandatoryParameters`, returns a hashmap of values or a string if error
|
||||
*/
|
||||
Commander.prototype.extractNamedParameters = function(params,mandatoryParameters) {
|
||||
var errors = [],
|
||||
paramsByName = Object.create(null);
|
||||
$tw.utils.each(params,function(param) {
|
||||
var index = param.indexOf(":");
|
||||
if(index < 1) {
|
||||
errors.push("malformed named parameter: '" + param + "'");
|
||||
}
|
||||
paramsByName[param.slice(0,index)] = param.slice(index+1);
|
||||
});
|
||||
$tw.utils.each(mandatoryParameters,function(mandatoryParameter) {
|
||||
if(!$tw.utils.hop(paramsByName,mandatoryParameter)) {
|
||||
errors.push("missing mandatory parameter: '" + mandatoryParameter + "'");
|
||||
}
|
||||
});
|
||||
if(errors.length > 0) {
|
||||
return errors.join(" and\n");
|
||||
} else {
|
||||
return paramsByName;
|
||||
}
|
||||
};
|
||||
|
||||
Commander.initCommands = function(moduleType) {
|
||||
moduleType = moduleType || "command";
|
||||
$tw.commands = {};
|
||||
|
||||
54
core/modules/commands/listen.js
Normal file
54
core/modules/commands/listen.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/*\
|
||||
title: $:/core/modules/commands/listen.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Listen for HTTP requests and serve tiddlers
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var Server = require("$:/core/modules/server.js").Server;
|
||||
|
||||
exports.info = {
|
||||
name: "listen",
|
||||
synchronous: true,
|
||||
namedParameters: [] // Use named parameters, but none of them are mandatory
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
var self = this;
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
this.knownParameters = ["port","host","rootTiddler","renderType","serveType","username","password","pathprefix","debugLevel"];
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
if(!$tw.boot.wikiTiddlersPath) {
|
||||
$tw.utils.warning("Warning: Wiki folder '" + $tw.boot.wikiPath + "' does not exist or is missing a tiddlywiki.info file");
|
||||
}
|
||||
// Set up server
|
||||
var variables = Object.create(null);
|
||||
$tw.utils.each(this.knownParameters,function(name) {
|
||||
if($tw.utils.hop(self.params,name)) {
|
||||
variables[name] = self.params[name];
|
||||
}
|
||||
});
|
||||
this.server = new Server({
|
||||
wiki: this.commander.wiki,
|
||||
variables: variables
|
||||
});
|
||||
var nodeServer = this.server.listen();
|
||||
$tw.hooks.invokeHook("th-server-command-post-start",this.server,nodeServer);
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
||||
@@ -3,7 +3,7 @@ title: $:/core/modules/commands/server.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Serve tiddlers over http
|
||||
Deprecated legacy command for serving tiddlers
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
@@ -12,194 +12,41 @@ Serve tiddlers over http
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
if($tw.node) {
|
||||
var util = require("util"),
|
||||
fs = require("fs"),
|
||||
url = require("url"),
|
||||
path = require("path"),
|
||||
http = require("http");
|
||||
}
|
||||
var Server = require("$:/core/modules/server.js").Server;
|
||||
|
||||
exports.info = {
|
||||
name: "server",
|
||||
synchronous: true
|
||||
};
|
||||
|
||||
/*
|
||||
A simple HTTP server with regexp-based routes
|
||||
*/
|
||||
function SimpleServer(options) {
|
||||
this.routes = options.routes || [];
|
||||
this.wiki = options.wiki;
|
||||
this.variables = options.variables || {};
|
||||
}
|
||||
|
||||
SimpleServer.prototype.set = function(obj) {
|
||||
var self = this;
|
||||
$tw.utils.each(obj,function(value,name) {
|
||||
self.variables[name] = value;
|
||||
});
|
||||
};
|
||||
|
||||
SimpleServer.prototype.get = function(name) {
|
||||
return this.variables[name];
|
||||
};
|
||||
|
||||
SimpleServer.prototype.addRoute = function(route) {
|
||||
this.routes.push(route);
|
||||
};
|
||||
|
||||
SimpleServer.prototype.findMatchingRoute = function(request,state) {
|
||||
var pathprefix = this.get("pathprefix") || "";
|
||||
for(var t=0; t<this.routes.length; t++) {
|
||||
var potentialRoute = this.routes[t],
|
||||
pathRegExp = potentialRoute.path,
|
||||
pathname = state.urlInfo.pathname,
|
||||
match;
|
||||
if(pathprefix) {
|
||||
if(pathname.substr(0,pathprefix.length) === pathprefix) {
|
||||
pathname = pathname.substr(pathprefix.length) || "/";
|
||||
match = potentialRoute.path.exec(pathname);
|
||||
} else {
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
match = potentialRoute.path.exec(pathname);
|
||||
}
|
||||
if(match && request.method === potentialRoute.method) {
|
||||
state.params = [];
|
||||
for(var p=1; p<match.length; p++) {
|
||||
state.params.push(match[p]);
|
||||
}
|
||||
return potentialRoute;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
SimpleServer.prototype.checkCredentials = function(request,incomingUsername,incomingPassword) {
|
||||
var header = request.headers.authorization || "",
|
||||
token = header.split(/\s+/).pop() || "",
|
||||
auth = $tw.utils.base64Decode(token),
|
||||
parts = auth.split(/:/),
|
||||
username = parts[0],
|
||||
password = parts[1];
|
||||
if(incomingUsername === username && incomingPassword === password) {
|
||||
return "ALLOWED";
|
||||
} else {
|
||||
return "DENIED";
|
||||
}
|
||||
};
|
||||
|
||||
SimpleServer.prototype.requestHandler = function(request,response) {
|
||||
// Compose the state object
|
||||
var self = this;
|
||||
var state = {};
|
||||
state.wiki = self.wiki;
|
||||
state.server = self;
|
||||
state.urlInfo = url.parse(request.url);
|
||||
// Optionally output debug info
|
||||
if(self.get("debugLevel") !== "none") {
|
||||
console.log("Request path:",JSON.stringify(state.urlInfo));
|
||||
console.log("Request headers:",JSON.stringify(request.headers));
|
||||
}
|
||||
// Find the route that matches this path
|
||||
var route = self.findMatchingRoute(request,state);
|
||||
// Check for the username and password if we've got one
|
||||
var username = self.get("username"),
|
||||
password = self.get("password");
|
||||
if(username && password) {
|
||||
// Check they match
|
||||
if(self.checkCredentials(request,username,password) !== "ALLOWED") {
|
||||
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
|
||||
response.writeHead(401,"Authentication required",{
|
||||
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + servername + '"'
|
||||
});
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Return a 404 if we didn't find a route
|
||||
if(!route) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
// Set the encoding for the incoming request
|
||||
// TODO: Presumably this would need tweaking if we supported PUTting binary tiddlers
|
||||
request.setEncoding("utf8");
|
||||
// Dispatch the appropriate method
|
||||
switch(request.method) {
|
||||
case "GET": // Intentional fall-through
|
||||
case "DELETE":
|
||||
route.handler(request,response,state);
|
||||
break;
|
||||
case "PUT":
|
||||
var data = "";
|
||||
request.on("data",function(chunk) {
|
||||
data += chunk.toString();
|
||||
});
|
||||
request.on("end",function() {
|
||||
state.data = data;
|
||||
route.handler(request,response,state);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SimpleServer.prototype.listen = function(port,host) {
|
||||
return http.createServer(this.requestHandler.bind(this)).listen(port,host);
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
var self = this;
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
// Set up server
|
||||
this.server = new SimpleServer({
|
||||
wiki: this.commander.wiki
|
||||
});
|
||||
// Add route handlers
|
||||
$tw.modules.forEachModuleOfType("serverroute", function(title,routeDefinition) {
|
||||
// console.log("Loading server route " + title);
|
||||
self.server.addRoute(routeDefinition);
|
||||
});
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
if(!$tw.boot.wikiTiddlersPath) {
|
||||
$tw.utils.warning("Warning: Wiki folder '" + $tw.boot.wikiPath + "' does not exist or is missing a tiddlywiki.info file");
|
||||
}
|
||||
var port = this.params[0] || "8080",
|
||||
rootTiddler = this.params[1] || "$:/core/save/all",
|
||||
renderType = this.params[2] || "text/plain",
|
||||
serveType = this.params[3] || "text/html",
|
||||
username = this.params[4],
|
||||
password = this.params[5],
|
||||
host = this.params[6] || "127.0.0.1",
|
||||
pathprefix = this.params[7],
|
||||
debugLevel = this.params[8] || "none";
|
||||
if(parseInt(port,10).toString() !== port) {
|
||||
port = process.env[port] || 8080;
|
||||
}
|
||||
this.server.set({
|
||||
rootTiddler: rootTiddler,
|
||||
renderType: renderType,
|
||||
serveType: serveType,
|
||||
username: username,
|
||||
password: password,
|
||||
pathprefix: pathprefix,
|
||||
debugLevel: debugLevel
|
||||
// Set up server
|
||||
this.server = new Server({
|
||||
wiki: this.commander.wiki,
|
||||
variables: {
|
||||
port: this.params[0],
|
||||
host: this.params[6],
|
||||
rootTiddler: this.params[1],
|
||||
renderType: this.params[2],
|
||||
serveType: this.params[3],
|
||||
username: this.params[4],
|
||||
password: this.params[5],
|
||||
pathprefix: this.params[7],
|
||||
debugLevel: this.params[8]
|
||||
}
|
||||
});
|
||||
var nodeServer = this.server.listen(port,host);
|
||||
$tw.utils.log("Serving on " + host + ":" + port,"brown/orange");
|
||||
$tw.utils.log("(press ctrl-C to exit)","red");
|
||||
// Warn if required plugins are missing
|
||||
if(!$tw.wiki.getTiddler("$:/plugins/tiddlywiki/tiddlyweb") || !$tw.wiki.getTiddler("$:/plugins/tiddlywiki/filesystem")) {
|
||||
$tw.utils.warning("Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file");
|
||||
}
|
||||
$tw.hooks.invokeHook('th-server-command-post-start', this.server, nodeServer);
|
||||
var nodeServer = this.server.listen();
|
||||
$tw.hooks.invokeHook("th-server-command-post-start",this.server,nodeServer);
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
187
core/modules/server.js
Normal file
187
core/modules/server.js
Normal file
@@ -0,0 +1,187 @@
|
||||
/*\
|
||||
title: $:/core/modules/server.js
|
||||
type: application/javascript
|
||||
module-type: library
|
||||
|
||||
Serve tiddlers over http
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
if($tw.node) {
|
||||
var util = require("util"),
|
||||
fs = require("fs"),
|
||||
url = require("url"),
|
||||
path = require("path"),
|
||||
http = require("http");
|
||||
}
|
||||
|
||||
/*
|
||||
A simple HTTP server with regexp-based routes
|
||||
options: variables - optional hashmap of variables to set (a misnomer - they are really constant parameters)
|
||||
routes - optional array of routes to use
|
||||
wiki - reference to wiki object
|
||||
*/
|
||||
function Server(options) {
|
||||
var self = this;
|
||||
this.routes = options.routes || [];
|
||||
this.wiki = options.wiki;
|
||||
this.variables = $tw.utils.extend({},this.defaultVariables,options.variables);
|
||||
// Add route handlers
|
||||
$tw.modules.forEachModuleOfType("serverroute", function(title,routeDefinition) {
|
||||
// console.log("Loading server route " + title);
|
||||
self.addRoute(routeDefinition);
|
||||
});
|
||||
}
|
||||
|
||||
Server.prototype.defaultVariables = {
|
||||
port: "8080",
|
||||
host: "127.0.0.1",
|
||||
rootTiddler: "$:/core/save/all",
|
||||
renderType: "text/plain",
|
||||
serveType: "text/html",
|
||||
debugLevel: "none"
|
||||
};
|
||||
|
||||
Server.prototype.set = function(obj) {
|
||||
var self = this;
|
||||
$tw.utils.each(obj,function(value,name) {
|
||||
self.variables[name] = value;
|
||||
});
|
||||
};
|
||||
|
||||
Server.prototype.get = function(name) {
|
||||
return this.variables[name];
|
||||
};
|
||||
|
||||
Server.prototype.addRoute = function(route) {
|
||||
this.routes.push(route);
|
||||
};
|
||||
|
||||
Server.prototype.findMatchingRoute = function(request,state) {
|
||||
var pathprefix = this.get("pathprefix") || "";
|
||||
for(var t=0; t<this.routes.length; t++) {
|
||||
var potentialRoute = this.routes[t],
|
||||
pathRegExp = potentialRoute.path,
|
||||
pathname = state.urlInfo.pathname,
|
||||
match;
|
||||
if(pathprefix) {
|
||||
if(pathname.substr(0,pathprefix.length) === pathprefix) {
|
||||
pathname = pathname.substr(pathprefix.length) || "/";
|
||||
match = potentialRoute.path.exec(pathname);
|
||||
} else {
|
||||
match = false;
|
||||
}
|
||||
} else {
|
||||
match = potentialRoute.path.exec(pathname);
|
||||
}
|
||||
if(match && request.method === potentialRoute.method) {
|
||||
state.params = [];
|
||||
for(var p=1; p<match.length; p++) {
|
||||
state.params.push(match[p]);
|
||||
}
|
||||
return potentialRoute;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Server.prototype.checkCredentials = function(request,incomingUsername,incomingPassword) {
|
||||
var header = request.headers.authorization || "",
|
||||
token = header.split(/\s+/).pop() || "",
|
||||
auth = $tw.utils.base64Decode(token),
|
||||
parts = auth.split(/:/),
|
||||
username = parts[0],
|
||||
password = parts[1];
|
||||
if(incomingUsername === username && incomingPassword === password) {
|
||||
return "ALLOWED";
|
||||
} else {
|
||||
return "DENIED";
|
||||
}
|
||||
};
|
||||
|
||||
Server.prototype.requestHandler = function(request,response) {
|
||||
// Compose the state object
|
||||
var self = this;
|
||||
var state = {};
|
||||
state.wiki = self.wiki;
|
||||
state.server = self;
|
||||
state.urlInfo = url.parse(request.url);
|
||||
// Optionally output debug info
|
||||
if(self.get("debugLevel") !== "none") {
|
||||
console.log("Request path:",JSON.stringify(state.urlInfo));
|
||||
console.log("Request headers:",JSON.stringify(request.headers));
|
||||
}
|
||||
// Find the route that matches this path
|
||||
var route = self.findMatchingRoute(request,state);
|
||||
// Check for the username and password if we've got one
|
||||
var username = self.get("username"),
|
||||
password = self.get("password");
|
||||
if(username && password) {
|
||||
// Check they match
|
||||
if(self.checkCredentials(request,username,password) !== "ALLOWED") {
|
||||
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
|
||||
response.writeHead(401,"Authentication required",{
|
||||
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + servername + '"'
|
||||
});
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Return a 404 if we didn't find a route
|
||||
if(!route) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
// Set the encoding for the incoming request
|
||||
// TODO: Presumably this would need tweaking if we supported PUTting binary tiddlers
|
||||
request.setEncoding("utf8");
|
||||
// Dispatch the appropriate method
|
||||
switch(request.method) {
|
||||
case "GET": // Intentional fall-through
|
||||
case "DELETE":
|
||||
route.handler(request,response,state);
|
||||
break;
|
||||
case "PUT":
|
||||
var data = "";
|
||||
request.on("data",function(chunk) {
|
||||
data += chunk.toString();
|
||||
});
|
||||
request.on("end",function() {
|
||||
state.data = data;
|
||||
route.handler(request,response,state);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Listen for requests
|
||||
port: optional port number (falls back to value of "port" variable)
|
||||
host: optional host address (falls back to value of "hist" variable)
|
||||
*/
|
||||
Server.prototype.listen = function(port,host) {
|
||||
// Handle defaults for port and host
|
||||
port = port || this.get("port");
|
||||
host = host || this.get("host");
|
||||
// Check for the port being a string and look it up as an environment variable
|
||||
if(parseInt(port,10).toString() !== port) {
|
||||
port = process.env[port] || 8080;
|
||||
}
|
||||
$tw.utils.log("Serving on " + host + ":" + port,"brown/orange");
|
||||
$tw.utils.log("(press ctrl-C to exit)","red");
|
||||
// Warn if required plugins are missing
|
||||
if(!$tw.wiki.getTiddler("$:/plugins/tiddlywiki/tiddlyweb") || !$tw.wiki.getTiddler("$:/plugins/tiddlywiki/filesystem")) {
|
||||
$tw.utils.warning("Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file");
|
||||
}
|
||||
return http.createServer(this.requestHandler.bind(this)).listen(port,host);
|
||||
};
|
||||
|
||||
exports.Server = Server;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user