DEV: Fix ember-cli proxying to production sites (#15042)

This commit is contained in:
Jarek Radosz 2021-11-23 23:31:54 +01:00 committed by GitHub
parent 73760c77d9
commit 3172e08b6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,11 +1,12 @@
"use strict"; "use strict";
const express = require("express");
const bent = require("bent"); const bent = require("bent");
const getJSON = bent("json"); const getJSON = bent("json");
const { encode } = require("html-entities"); const { encode } = require("html-entities");
const cleanBaseURL = require("clean-base-url"); const cleanBaseURL = require("clean-base-url");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs/promises");
// via https://stackoverflow.com/a/6248722/165668 // via https://stackoverflow.com/a/6248722/165668
function generateUID() { function generateUID() {
@ -16,11 +17,6 @@ function generateUID() {
return firstPart + secondPart; return firstPart + secondPart;
} }
const IGNORE_PATHS = [
/\/ember-cli-live-reload\.js$/,
/\/session\/[^\/]+\/become$/,
];
function htmlTag(buffer, bootstrap) { function htmlTag(buffer, bootstrap) {
let classList = ""; let classList = "";
if (bootstrap.html_classes) { if (bootstrap.html_classes) {
@ -184,79 +180,81 @@ async function applyBootstrap(bootstrap, template, response, baseURL) {
return template; return template;
} }
function buildFromBootstrap(assetPath, proxy, baseURL, req, response) { async function buildFromBootstrap(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 handleRequest(assetPath, proxy, baseURL, req, res) {
if (assetPath.endsWith("tests/index.html")) {
return;
}
if (assetPath.endsWith("index.html")) {
try { try {
// Avoid Ember CLI's proxy if doing a GET, since Discourse depends on some non-XHR const template = await fs.readFile(
// GET requests to work. path.join(process.cwd(), "dist", "index.html"),
if (req.method === "GET") { "utf8"
let url = `${proxy}${req.path}`; );
let queryLoc = req.url.indexOf("?"); let url = `${proxy}${baseURL}bootstrap.json`;
const queryLoc = req.url.indexOf("?");
if (queryLoc !== -1) { if (queryLoc !== -1) {
url += req.url.substr(queryLoc); url += req.url.substr(queryLoc);
} }
req.headers["X-Discourse-Ember-CLI"] = "true"; const json = await getJSON(url, null, req.headers);
let get = bent("GET", [200, 301, 302, 303, 307, 308, 404, 403, 500]);
let response = await get(url, null, req.headers); return applyBootstrap(json.bootstrap, template, response, baseURL);
res.set(response.headers); } catch (error) {
res.set("content-type", "text/html"); throw new Error(
if (response.headers["x-discourse-bootstrap-required"] === "true") { `Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}`
req.headers["X-Discourse-Asset-Path"] = req.path;
let html = await buildFromBootstrap(
assetPath,
proxy,
baseURL,
req,
response
); );
return res.send(html);
} }
}
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 (req.headers["Referer"]) {
req.headers["Referer"] = req.headers["Referer"]
.replace(req.headers.host, originalHost)
.replace(/^https/, "http");
}
let url = `${proxy}${req.path}`;
const queryLoc = req.url.indexOf("?");
if (queryLoc !== -1) {
url += req.url.substr(queryLoc);
}
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.status(response.status);
res.send(await response.text()); res.send(await response.text());
} }
} catch (e) {
res.send(`
<html>
<h1>Discourse Build Error</h1>
<pre><code>${e.toString()}</code></pre>
</html>
`);
}
}
} }
module.exports = { module.exports = {
@ -267,12 +265,11 @@ module.exports = {
}, },
serverMiddleware(config) { serverMiddleware(config) {
let proxy = config.options.proxy; const app = config.app;
let app = config.app; let { proxy, rootURL, baseURL } = config.options;
let options = config.options;
if (!proxy) { if (!proxy) {
// eslint-disable-next-line // eslint-disable-next-line no-console
console.error(` console.error(`
Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application
to serve API requests. For example: to serve API requests. For example:
@ -281,31 +278,20 @@ to serve API requests. For example:
throw "--proxy argument is required"; throw "--proxy argument is required";
} }
let watcher = options.watcher; baseURL = rootURL === "" ? "/" : cleanBaseURL(rootURL || baseURL);
let baseURL = app.use(express.raw({ type: "*/*" }), async (req, res, next) => {
options.rootURL === ""
? "/"
: cleanBaseURL(options.rootURL || options.baseURL);
app.use(async (req, res, next) => {
try { try {
const results = await watcher; if (this.shouldHandleRequest(req)) {
if (this.shouldHandleRequest(req, options)) { await handleRequest(proxy, baseURL, req, res);
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);
} }
} catch (error) {
res.send(`
<html>
<h1>Discourse Build Error</h1>
<pre><code>${error}</code></pre>
</html>
`);
} finally { } finally {
if (!res.headersSent) { if (!res.headersSent) {
return next(); return next();
@ -314,25 +300,17 @@ to serve API requests. For example:
}); });
}, },
shouldHandleRequest(req) { shouldHandleRequest(request) {
let acceptHeaders = req.headers.accept || []; if (request.get("Accept")?.includes("text/html")) {
let hasHTMLHeader = acceptHeaders.indexOf("text/html") !== -1; return true;
if (req.method !== "GET") {
return false;
}
if (!hasHTMLHeader) {
return false;
} }
if (IGNORE_PATHS.some((ip) => ip.test(req.path))) { if (
return false; request.get("Content-Type")?.includes("application/x-www-form-urlencoded")
) {
return true;
} }
if (req.path.endsWith(".json")) {
return false; return false;
}
let baseURLRegexp = new RegExp(`^/`);
return baseURLRegexp.test(req.path);
}, },
}; };