mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-09-13 13:25:42 +00:00
Module-ize server routes, add static file support and other enhancements(#2679)
* Module-ize server routes and add static file support (#2510)
* Refactor server routes to modules
New module type: serverroute
Caveats: Loading order is not deterministic but this would only matter
if two route modules attempted to use the same path regexp (that would
be silly).
* Add static assets plugin
This plugin allows the node server to fetch static assets in the /assets
directory. I felt that this was a feature that goes above the core
functionality. That is why I added it as a plugin. with the modular
route extensions this was a breeze.
* Add serverroute description to ModuleTypes
* Coding standards tweaks
* Fix filename typo
* Move support for attachments from a plugin into the core
* Missing "else"
* 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
* Add check for unknown parameters
* Add support for multiple basic authentication credentials in a CSV file
Beware: Passwords are stored in plain text. If that's a problem, use an authenticating proxy and the trusted header authentication approach.
* Refactor module locations
* Rename "serverroute" module type to "route"
* Remove support for verifying optional named command parameters
The idea was to be able to flag unknown parameter names, but requiring a command to pre-specify all the parameter names makes it harder for (say) the listen command to be extensible so that plugins can add new optional parameters that they handle. (This is particularly in the context of work in progress to encapsulate authenticators into their own modules).
* Refactor the two authenticators into separate modules and add support for authorization
* Correct mistaken path.join vs. path.resolve
See https://stackoverflow.com/a/39836259
* Docs for the named command parameters
I'd be grateful if anyone with sufficient Windows experience could confirm that the note about double quotes in "NamedCommandParameters" is correct.
* Be consistent about lower case parameter names
* Do the right thing when we have a username but no password
With a username parameter but no password parameter we'll attribute edits to that username, but not require authentication.
* Remove obsolete code
* Add support for requiring authentication without restricting the username
* Refactor authorization checks
* Return read_only status in /status response
* Fix two code typos
* Add basic support for detecting readonly status and avoiding write errors
We now have syncadaptors returning readonly status and avoid attempting to write to the server if it's going to fail
* Add readonly-styles
We hide editing-related buttons in read only mode
I've made this part of the tiddlyweb plugin but I think a case could be made for putting it into the core.
* Add custom request header as CSRF mitigation
By default we require the header X-Requested-With to be set to TiddlyWiki. Can be overriden by setting csrfdisable to "yes"
See https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers
* Add support for HTTPS
* First pass at a route for serving rendered tiddlers
cc @Drakor
* Tweaks to the single tiddler static view
Adding a simple sidebar
* Switch to "dash" separated parameter names
* Typo
* Docs: Update ServerCommand and ListenCommand
* First pass at docs for the new web server stuff
Writing the docs is turning out to be quite an undertaking, much harder than writing the code!
* Get rid of extraneous paragraphs in static renderings
* Rejig anonymous user handling
Now we can support wikis that are read-only for anonymous access, but allow a user to login for read/write access.
* More docs
Slowly getting there...
* Static tiddler rendering: Fix HTML content in page title
* Docs updates
* Fix server command parameter names
Missed off 30ce7ea
* Docs: Missing quotes
* Avoid inadvertent dependency on Node.js > v9.6.0
The listenOptions parameter of the plain HTTP version of CreateServer was only introduced in v9.6.0
cc @Drakor @pmario
* Typo
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/authenticators/basic.js
|
||||
type: application/javascript
|
||||
module-type: authenticator
|
||||
|
||||
Authenticator for WWW basic authentication
|
||||
|
||||
\*/
|
||||
(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");
|
||||
}
|
||||
|
||||
function BasicAuthenticator(server) {
|
||||
this.server = server;
|
||||
this.credentialsData = [];
|
||||
}
|
||||
|
||||
/*
|
||||
Returns true if the authenticator is active, false if it is inactive, or a string if there is an error
|
||||
*/
|
||||
BasicAuthenticator.prototype.init = function() {
|
||||
// Read the credentials data
|
||||
this.credentialsFilepath = this.server.get("credentials");
|
||||
if(this.credentialsFilepath) {
|
||||
var resolveCredentialsFilepath = path.resolve($tw.boot.wikiPath,this.credentialsFilepath);
|
||||
if(fs.existsSync(resolveCredentialsFilepath) && !fs.statSync(resolveCredentialsFilepath).isDirectory()) {
|
||||
var credentialsText = fs.readFileSync(resolveCredentialsFilepath,"utf8"),
|
||||
credentialsData = $tw.utils.parseCsvStringWithHeader(credentialsText);
|
||||
if(typeof credentialsData === "string") {
|
||||
return "Error: " + credentialsData + " reading credentials from '" + resolveCredentialsFilepath + "'";
|
||||
} else {
|
||||
this.credentialsData = credentialsData;
|
||||
}
|
||||
} else {
|
||||
return "Error: Unable to load user credentials from '" + credentialsFilepath + "'";
|
||||
}
|
||||
}
|
||||
// Add the hardcoded username and password if specified
|
||||
if(this.server.get("username") && this.server.get("password")) {
|
||||
this.credentialsData = this.credentialsData || [];
|
||||
this.credentialsData.push({
|
||||
username: this.server.get("username"),
|
||||
password: this.server.get("password")
|
||||
});
|
||||
}
|
||||
return this.credentialsData.length > 0;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns true if the request is authenticated and assigns the "authenticatedUsername" state variable.
|
||||
Returns false if the request couldn't be authenticated having sent an appropriate response to the browser
|
||||
*/
|
||||
BasicAuthenticator.prototype.authenticateRequest = function(request,response,state) {
|
||||
// Extract the incoming username and password from the request
|
||||
var header = request.headers.authorization || "";
|
||||
if(!header && state.allowAnon) {
|
||||
// If there's no header and anonymous access is allowed then we don't set authenticatedUsername
|
||||
return true;
|
||||
}
|
||||
var token = header.split(/\s+/).pop() || "",
|
||||
auth = $tw.utils.base64Decode(token),
|
||||
parts = auth.split(/:/),
|
||||
incomingUsername = parts[0],
|
||||
incomingPassword = parts[1];
|
||||
// Check that at least one of the credentials matches
|
||||
var matchingCredentials = this.credentialsData.find(function(credential) {
|
||||
return credential.username === incomingUsername && credential.password === incomingPassword;
|
||||
});
|
||||
if(matchingCredentials) {
|
||||
// If so, add the authenticated username to the request state
|
||||
state.authenticatedUsername = incomingUsername;
|
||||
return true;
|
||||
} else {
|
||||
// If not, return an authentication challenge
|
||||
response.writeHead(401,"Authentication required",{
|
||||
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + state.server.servername + '"'
|
||||
});
|
||||
response.end();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.AuthenticatorClass = BasicAuthenticator;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/authenticators/header.js
|
||||
type: application/javascript
|
||||
module-type: authenticator
|
||||
|
||||
Authenticator for trusted header authentication
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
function HeaderAuthenticator(server) {
|
||||
this.server = server;
|
||||
this.header = server.get("authenticated-user-header");
|
||||
}
|
||||
|
||||
/*
|
||||
Returns true if the authenticator is active, false if it is inactive, or a string if there is an error
|
||||
*/
|
||||
HeaderAuthenticator.prototype.init = function() {
|
||||
return !!this.header;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns true if the request is authenticated and assigns the "authenticatedUsername" state variable.
|
||||
Returns false if the request couldn't be authenticated having sent an appropriate response to the browser
|
||||
*/
|
||||
HeaderAuthenticator.prototype.authenticateRequest = function(request,response,state) {
|
||||
// Otherwise, authenticate as the username in the specified header
|
||||
var username = request.headers[this.header];
|
||||
if(!username && !state.allowAnon) {
|
||||
response.writeHead(401,"Authorization header required to login to '" + state.server.servername + "'");
|
||||
response.end();
|
||||
return false;
|
||||
} else {
|
||||
// authenticatedUsername will be undefined for anonymous users
|
||||
state.authenticatedUsername = username;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
exports.AuthenticatorClass = HeaderAuthenticator;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,28 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/delete-tiddler.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
DELETE /recipes/default/tiddlers/:title
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "DELETE";
|
||||
|
||||
exports.path = /^\/bags\/default\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var title = decodeURIComponent(state.params[0]);
|
||||
state.wiki.deleteTiddler(title);
|
||||
response.writeHead(204, "OK", {
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,25 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-favicon.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /favicon.ico
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/favicon.ico$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
response.writeHead(200, {"Content-Type": "image/x-icon"});
|
||||
var buffer = state.wiki.getTiddlerText("$:/favicon.ico","");
|
||||
response.end(buffer,"base64");
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,50 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-file.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /files/:filepath
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/files\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var path = require("path"),
|
||||
fs = require("fs"),
|
||||
util = require("util");
|
||||
var filename = path.resolve($tw.boot.wikiPath,"files",decodeURIComponent(state.params[0])),
|
||||
extension = path.extname(filename);
|
||||
fs.readFile(filename,function(err,content) {
|
||||
var status,content,type = "text/plain";
|
||||
if(err) {
|
||||
if(err.code === "ENOENT") {
|
||||
status = 404;
|
||||
content = "File '" + filename + "' not found";
|
||||
} else if(err.code === "EACCES") {
|
||||
status = 403;
|
||||
content = "You do not have permission to access the file '" + filename + "'";
|
||||
} else {
|
||||
status = 500;
|
||||
content = err.toString();
|
||||
}
|
||||
} else {
|
||||
status = 200;
|
||||
content = content;
|
||||
type = $tw.config.fileExtensionInfo[extension] || "application/octet-stream";
|
||||
}
|
||||
response.writeHead(status,{
|
||||
"Content-Type": type
|
||||
});
|
||||
response.end(content);
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,25 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-index.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
response.writeHead(200, {"Content-Type": state.server.get("root-serve-type")});
|
||||
var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler"));
|
||||
response.end(text,"utf8");
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,35 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-login-basic.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /login-basic -- force a Basic Authentication challenge
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/login-basic$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
if(!state.authenticatedUsername) {
|
||||
// Challenge if there's no username
|
||||
response.writeHead(401,{
|
||||
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + state.server.servername + '"'
|
||||
});
|
||||
response.end();
|
||||
} else {
|
||||
// Redirect to the root wiki if login worked
|
||||
response.writeHead(302,{
|
||||
Location: "/"
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,33 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-status.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /status
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/status$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
response.writeHead(200, {"Content-Type": "application/json"});
|
||||
var text = JSON.stringify({
|
||||
username: state.authenticatedUsername || state.server.get("anon-username") || "",
|
||||
anonymous: !state.authenticatedUsername,
|
||||
read_only: !state.server.isAuthorized("writers",state.authenticatedUsername),
|
||||
space: {
|
||||
recipe: "default"
|
||||
},
|
||||
tiddlywiki_version: $tw.version
|
||||
});
|
||||
response.end(text,"utf8");
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,42 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-tiddler-html.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /:title
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/([^\/]+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var title = decodeURIComponent(state.params[0]),
|
||||
tiddler = state.wiki.getTiddler(title);
|
||||
if(tiddler) {
|
||||
var renderType,template;
|
||||
// Render ordinary tiddlers as HTML, and system tiddlers in plain text
|
||||
if(state.wiki.isSystemTiddler(title)) {
|
||||
renderType = state.server.get("system-tiddler-render-type");
|
||||
template = state.server.get("system-tiddler-template");
|
||||
} else {
|
||||
renderType = state.server.get("tiddler-render-type");
|
||||
template = state.server.get("tiddler-template");
|
||||
}
|
||||
var text = state.wiki.renderTiddler(renderType,template,{parseAsInline: true, variables: {currentTiddler: title}});
|
||||
// Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS
|
||||
response.writeHead(200);
|
||||
response.end(text,"utf8");
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,46 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-tiddler.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /recipes/default/tiddlers/:title
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var title = decodeURIComponent(state.params[0]),
|
||||
tiddler = state.wiki.getTiddler(title),
|
||||
tiddlerFields = {},
|
||||
knownFields = [
|
||||
"bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri"
|
||||
];
|
||||
if(tiddler) {
|
||||
$tw.utils.each(tiddler.fields,function(field,name) {
|
||||
var value = tiddler.getFieldString(name);
|
||||
if(knownFields.indexOf(name) !== -1) {
|
||||
tiddlerFields[name] = value;
|
||||
} else {
|
||||
tiddlerFields.fields = tiddlerFields.fields || {};
|
||||
tiddlerFields.fields[name] = value;
|
||||
}
|
||||
});
|
||||
tiddlerFields.revision = state.wiki.getChangeCount(title);
|
||||
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
|
||||
response.writeHead(200, {"Content-Type": "application/json"});
|
||||
response.end(JSON.stringify(tiddlerFields),"utf8");
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,37 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/get-tiddlers-json.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
GET /recipes/default/tiddlers/tiddlers.json
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/recipes\/default\/tiddlers.json$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
response.writeHead(200, {"Content-Type": "application/json"});
|
||||
var tiddlers = [];
|
||||
state.wiki.forEachTiddler({sortField: "title"},function(title,tiddler) {
|
||||
var tiddlerFields = {};
|
||||
$tw.utils.each(tiddler.fields,function(field,name) {
|
||||
if(name !== "text") {
|
||||
tiddlerFields[name] = tiddler.getFieldString(name);
|
||||
}
|
||||
});
|
||||
tiddlerFields.revision = state.wiki.getChangeCount(title);
|
||||
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
|
||||
tiddlers.push(tiddlerFields);
|
||||
});
|
||||
var text = JSON.stringify(tiddlers);
|
||||
response.end(text,"utf8");
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,42 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/routes/put-tiddler.js
|
||||
type: application/javascript
|
||||
module-type: route
|
||||
|
||||
PUT /recipes/default/tiddlers/:title
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var title = decodeURIComponent(state.params[0]),
|
||||
fields = JSON.parse(state.data);
|
||||
// Pull up any subfields in the `fields` object
|
||||
if(fields.fields) {
|
||||
$tw.utils.each(fields.fields,function(field,name) {
|
||||
fields[name] = field;
|
||||
});
|
||||
delete fields.fields;
|
||||
}
|
||||
// Remove any revision field
|
||||
if(fields.revision) {
|
||||
delete fields.revision;
|
||||
}
|
||||
state.wiki.addTiddler(new $tw.Tiddler(state.wiki.getCreationFields(),fields,{title: title},state.wiki.getModificationFields()));
|
||||
var changeCount = state.wiki.getChangeCount(title).toString();
|
||||
response.writeHead(204, "OK",{
|
||||
Etag: "\"default/" + encodeURIComponent(title) + "/" + changeCount + ":\"",
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,252 @@
|
||||
/*\
|
||||
title: $:/core/modules/server/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");
|
||||
}
|
||||
|
||||
/*
|
||||
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.authenticators = options.authenticators || [];
|
||||
this.wiki = options.wiki;
|
||||
this.servername = this.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
|
||||
// Initialise the variables
|
||||
this.variables = $tw.utils.extend({},this.defaultVariables);
|
||||
if(options.variables) {
|
||||
for(var variable in options.variables) {
|
||||
if(options.variables[variable]) {
|
||||
this.variables[variable] = options.variables[variable];
|
||||
}
|
||||
}
|
||||
}
|
||||
$tw.utils.extend({},this.defaultVariables,options.variables);
|
||||
// Initialise CSRF
|
||||
this.csrfDisable = this.get("csrf-disable") === "yes";
|
||||
// Initialise authorization
|
||||
var authorizedUserName = (this.get("username") && this.get("password")) ? this.get("username") : "(anon)";
|
||||
this.authorizationPrincipals = {
|
||||
readers: (this.get("readers") || authorizedUserName).split(",").map($tw.utils.trim),
|
||||
writers: (this.get("writers") || authorizedUserName).split(",").map($tw.utils.trim)
|
||||
}
|
||||
// Load and initialise authenticators
|
||||
$tw.modules.forEachModuleOfType("authenticator", function(title,authenticatorDefinition) {
|
||||
// console.log("Loading server route " + title);
|
||||
self.addAuthenticator(authenticatorDefinition.AuthenticatorClass);
|
||||
});
|
||||
// Load route handlers
|
||||
$tw.modules.forEachModuleOfType("route", function(title,routeDefinition) {
|
||||
// console.log("Loading server route " + title);
|
||||
self.addRoute(routeDefinition);
|
||||
});
|
||||
// Initialise the http vs https
|
||||
this.listenOptions = null;
|
||||
this.protocol = "http";
|
||||
var tlsKeyFilepath = this.get("tls-key"),
|
||||
tlsCertFilepath = this.get("tls-cert");
|
||||
if(tlsCertFilepath && tlsKeyFilepath) {
|
||||
this.listenOptions = {
|
||||
key: fs.readFileSync(path.resolve($tw.boot.wikiPath,tlsKeyFilepath),"utf8"),
|
||||
cert: fs.readFileSync(path.resolve($tw.boot.wikiPath,tlsCertFilepath),"utf8")
|
||||
};
|
||||
this.protocol = "https";
|
||||
}
|
||||
this.transport = require(this.protocol);
|
||||
}
|
||||
|
||||
Server.prototype.defaultVariables = {
|
||||
port: "8080",
|
||||
host: "127.0.0.1",
|
||||
"root-tiddler": "$:/core/save/all",
|
||||
"root-render-type": "text/plain",
|
||||
"root-serve-type": "text/html",
|
||||
"tiddler-render-type": "text/html",
|
||||
"tiddler-template": "$:/core/templates/server/static.tiddler.html",
|
||||
"system-tiddler-render-type": "text/plain",
|
||||
"system-tiddler-template": "$:/core/templates/wikified-tiddler",
|
||||
"debug-level": "none"
|
||||
};
|
||||
|
||||
Server.prototype.get = function(name) {
|
||||
return this.variables[name];
|
||||
};
|
||||
|
||||
Server.prototype.addRoute = function(route) {
|
||||
this.routes.push(route);
|
||||
};
|
||||
|
||||
Server.prototype.addAuthenticator = function(AuthenticatorClass) {
|
||||
// Instantiate and initialise the authenticator
|
||||
var authenticator = new AuthenticatorClass(this),
|
||||
result = authenticator.init();
|
||||
if(typeof result === "string") {
|
||||
$tw.utils.error("Error: " + result);
|
||||
} else if(result) {
|
||||
// Only use the authenticator if it initialised successfully
|
||||
this.authenticators.push(authenticator);
|
||||
}
|
||||
};
|
||||
|
||||
Server.prototype.findMatchingRoute = function(request,state) {
|
||||
var pathprefix = this.get("path-prefix") || "";
|
||||
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.methodMappings = {
|
||||
"GET": "readers",
|
||||
"OPTIONS": "readers",
|
||||
"HEAD": "readers",
|
||||
"PUT": "writers",
|
||||
"POST": "writers",
|
||||
"DELETE": "writers"
|
||||
};
|
||||
|
||||
/*
|
||||
Check whether a given user is authorized for the specified authorizationType ("readers" or "writers"). Pass null or undefined as the username to check for anonymous access
|
||||
*/
|
||||
Server.prototype.isAuthorized = function(authorizationType,username) {
|
||||
var principals = this.authorizationPrincipals[authorizationType] || [];
|
||||
return principals.indexOf("(anon)") !== -1 || (username && (principals.indexOf("(authenticated)") !== -1 || principals.indexOf(username) !== -1));
|
||||
}
|
||||
|
||||
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);
|
||||
// Get the principals authorized to access this resource
|
||||
var authorizationType = this.methodMappings[request.method] || "readers";
|
||||
// Check for the CSRF header if this is a write
|
||||
if(!this.csrfDisable && authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") {
|
||||
response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
// Check whether anonymous access is granted
|
||||
state.allowAnon = this.isAuthorized(authorizationType,null);
|
||||
// Authenticate with the first active authenticator
|
||||
if(this.authenticators.length > 0) {
|
||||
if(!this.authenticators[0].authenticateRequest(request,response,state)) {
|
||||
// Bail if we failed (the authenticator will have sent the response)
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Authorize with the authenticated username
|
||||
if(!this.isAuthorized(authorizationType,state.authenticatedUsername)) {
|
||||
response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
// Find the route that matches this path
|
||||
var route = self.findMatchingRoute(request,state);
|
||||
// Optionally output debug info
|
||||
if(self.get("debug-level") !== "none") {
|
||||
console.log("Request path:",JSON.stringify(state.urlInfo));
|
||||
console.log("Request headers:",JSON.stringify(request.headers));
|
||||
console.log("authenticatedUsername:",state.authenticatedUsername);
|
||||
}
|
||||
// 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 " + this.protocol + "://" + 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");
|
||||
}
|
||||
// Listen
|
||||
var server;
|
||||
if(this.listenOptions) {
|
||||
server = this.transport.createServer(this.listenOptions,this.requestHandler.bind(this));
|
||||
} else {
|
||||
server = this.transport.createServer(this.requestHandler.bind(this));
|
||||
}
|
||||
return server.listen(port,host);
|
||||
};
|
||||
|
||||
exports.Server = Server;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user