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,78 +180,80 @@ 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 try {
return new Promise((resolve, reject) => { const template = await fs.readFile(
fs.readFile( path.join(process.cwd(), "dist", "index.html"),
path.join(process.cwd(), "dist", assetPath), "utf8"
"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()}`
);
});
}
); );
});
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) { async function handleRequest(proxy, baseURL, req, res) {
if (assetPath.endsWith("tests/index.html")) { const originalHost = req.headers.host;
return; 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")) { if (req.headers["Referer"]) {
try { req.headers["Referer"] = req.headers["Referer"]
// Avoid Ember CLI's proxy if doing a GET, since Discourse depends on some non-XHR .replace(req.headers.host, originalHost)
// GET requests to work. .replace(/^https/, "http");
if (req.method === "GET") { }
let url = `${proxy}${req.path}`;
let queryLoc = req.url.indexOf("?"); let url = `${proxy}${req.path}`;
if (queryLoc !== -1) { const queryLoc = req.url.indexOf("?");
url += req.url.substr(queryLoc); if (queryLoc !== -1) {
} url += req.url.substr(queryLoc);
}
req.headers["X-Discourse-Ember-CLI"] = "true"; if (req.method === "GET") {
let get = bent("GET", [200, 301, 302, 303, 307, 308, 404, 403, 500]); req.headers["X-Discourse-Ember-CLI"] = "true";
let response = await get(url, null, req.headers); req.headers["X-Discourse-Asset-Path"] = req.path;
res.set(response.headers); }
res.set("content-type", "text/html");
if (response.headers["x-discourse-bootstrap-required"] === "true") { const acceptedStatusCodes = [200, 301, 302, 303, 307, 308, 404, 403, 500];
req.headers["X-Discourse-Asset-Path"] = req.path; const proxyRequest = bent(req.method, acceptedStatusCodes);
let html = await buildFromBootstrap( const requestBody = req.method === "GET" ? null : req.body;
assetPath, const response = await proxyRequest(url, requestBody, req.headers);
proxy,
baseURL, res.set(response.headers);
req, res.set("content-encoding", null);
response
); const { location } = response.headers;
return res.send(html); if (location) {
} const newLocation = location
res.status(response.status); .replace(req.headers.host, originalHost)
res.send(await response.text()); .replace(/^https/, "http");
}
} catch (e) { res.set("location", newLocation);
res.send(` }
<html>
<h1>Discourse Build Error</h1> if (response.headers["x-discourse-bootstrap-required"] === "true") {
<pre><code>${e.toString()}</code></pre> const html = await buildFromBootstrap(proxy, baseURL, req, response);
</html> 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) { 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);
}, },
}; };