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;
   },
 };