mirror of
https://github.com/azures04/crafatar.git
synced 2026-03-21 23:41:18 +01:00
uses `new URL()` and `decodeURI()` instead of `url.parse()` also checks that the requested file is in a subdirectory of `public/` before serving the file fixes path traversal vulnerability GHSA-5cxq-25mp-q5f2
180 lines
4.5 KiB
JavaScript
180 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
var querystring = require("querystring");
|
|
var response = require("./response");
|
|
var helpers = require("./helpers.js");
|
|
var toobusy = require("toobusy-js");
|
|
var logging = require("./logging");
|
|
var config = require("../config");
|
|
var http = require("http");
|
|
var mime = require("mime");
|
|
var path = require("path");
|
|
var url = require("url");
|
|
var fs = require("fs");
|
|
var server = null;
|
|
|
|
var routes = {
|
|
index: require("./routes/index"),
|
|
avatars: require("./routes/avatars"),
|
|
skins: require("./routes/skins"),
|
|
renders: require("./routes/renders"),
|
|
capes: require("./routes/capes"),
|
|
};
|
|
|
|
// serves assets from lib/public
|
|
function asset_request(req, callback) {
|
|
const filename = path.join(__dirname, "public", ...req.url.path_list);
|
|
const relative = path.relative(path.join(__dirname, "public"), filename);
|
|
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
fs.access(filename, function(fs_err) {
|
|
if (!fs_err) {
|
|
fs.readFile(filename, function(err, data) {
|
|
callback({
|
|
body: data,
|
|
type: mime.getType(filename),
|
|
err: err,
|
|
});
|
|
});
|
|
} else {
|
|
callback({
|
|
body: "Not found",
|
|
status: -2,
|
|
code: 404,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
callback({
|
|
body: "Forbidden",
|
|
status: -2,
|
|
code: 403,
|
|
});
|
|
}
|
|
}
|
|
|
|
// generates a 12 character random string
|
|
function request_id() {
|
|
return Math.random().toString(36).substring(2, 14);
|
|
}
|
|
|
|
// splits decoded URL path into an Array
|
|
function path_list(pathname) {
|
|
var list = pathname.split("/");
|
|
list.shift();
|
|
return list;
|
|
}
|
|
|
|
// handles the +req+ by routing to the request to the appropriate module
|
|
function requestHandler(req, res) {
|
|
req.url = new URL(decodeURI(req.url), 'http://' + req.headers.host);
|
|
req.url.pathname = path.resolve('/', req.url.pathname);
|
|
req.url.path_list = path_list(req.url.pathname);
|
|
req.id = request_id();
|
|
req.start = Date.now();
|
|
|
|
var local_path = req.url.path_list[0];
|
|
logging.debug(req.id, req.method, req.url.href);
|
|
|
|
toobusy.maxLag(200);
|
|
if (toobusy() && !process.env.TRAVIS) {
|
|
response(req, res, {
|
|
status: -1,
|
|
body: "Server is over capacity :/",
|
|
err: "Too busy",
|
|
code: 503,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
try {
|
|
switch (local_path) {
|
|
case "":
|
|
routes.index(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
break;
|
|
case "avatars":
|
|
routes.avatars(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
break;
|
|
case "skins":
|
|
routes.skins(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
break;
|
|
case "renders":
|
|
routes.renders(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
break;
|
|
case "capes":
|
|
routes.capes(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
break;
|
|
default:
|
|
asset_request(req, function(result) {
|
|
response(req, res, result);
|
|
});
|
|
}
|
|
} catch(e) {
|
|
var error = JSON.stringify(req.headers) + "\n" + e.stack;
|
|
response(req, res, {
|
|
status: -1,
|
|
body: config.server.debug_enabled ? error : "Internal Server Error",
|
|
err: error,
|
|
});
|
|
}
|
|
} else {
|
|
response(req, res, {
|
|
status: -2,
|
|
body: "Method Not Allowed",
|
|
code: 405,
|
|
});
|
|
}
|
|
}
|
|
|
|
var exp = {};
|
|
|
|
// Start the server
|
|
exp.boot = function(callback) {
|
|
var port = config.server.port;
|
|
var bind_ip = config.server.bind;
|
|
server = http.createServer(requestHandler).listen(port, bind_ip, function() {
|
|
logging.log("Server running on http://" + bind_ip + ":" + port + "/");
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// stop accepting new connections,
|
|
// wait for established connections to finish (30s max),
|
|
// then exit
|
|
process.on("SIGTERM", function() {
|
|
logging.warn("Got SIGTERM, no longer accepting new connections!");
|
|
|
|
setTimeout(function() {
|
|
logging.error("Dropping connections after 30s. Force quit.");
|
|
process.exit(1);
|
|
}, 30000);
|
|
|
|
server.close(function() {
|
|
logging.log("All connections closed, shutting down.");
|
|
process.exit();
|
|
});
|
|
});
|
|
};
|
|
|
|
// Close the server
|
|
exp.close = function(callback) {
|
|
helpers.stoplog();
|
|
server.close(callback);
|
|
};
|
|
|
|
module.exports = exp;
|
|
|
|
if (require.main === module) {
|
|
logging.error("Please use 'npm start' or 'www.js'");
|
|
process.exit(1);
|
|
} |