mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-09-12 15:56:05 +00:00
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.
This commit is contained in:
@@ -19,7 +19,7 @@ exports.info = {
|
|||||||
synchronous: true,
|
synchronous: true,
|
||||||
namedParameterMode: true,
|
namedParameterMode: true,
|
||||||
mandatoryParameters: [],
|
mandatoryParameters: [],
|
||||||
optionalParameters: ["port","host","rootTiddler","renderType","serveType","username","password","pathprefix","debugLevel"]
|
optionalParameters: ["port","host","rootTiddler","renderType","serveType","username","password","pathprefix","debugLevel","credentials"]
|
||||||
};
|
};
|
||||||
|
|
||||||
var Command = function(params,commander,callback) {
|
var Command = function(params,commander,callback) {
|
||||||
|
@@ -90,17 +90,56 @@ Server.prototype.findMatchingRoute = function(request,state) {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.checkCredentials = function(request,incomingUsername,incomingPassword) {
|
Server.prototype.authenticateRequestBasic = function(request,response,state) {
|
||||||
var header = request.headers.authorization || "",
|
if(!this.credentialsData) {
|
||||||
token = header.split(/\s+/).pop() || "",
|
// Authenticate as anonymous if no credentials have been specified
|
||||||
auth = $tw.utils.base64Decode(token),
|
return true;
|
||||||
parts = auth.split(/:/),
|
|
||||||
username = parts[0],
|
|
||||||
password = parts[1];
|
|
||||||
if(incomingUsername === username && incomingPassword === password) {
|
|
||||||
return "ALLOWED";
|
|
||||||
} else {
|
} else {
|
||||||
return "DENIED";
|
// Extract the incoming username and password from the request
|
||||||
|
var header = request.headers.authorization || "",
|
||||||
|
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
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Server.prototype.authenticateRequestByHeader = function(request,response,state) {
|
||||||
|
var self = this,
|
||||||
|
header = self.get("authenticatedUserHeader")
|
||||||
|
if(!header) {
|
||||||
|
// Authenticate as anonymous if no trusted authenticated user header is specified
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Otherwise, authenticate as the username in the specified header
|
||||||
|
var username = request.headers[header];
|
||||||
|
if(!username) {
|
||||||
|
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
|
||||||
|
response.writeHead(401,"Authorization header required to login to '" + servername + "'");
|
||||||
|
response.end();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
state.authenticatedUsername = username;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,26 +150,19 @@ Server.prototype.requestHandler = function(request,response) {
|
|||||||
state.wiki = self.wiki;
|
state.wiki = self.wiki;
|
||||||
state.server = self;
|
state.server = self;
|
||||||
state.urlInfo = url.parse(request.url);
|
state.urlInfo = url.parse(request.url);
|
||||||
|
// Authenticate: provide error response on failure, add "username" to the state on success
|
||||||
|
if(!this.authenticateRequestBasic(request,response,state) || !this.authenticateRequestByHeader(request,response,state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Authorize
|
||||||
|
|
||||||
|
// Find the route that matches this path
|
||||||
|
var route = self.findMatchingRoute(request,state);
|
||||||
// Optionally output debug info
|
// Optionally output debug info
|
||||||
if(self.get("debugLevel") !== "none") {
|
if(self.get("debugLevel") !== "none") {
|
||||||
console.log("Request path:",JSON.stringify(state.urlInfo));
|
console.log("Request path:",JSON.stringify(state.urlInfo));
|
||||||
console.log("Request headers:",JSON.stringify(request.headers));
|
console.log("Request headers:",JSON.stringify(request.headers));
|
||||||
}
|
console.log("authenticatedUsername:",state.authenticatedUsername);
|
||||||
// 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
|
// Return a 404 if we didn't find a route
|
||||||
if(!route) {
|
if(!route) {
|
||||||
@@ -179,6 +211,30 @@ Server.prototype.listen = function(port,host) {
|
|||||||
if(!$tw.wiki.getTiddler("$:/plugins/tiddlywiki/tiddlyweb") || !$tw.wiki.getTiddler("$:/plugins/tiddlywiki/filesystem")) {
|
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.utils.warning("Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file");
|
||||||
}
|
}
|
||||||
|
// Read the credentials data if present
|
||||||
|
var credentialsFilepath = this.get("credentials");
|
||||||
|
if(credentialsFilepath) {
|
||||||
|
credentialsFilepath = path.join($tw.boot.wikiPath,credentialsFilepath);
|
||||||
|
if(fs.existsSync(credentialsFilepath) && !fs.statSync(credentialsFilepath).isDirectory()) {
|
||||||
|
var credentialsText = fs.readFileSync(credentialsFilepath,"utf8"),
|
||||||
|
credentialsData = $tw.utils.parseCsvStringWithHeader(credentialsText);
|
||||||
|
if(typeof credentialsData === "string") {
|
||||||
|
$tw.utils.error("Error: " + credentialsData + " reading credentials from '" + credentialsFilepath + "'");
|
||||||
|
} else {
|
||||||
|
this.credentialsData = credentialsData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$tw.utils.error("Error: Unable to load user credentials from '" + credentialsFilepath + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the hardcoded username and password if specified
|
||||||
|
if(this.get("username") && this.get("password")) {
|
||||||
|
this.credentialsData = this.credentialsData || [];
|
||||||
|
this.credentialsData.push({
|
||||||
|
username: this.get("username"),
|
||||||
|
password: this.get("password")
|
||||||
|
});
|
||||||
|
}
|
||||||
return http.createServer(this.requestHandler.bind(this)).listen(port,host);
|
return http.createServer(this.requestHandler.bind(this)).listen(port,host);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ exports.path = /^\/status$/;
|
|||||||
exports.handler = function(request,response,state) {
|
exports.handler = function(request,response,state) {
|
||||||
response.writeHead(200, {"Content-Type": "application/json"});
|
response.writeHead(200, {"Content-Type": "application/json"});
|
||||||
var text = JSON.stringify({
|
var text = JSON.stringify({
|
||||||
username: state.server.get("username"),
|
username: state.authenticatedUsername,
|
||||||
space: {
|
space: {
|
||||||
recipe: "default"
|
recipe: "default"
|
||||||
},
|
},
|
||||||
|
46
core/modules/utils/csv.js
Normal file
46
core/modules/utils/csv.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/core/modules/utils/csv.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: utils
|
||||||
|
|
||||||
|
A barebones CSV parser
|
||||||
|
|
||||||
|
\*/
|
||||||
|
(function(){
|
||||||
|
|
||||||
|
/*jslint node: true, browser: true */
|
||||||
|
/*global $tw: false */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse a CSV string with a header row and return an array of hashmaps.
|
||||||
|
*/
|
||||||
|
exports.parseCsvStringWithHeader = function(text,options) {
|
||||||
|
options = options || {};
|
||||||
|
var separator = options.separator || ",",
|
||||||
|
rows = text.split(/\r?\n/mg).map(function(row) {
|
||||||
|
return $tw.utils.trim(row);
|
||||||
|
}).filter(function(row) {
|
||||||
|
return row !== "";
|
||||||
|
});
|
||||||
|
if(rows.length < 1) {
|
||||||
|
return "Missing header row";
|
||||||
|
}
|
||||||
|
var headings = rows[0].split(separator),
|
||||||
|
results = [];
|
||||||
|
for(var row=1; row<rows.length; row++) {
|
||||||
|
var columns = rows[row].split(separator),
|
||||||
|
columnResult = Object.create(null);
|
||||||
|
if(columns.length !== headings.length) {
|
||||||
|
return "Malformed CSV row '" + rows[row] + "'";
|
||||||
|
}
|
||||||
|
for(var column=0; column<columns.length; column++) {
|
||||||
|
var columnName = headings[column];
|
||||||
|
columnResult[columnName] = $tw.utils.trim(columns[column] || "");
|
||||||
|
}
|
||||||
|
results.push(columnResult);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
Reference in New Issue
Block a user