From 3172e08b6d17e82c09b4524394ea5e95ee301616 Mon Sep 17 00:00:00 2001 From: Jarek Radosz <jradosz@gmail.com> Date: Tue, 23 Nov 2021 23:31:54 +0100 Subject: [PATCH] DEV: Fix ember-cli proxying to production sites (#15042) --- .../discourse/lib/bootstrap-json/index.js | 204 ++++++++---------- 1 file changed, 91 insertions(+), 113 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index 60424357282..86f346d34cf 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -1,11 +1,12 @@ "use strict"; +const express = require("express"); const bent = require("bent"); const getJSON = bent("json"); const { encode } = require("html-entities"); const cleanBaseURL = require("clean-base-url"); const path = require("path"); -const fs = require("fs"); +const fs = require("fs/promises"); // via https://stackoverflow.com/a/6248722/165668 function generateUID() { @@ -16,11 +17,6 @@ function generateUID() { return firstPart + secondPart; } -const IGNORE_PATHS = [ - /\/ember-cli-live-reload\.js$/, - /\/session\/[^\/]+\/become$/, -]; - function htmlTag(buffer, bootstrap) { let classList = ""; if (bootstrap.html_classes) { @@ -184,78 +180,80 @@ async function applyBootstrap(bootstrap, template, response, baseURL) { return template; } -function buildFromBootstrap(assetPath, proxy, baseURL, req, response) { - // eslint-disable-next-line - return new Promise((resolve, reject) => { - fs.readFile( - path.join(process.cwd(), "dist", assetPath), - "utf8", - (err, template) => { - let url = `${proxy}${baseURL}bootstrap.json`; - let queryLoc = req.url.indexOf("?"); - if (queryLoc !== -1) { - url += req.url.substr(queryLoc); - } - - getJSON(url, null, req.headers) - .then((json) => { - return applyBootstrap(json.bootstrap, template, response, baseURL); - }) - .then(resolve) - .catch((e) => { - reject( - `Could not get ${proxy}${baseURL}bootstrap.json\n\n${e.toString()}` - ); - }); - } +async function buildFromBootstrap(proxy, baseURL, req, response) { + try { + const template = await fs.readFile( + path.join(process.cwd(), "dist", "index.html"), + "utf8" ); - }); + + let url = `${proxy}${baseURL}bootstrap.json`; + const queryLoc = req.url.indexOf("?"); + if (queryLoc !== -1) { + url += req.url.substr(queryLoc); + } + + const json = await getJSON(url, null, req.headers); + + return applyBootstrap(json.bootstrap, template, response, baseURL); + } catch (error) { + throw new Error( + `Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}` + ); + } } -async function handleRequest(assetPath, proxy, baseURL, req, res) { - if (assetPath.endsWith("tests/index.html")) { - return; +async function handleRequest(proxy, baseURL, req, res) { + const originalHost = req.headers.host; + req.headers.host = new URL(proxy).host; + + if (req.headers["Origin"]) { + req.headers["Origin"] = req.headers["Origin"] + .replace(req.headers.host, originalHost) + .replace(/^https/, "http"); } - if (assetPath.endsWith("index.html")) { - try { - // Avoid Ember CLI's proxy if doing a GET, since Discourse depends on some non-XHR - // GET requests to work. - if (req.method === "GET") { - let url = `${proxy}${req.path}`; + if (req.headers["Referer"]) { + req.headers["Referer"] = req.headers["Referer"] + .replace(req.headers.host, originalHost) + .replace(/^https/, "http"); + } - let queryLoc = req.url.indexOf("?"); - if (queryLoc !== -1) { - url += req.url.substr(queryLoc); - } + let url = `${proxy}${req.path}`; + const queryLoc = req.url.indexOf("?"); + if (queryLoc !== -1) { + url += req.url.substr(queryLoc); + } - req.headers["X-Discourse-Ember-CLI"] = "true"; - let get = bent("GET", [200, 301, 302, 303, 307, 308, 404, 403, 500]); - let response = await get(url, null, req.headers); - res.set(response.headers); - res.set("content-type", "text/html"); - if (response.headers["x-discourse-bootstrap-required"] === "true") { - req.headers["X-Discourse-Asset-Path"] = req.path; - let html = await buildFromBootstrap( - assetPath, - proxy, - baseURL, - req, - response - ); - return res.send(html); - } - res.status(response.status); - res.send(await response.text()); - } - } catch (e) { - res.send(` - <html> - <h1>Discourse Build Error</h1> - <pre><code>${e.toString()}</code></pre> - </html> - `); - } + if (req.method === "GET") { + req.headers["X-Discourse-Ember-CLI"] = "true"; + req.headers["X-Discourse-Asset-Path"] = req.path; + } + + const acceptedStatusCodes = [200, 301, 302, 303, 307, 308, 404, 403, 500]; + const proxyRequest = bent(req.method, acceptedStatusCodes); + const requestBody = req.method === "GET" ? null : req.body; + const response = await proxyRequest(url, requestBody, req.headers); + + res.set(response.headers); + res.set("content-encoding", null); + + const { location } = response.headers; + if (location) { + const newLocation = location + .replace(req.headers.host, originalHost) + .replace(/^https/, "http"); + + res.set("location", newLocation); + } + + if (response.headers["x-discourse-bootstrap-required"] === "true") { + const html = await buildFromBootstrap(proxy, baseURL, req, response); + res.set("content-type", "text/html"); + res.send(html); + } else { + res.status(response.status); + res.send(await response.text()); } } @@ -267,12 +265,11 @@ module.exports = { }, serverMiddleware(config) { - let proxy = config.options.proxy; - let app = config.app; - let options = config.options; + const app = config.app; + let { proxy, rootURL, baseURL } = config.options; if (!proxy) { - // eslint-disable-next-line + // eslint-disable-next-line no-console console.error(` Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application to serve API requests. For example: @@ -281,31 +278,20 @@ to serve API requests. For example: throw "--proxy argument is required"; } - let watcher = options.watcher; + baseURL = rootURL === "" ? "/" : cleanBaseURL(rootURL || baseURL); - let baseURL = - options.rootURL === "" - ? "/" - : cleanBaseURL(options.rootURL || options.baseURL); - - app.use(async (req, res, next) => { + app.use(express.raw({ type: "*/*" }), async (req, res, next) => { try { - const results = await watcher; - if (this.shouldHandleRequest(req, options)) { - let assetPath = req.path.slice(baseURL.length); - let isFile = false; - - try { - isFile = fs - .statSync(path.join(results.directory, assetPath)) - .isFile(); - } catch (err) {} - - if (!isFile) { - assetPath = "index.html"; - } - await handleRequest(assetPath, proxy, baseURL, req, res); + if (this.shouldHandleRequest(req)) { + await handleRequest(proxy, baseURL, req, res); } + } catch (error) { + res.send(` + <html> + <h1>Discourse Build Error</h1> + <pre><code>${error}</code></pre> + </html> + `); } finally { if (!res.headersSent) { return next(); @@ -314,25 +300,17 @@ to serve API requests. For example: }); }, - shouldHandleRequest(req) { - let acceptHeaders = req.headers.accept || []; - let hasHTMLHeader = acceptHeaders.indexOf("text/html") !== -1; - if (req.method !== "GET") { - return false; - } - if (!hasHTMLHeader) { - return false; + shouldHandleRequest(request) { + if (request.get("Accept")?.includes("text/html")) { + return true; } - if (IGNORE_PATHS.some((ip) => ip.test(req.path))) { - return false; + if ( + request.get("Content-Type")?.includes("application/x-www-form-urlencoded") + ) { + return true; } - if (req.path.endsWith(".json")) { - return false; - } - - let baseURLRegexp = new RegExp(`^/`); - return baseURLRegexp.test(req.path); + return false; }, };