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

#!/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}`);
});