You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
122 lines
3.6 KiB
JavaScript
122 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
"use strict";
|
|
|
|
const Promise = require("bluebird");
|
|
const React = require("react");
|
|
const ReactDOMServer = require("react-dom/server");
|
|
const bhttp = require("bhttp");
|
|
const express = require("express");
|
|
const expressPromiseRouter = require("express-promise-router");
|
|
const { expression } = require("dataprog");
|
|
const url = require("url");
|
|
const createError = require("create-error");
|
|
const objectPick = require("object.pick");
|
|
const cheerio = require("cheerio");
|
|
const defaultValue = require("default-value");
|
|
const whatwgMimeType = require("whatwg-mimetype");
|
|
|
|
const rewriteHtmlUrls = require("./rewrite-html-urls");
|
|
const config = require("./load-config")(defaultValue(process.argv[2], "./config.jsx"));
|
|
|
|
let ConfigError = createError("ConfigError");
|
|
|
|
function stripWWW(hostname) {
|
|
return hostname.replace(/^www\./, "");
|
|
}
|
|
|
|
let app = express();
|
|
|
|
app.set("trust proxy", "loopback");
|
|
|
|
let router = expressPromiseRouter();
|
|
|
|
router.use((req, res) => {
|
|
let siteConfiguration = config.hosts[req.hostname];
|
|
|
|
if (siteConfiguration == null) {
|
|
res.status(404).send(`No configuration exists for host '${req.hostname}'`);
|
|
} else {
|
|
let filter = siteConfiguration.filters.find((filter) => {
|
|
if (filter.matchPath != null) {
|
|
if (typeof filter.matchPath === "string") {
|
|
return (filter.matchPath === req.path);
|
|
} else if (filter.matchPath instanceof RegExp) {
|
|
return filter.matchPath.test(req.path);
|
|
} else {
|
|
console.warn(`Unrecognized URL filter type while handling path '${req.path}'`);
|
|
return false;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
let newUrl = expression(() => {
|
|
if (filter.mapUrl == null) {
|
|
throw new ConfigError(`No URL mapper specified for path '${req.path}'`);
|
|
} else if (typeof filter.mapUrl === "function") {
|
|
return filter.mapUrl(url.parse(req.originalUrl));
|
|
} else if (typeof filter.mapUrl === "string") {
|
|
return filter.mapUrl;
|
|
} else {
|
|
throw new ConfigError(`Unrecognized URL filter type while handling path '${req.path}'`);
|
|
}
|
|
});
|
|
|
|
return Promise.try(() => {
|
|
return bhttp.get(newUrl, { headers: { "user-agent": "mobile proxy (questions/comments: admin@cryto.net)" } });
|
|
}).then((response) => {
|
|
res.set(objectPick(response.headers, [ "content-type" ]));
|
|
|
|
let mimeType = new whatwgMimeType(response.headers["content-type"]);
|
|
|
|
if (mimeType.isHTML()) {
|
|
let $ = cheerio.load(response.body);
|
|
|
|
rewriteHtmlUrls($, (path) => {
|
|
let parsedOrigin = url.parse(newUrl);
|
|
let parsedPath = url.parse(path);
|
|
|
|
if (!["http:", "https:"].includes(parsedPath.protocol)) {
|
|
return path;
|
|
} else if (parsedPath.host == null) {
|
|
return url.resolve(req.hostname, path);
|
|
} else if (stripWWW(parsedPath.host) === stripWWW(parsedOrigin.host)) {
|
|
/* Internal absolute URL */
|
|
return url.resolve(req.hostname, parsedPath.path);
|
|
} else {
|
|
/* External absolute URL */
|
|
return path;
|
|
}
|
|
});
|
|
|
|
/* Strips any target="_blank" attributes, which mess up some mobile readers */
|
|
$("a").attr("target", null);
|
|
|
|
let prefix = (siteConfiguration.prefix != null)
|
|
? ReactDOMServer.renderToStaticMarkup(React.createElement(siteConfiguration.prefix))
|
|
: "";
|
|
|
|
res.send(prefix + filter.mapContent($));
|
|
} else {
|
|
res.send(response.body);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
router.use((err, req, res, next) => {
|
|
if (err instanceof ConfigError) {
|
|
console.warn("Configuration error:", err.message);
|
|
res.status(500).send("Could not handle your request");
|
|
} else {
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
app.use(router);
|
|
app.listen(config.port, () => {
|
|
console.log(`Listening on port ${config.port}`);
|
|
});
|