mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-11-24 03:04: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) {
|
if(this.verbose) {
|
||||||
this.streams.output.write("Executing command: " + commandName + " " + params.join(" ") + "\n");
|
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) {
|
if(command.info.synchronous) {
|
||||||
// Synchronous command
|
// Synchronous command
|
||||||
c = new command.Command(params,this);
|
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) {
|
Commander.initCommands = function(moduleType) {
|
||||||
moduleType = moduleType || "command";
|
moduleType = moduleType || "command";
|
||||||
$tw.commands = {};
|
$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
|
type: application/javascript
|
||||||
module-type: command
|
module-type: command
|
||||||
|
|
||||||
Serve tiddlers over http
|
Deprecated legacy command for serving tiddlers
|
||||||
|
|
||||||
\*/
|
\*/
|
||||||
(function(){
|
(function(){
|
||||||
@@ -12,194 +12,41 @@ Serve tiddlers over http
|
|||||||
/*global $tw: false */
|
/*global $tw: false */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
if($tw.node) {
|
var Server = require("$:/core/modules/server.js").Server;
|
||||||
var util = require("util"),
|
|
||||||
fs = require("fs"),
|
|
||||||
url = require("url"),
|
|
||||||
path = require("path"),
|
|
||||||
http = require("http");
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.info = {
|
exports.info = {
|
||||||
name: "server",
|
name: "server",
|
||||||
synchronous: true
|
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 Command = function(params,commander,callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
this.commander = commander;
|
this.commander = commander;
|
||||||
this.callback = callback;
|
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() {
|
Command.prototype.execute = function() {
|
||||||
if(!$tw.boot.wikiTiddlersPath) {
|
if(!$tw.boot.wikiTiddlersPath) {
|
||||||
$tw.utils.warning("Warning: Wiki folder '" + $tw.boot.wikiPath + "' does not exist or is missing a tiddlywiki.info file");
|
$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",
|
// Set up server
|
||||||
rootTiddler = this.params[1] || "$:/core/save/all",
|
this.server = new Server({
|
||||||
renderType = this.params[2] || "text/plain",
|
wiki: this.commander.wiki,
|
||||||
serveType = this.params[3] || "text/html",
|
variables: {
|
||||||
username = this.params[4],
|
port: this.params[0],
|
||||||
password = this.params[5],
|
host: this.params[6],
|
||||||
host = this.params[6] || "127.0.0.1",
|
rootTiddler: this.params[1],
|
||||||
pathprefix = this.params[7],
|
renderType: this.params[2],
|
||||||
debugLevel = this.params[8] || "none";
|
serveType: this.params[3],
|
||||||
if(parseInt(port,10).toString() !== port) {
|
username: this.params[4],
|
||||||
port = process.env[port] || 8080;
|
password: this.params[5],
|
||||||
}
|
pathprefix: this.params[7],
|
||||||
this.server.set({
|
debugLevel: this.params[8]
|
||||||
rootTiddler: rootTiddler,
|
}
|
||||||
renderType: renderType,
|
|
||||||
serveType: serveType,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
pathprefix: pathprefix,
|
|
||||||
debugLevel: debugLevel
|
|
||||||
});
|
});
|
||||||
var nodeServer = this.server.listen(port,host);
|
var nodeServer = this.server.listen();
|
||||||
$tw.utils.log("Serving on " + host + ":" + port,"brown/orange");
|
$tw.hooks.invokeHook("th-server-command-post-start",this.server,nodeServer);
|
||||||
$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);
|
|
||||||
return null;
|
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