mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-14 00:07:07 +00:00
235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
var app = require("http").createServer(handler),
|
|
sockets = require("./sockets.js"),
|
|
{log, monitorFunction} = require("./log.js"),
|
|
path = require("path"),
|
|
fs = require("fs"),
|
|
crypto = require("crypto"),
|
|
serveStatic = require("serve-static"),
|
|
createSVG = require("./createSVG.js"),
|
|
templating = require("./templating.js"),
|
|
config = require("./configuration.js"),
|
|
polyfillLibrary = require("polyfill-library"),
|
|
check_output_directory = require("./check_output_directory.js"),
|
|
jwtauth = require("./jwtauth.js");
|
|
jwtBoardName = require("./jwtBoardnameAuth.js");
|
|
|
|
var MIN_NODE_VERSION = 10.0;
|
|
|
|
if (parseFloat(process.versions.node) < MIN_NODE_VERSION) {
|
|
console.warn(
|
|
"!!! You are using node " +
|
|
process.version +
|
|
", wbo requires at least " +
|
|
MIN_NODE_VERSION +
|
|
" !!!"
|
|
);
|
|
}
|
|
|
|
check_output_directory(config.HISTORY_DIR);
|
|
|
|
sockets.start(app);
|
|
|
|
app.listen(config.PORT, config.HOST);
|
|
log("server started", { port: config.PORT });
|
|
|
|
var CSP =
|
|
"default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:";
|
|
|
|
var fileserver = serveStatic(config.WEBROOT, {
|
|
maxAge: 2 * 3600 * 1000,
|
|
setHeaders: function (res) {
|
|
res.setHeader("X-UA-Compatible", "IE=Edge");
|
|
res.setHeader("Content-Security-Policy", CSP);
|
|
},
|
|
});
|
|
|
|
var errorPage = fs.readFileSync(path.join(config.WEBROOT, "error.html"));
|
|
function serveError(request, response) {
|
|
return function (err) {
|
|
log("error", { error: err && err.toString(), url: request.url });
|
|
response.writeHead(err ? 500 : 404, { "Content-Length": errorPage.length });
|
|
response.end(errorPage);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Write a request to the logs
|
|
* @param {import("http").IncomingMessage} request
|
|
*/
|
|
function logRequest(request) {
|
|
log("connection", {
|
|
ip: request.socket.remoteAddress,
|
|
original_ip:
|
|
request.headers["x-forwarded-for"] || request.headers["forwarded"],
|
|
user_agent: request.headers["user-agent"],
|
|
referer: request.headers["referer"],
|
|
language: request.headers["accept-language"],
|
|
url: request.url,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @type {import('http').RequestListener}
|
|
*/
|
|
function handler(request, response) {
|
|
try {
|
|
handleRequestAndLog(request, response);
|
|
} catch (err) {
|
|
console.trace(err);
|
|
response.writeHead(500, { "Content-Type": "text/plain" });
|
|
response.end(err.toString());
|
|
}
|
|
}
|
|
|
|
const boardTemplate = new templating.BoardTemplate(
|
|
path.join(config.WEBROOT, "board.html")
|
|
);
|
|
const indexTemplate = new templating.Template(
|
|
path.join(config.WEBROOT, "index.html")
|
|
);
|
|
|
|
/**
|
|
* Throws an error if the given board name is not allowed
|
|
* @param {string} boardName
|
|
* @throws {Error}
|
|
*/
|
|
function validateBoardName(boardName) {
|
|
if (/^[\w%\-_~()]*$/.test(boardName)) return boardName;
|
|
throw new Error("Illegal board name: " + boardName);
|
|
}
|
|
|
|
/**
|
|
* @type {import('http').RequestListener}
|
|
*/
|
|
function handleRequest(request, response) {
|
|
var parsedUrl = new URL(request.url, 'http://wbo/');
|
|
var parts = parsedUrl.pathname.split("/");
|
|
|
|
if (parts[0] === "") parts.shift();
|
|
|
|
var fileExt = path.extname(parsedUrl.pathname);
|
|
var staticResources = ['.js','.css', '.svg', '.ico', '.png', '.jpg', 'gif'];
|
|
// If we're not being asked for a file, then we should check permissions.
|
|
var isModerator = false;
|
|
if(!staticResources.includes(fileExt)) {
|
|
isModerator = jwtauth.checkUserPermission(parsedUrl);
|
|
}
|
|
|
|
switch (parts[0]) {
|
|
case "boards":
|
|
// "boards" refers to the root directory
|
|
if (parts.length === 1) {
|
|
// '/boards?board=...' This allows html forms to point to boards
|
|
var boardName = parsedUrl.searchParams.get("board") || "anonymous";
|
|
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
|
|
var headers = { Location: "boards/" + encodeURIComponent(boardName) };
|
|
response.writeHead(301, headers);
|
|
response.end();
|
|
} else if (parts.length === 2 && parsedUrl.pathname.indexOf(".") === -1) {
|
|
var boardName = validateBoardName(parts[1]);
|
|
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
|
|
boardTemplate.serve(request, response, isModerator);
|
|
// If there is no dot and no directory, parts[1] is the board name
|
|
} else {
|
|
request.url = "/" + parts.slice(1).join("/");
|
|
fileserver(request, response, serveError(request, response));
|
|
}
|
|
break;
|
|
|
|
case "download":
|
|
var boardName = validateBoardName(parts[1]),
|
|
history_file = path.join(
|
|
config.HISTORY_DIR,
|
|
"board-" + boardName + ".json"
|
|
);
|
|
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
|
|
if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
|
|
history_file += "." + parts[2] + ".bak";
|
|
}
|
|
log("download", { file: history_file });
|
|
fs.readFile(history_file, function (err, data) {
|
|
if (err) return serveError(request, response)(err);
|
|
response.writeHead(200, {
|
|
"Content-Type": "application/json",
|
|
"Content-Disposition": 'attachment; filename="' + boardName + '.wbo"',
|
|
"Content-Length": data.length,
|
|
});
|
|
response.end(data);
|
|
});
|
|
break;
|
|
|
|
case "export":
|
|
case "preview":
|
|
var boardName = validateBoardName(parts[1]),
|
|
history_file = path.join(
|
|
config.HISTORY_DIR,
|
|
"board-" + boardName + ".json"
|
|
);
|
|
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
|
|
response.writeHead(200, {
|
|
"Content-Type": "image/svg+xml",
|
|
"Content-Security-Policy": CSP,
|
|
"Cache-Control": "public, max-age=30",
|
|
});
|
|
var t = Date.now();
|
|
createSVG
|
|
.renderBoard(history_file, response)
|
|
.then(function () {
|
|
log("preview", { board: boardName, time: Date.now() - t });
|
|
response.end();
|
|
})
|
|
.catch(function (err) {
|
|
log("error", { error: err.toString(), stack: err.stack });
|
|
response.end("<text>Sorry, an error occured</text>");
|
|
});
|
|
break;
|
|
|
|
case "random":
|
|
var name = crypto
|
|
.randomBytes(32)
|
|
.toString("base64")
|
|
.replace(/[^\w]/g, "-");
|
|
response.writeHead(307, { Location: "boards/" + name });
|
|
response.end(name);
|
|
break;
|
|
|
|
case "polyfill.js": // serve tailored polyfills
|
|
case "polyfill.min.js":
|
|
polyfillLibrary
|
|
.getPolyfillString({
|
|
uaString: request.headers["user-agent"],
|
|
minify: request.url.endsWith(".min.js"),
|
|
features: {
|
|
default: { flags: ["gated"] },
|
|
es5: { flags: ["gated"] },
|
|
es6: { flags: ["gated"] },
|
|
es7: { flags: ["gated"] },
|
|
es2017: { flags: ["gated"] },
|
|
es2018: { flags: ["gated"] },
|
|
es2019: { flags: ["gated"] },
|
|
"performance.now": { flags: ["gated"] },
|
|
},
|
|
})
|
|
.then(function (bundleString) {
|
|
response.setHeader(
|
|
"Cache-Control",
|
|
"private, max-age=172800, stale-while-revalidate=1728000"
|
|
);
|
|
response.setHeader("Vary", "User-Agent");
|
|
response.setHeader("Content-Type", "application/javascript");
|
|
response.end(bundleString);
|
|
});
|
|
break;
|
|
|
|
case "": // Index page
|
|
logRequest(request);
|
|
indexTemplate.serve(request, response);
|
|
break;
|
|
|
|
default:
|
|
fileserver(request, response, serveError(request, response));
|
|
}
|
|
}
|
|
|
|
const handleRequestAndLog = monitorFunction(handleRequest);
|
|
module.exports = app;
|