blob: 1f3d3ae42c8d5b37cc268ade1ed6b0e2e4108203 [file] [log] [blame]
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import "zone.js/node";
import { existsSync, readFileSync } from "fs";
import { createServer as createRedirectServer } from "http";
import { createServer, request } from "https";
import { join } from "path";
import { APP_BASE_HREF } from "@angular/common";
import { ngExpressEngine } from "@nguniversal/express-engine";
import { ArgumentParser } from "argparse";
import * as express from "express";
import { getConfig, getVersion, ServerConfig, versionToString } from "server.config";
import { AppServerModule } from "./src/main.server";
let config: ServerConfig;
/**
* The Express app is exported so that it can be used by serverless Functions.
*
* @returns The Express.js application.
*/
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), "dist/traffic-portal/browser");
const indexHtml = existsSync(join(distFolder, "index.original.html")) ? "index.original.html" : "index";
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine("html", ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set("view engine", "html");
server.set("views", distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get("*.*", express.static(distFolder, {
maxAge: "1y"
}));
/**
* A handler for proxying the Traffic Ops API.
*
* @param req The client's request.
* @param res The server's response writer.
*/
function toProxyHandler(req: express.Request, res: express.Response): void {
console.log(`Making TO API request to \`${req.originalUrl}\``);
const fwdRequest = {
headers: req.headers,
host: config.trafficOps.hostname,
method: req.method,
path: req.originalUrl,
port: config.trafficOps.port,
rejectUnauthorized: !config.insecure,
};
try {
const proxiedRequest = request(fwdRequest, (r) => {
res.writeHead(r.statusCode ?? 502, r.headers);
r.pipe(res);
});
req.pipe(proxiedRequest);
} catch (e) {
console.error("proxying request:", e);
}
}
server.use("api/**", toProxyHandler);
server.use("/api/**", toProxyHandler);
// All regular routes use the Universal engine
server.get("*", (req, res) => {
res.render(indexHtml, { providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }], req });
});
server.enable("trust proxy");
return server;
}
/**
* Runs the server.
*
* @returns An exit code for the process.
*/
function run(): number {
const version = getVersion();
const parser = new ArgumentParser({
// Nothing I can do about this, library specifies its interface.
/* eslint-disable @typescript-eslint/naming-convention */
add_help: true,
/* eslint-enable @typescript-eslint/naming-convention */
description: "Traffic Portal re-written in modern Angular"
});
parser.add_argument("-t", "--traffic-ops", {
dest: "trafficOps",
help: "Specify the Traffic Ops host/URL, including port. (Default: uses the `TO_URL` environment variable)",
type: (arg: string) => {
try {
return new URL(arg);
} catch (e) {
if (e instanceof TypeError) {
return new URL(`https://${arg}`);
}
throw e;
}
}
});
parser.add_argument("-k", "--insecure", {
action: "store_true",
help: "Skip Traffic Ops server certificate validation. This affects requests from Traffic Portal to Traffic Ops AND signature" +
" verification of any passed SSL keys/certificates"
});
parser.add_argument("-p", "--port", {
default: 4200,
help: "Specify the port on which Traffic Portal will listen (Default: 4200)",
type: "int"
});
parser.add_argument("-c", "--cert-path", {
dest: "certPath",
help: "Specify a location for an SSL certificate to be used by Traffic Portal. (Requires `-K`/`--key-path`. If both are omitted," +
" will serve using HTTP)",
type: "str"
});
parser.add_argument("-K", "--key-path", {
dest: "keyPath",
help: "Specify a location for an SSL certificate to be used by Traffic Portal. (Requires `-c`/`--cert-path`. If both are omitted," +
" will serve using HTTP)",
type: "str"
});
parser.add_argument("-C", "--config-file", {
default: "/etc/traffic-portal/config.js",
dest: "configFile",
help: "Specify a path to a configuration file - options are overridden by command-line flags.",
type: "str"
});
parser.add_argument("-v", "--version", {
action: "version",
version: versionToString(version)
});
try {
config = getConfig(parser.parse_args(), version);
} catch (e) {
console.error(`Failed to initialize server configuration: ${e}`);
return 1;
}
// Start up the Node server
const server = app();
if (config.useSSL) {
let cert: string;
let key: string;
try {
cert = readFileSync(config.certPath, {encoding: "utf8"});
key = readFileSync(config.keyPath, {encoding: "utf8"});
} catch (e) {
console.error("reading SSL key/cert:", e);
return 1;
}
createServer(
{
cert,
key,
rejectUnauthorized: !config.insecure,
},
server
).listen(config.port, ()=> {
console.log(`Node Express server listening on port ${config.port}`);
});
try {
const redirectServer = createRedirectServer(
(req, res) => {
if (!req.url) {
res.statusCode = 500;
console.error("got HTTP request for redirect that had no URL");
res.end();
return;
}
res.statusCode = 308;
res.setHeader("Location", req.url.replace(/^[hH][tT][tT][pP]:/, "https:"));
res.end();
}
);
redirectServer.listen(80);
redirectServer.on("error", e => {
console.error(`redirect server encountered error: ${e}`);
if (Object.prototype.hasOwnProperty.call(e, "code") && (e as typeof e & {code: unknown}).code === "EACCES") {
console.warn("access to port 80 not allowed; closing redirect server");
redirectServer.close();
}
});
} catch (e) {
console.warn("Failed to initialize HTTP-to-HTTPS redirect listener:", e);
}
} else {
server.listen(config.port, () => {
console.log(`Node Express server listening on port ${config.port}`);
});
}
return 0;
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
/* eslint-disable no-underscore-dangle */
// eslint-disable-next-line @typescript-eslint/naming-convention
declare const __non_webpack_require__: NodeRequire;
/* eslint-enable no-underscore-dangle */
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || "";
if (moduleFilename === __filename || moduleFilename.includes("iisnode")) {
const code = run();
if (code) {
process.exit(code);
}
}
export * from "./src/main.server";