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