diff --git a/build/build.ts b/build/build.ts
index a56a51c..d33c940 100644
--- a/build/build.ts
+++ b/build/build.ts
@@ -77,7 +77,8 @@ const externals = [
"yaml",
"codemirror",
"codemirror/mode/javascript/javascript",
- "codemirror/mode/yaml/yaml"
+ "codemirror/mode/yaml/yaml",
+ "ipaddr.js"
];
function rmDirSync(dirPath): void {
diff --git a/docs/environment-variables.rst b/docs/environment-variables.rst
index f17ad85..4c606e7 100644
--- a/docs/environment-variables.rst
+++ b/docs/environment-variables.rst
@@ -164,12 +164,13 @@ FS_ACCESS_LOG_FILE
Default: unset
-FS_HOSTNAME
- The hostname to use when generating the file URL for TR-069 Download
- requests. Set this if the hostname where genieacs-fs is listening differs
- from the hostname in the ACS URL.
+FS_URL_PREFIX
+ The URL prefix (e.g. 'https://example.com:7657/') to use when generating the
+ file URL for TR-069 Download requests. Set this if genieacs-fs and
+ genieacs-cwmp are behind a proxy or running on different servers.
- Default: the hostname from the ACS URL
+ Default: auto generated based on the hostname from the ACS URL, FS_PORT
+ config, and whether or not SSL is enabled for genieacs-fs.
UI_WORKER_PROCESSES
The number of worker processes to spawn for genieacs-ui. A value of 0 means
diff --git a/lib/config.ts b/lib/config.ts
index 8d8ed8d..2ab8765 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -32,7 +32,7 @@ while (!existsSync(`${ROOT_DIR}/package.json`)) {
}
// For compatibility with v1.1
-let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl;
+let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl, fsHostname;
const options = {
EXT_DIR: { type: "path", default: resolve(ROOT_DIR, "config/ext") },
@@ -62,7 +62,7 @@ const options = {
FS_INTERFACE: { type: "string", default: "0.0.0.0" },
FS_SSL_CERT: { type: "string", default: "" },
FS_SSL_KEY: { type: "string", default: "" },
- FS_HOSTNAME: { type: "string", default: "" },
+ FS_URL_PREFIX: { type: "string", default: "" },
FS_LOG_FILE: { type: "path", default: "" },
FS_ACCESS_LOG_FILE: { type: "path", default: "" },
@@ -76,6 +76,7 @@ const options = {
UI_JWT_SECRET: { type: "string", default: "" },
UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 },
+ FORWARDED_HEADER: { type: "string", default: "" },
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
EXT_TIMEOUT: { type: "int", default: 3000 },
@@ -143,6 +144,9 @@ function setConfig(name, value, commandLineArgument = false): boolean {
.trim();
}
+ if (name === "FS_HOSTNAME" || name === "fs-hostname")
+ fsHostname = fsHostname || String(value).trim();
+
// For compatibility with v1.0
if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration")
setConfig("MAX_CACHE_TTL", value);
@@ -260,6 +264,15 @@ if (["true", "1"].includes(uiSsl)) {
setConfig("UI_SSL_KEY", `${d}/cwmp.key`);
}
+if (fsHostname) {
+ const FS_PORT = allConfig["FS_PORT"] || 7567;
+ const FS_SSL = !!allConfig["FS_SSL_CERT"];
+ setConfig(
+ "FS_URL_PREFIX",
+ (FS_SSL ? "https" : "http") + `://${fsHostname}:${FS_PORT}/`
+ );
+}
+
// Defaults
for (const [k, v] of Object.entries(options))
if (v["default"] != null) setConfig(k, v["default"]);
diff --git a/lib/cwmp.ts b/lib/cwmp.ts
index d5352db..43edef2 100644
--- a/lib/cwmp.ts
+++ b/lib/cwmp.ts
@@ -48,10 +48,10 @@ import {
import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream";
import { promisify } from "util";
-import { TLSSocket } from "tls";
import { decode, encodingExists } from "iconv-lite";
import { parseXmlDeclaration } from "./xml-parser";
import * as debug from "./debug";
+import { getRequestOrigin } from "./forwarded";
const gzipPromisified = promisify(zlib.gzip);
const deflatePromisified = promisify(zlib.deflate);
@@ -770,18 +770,18 @@ async function sendAcsRequest(
const downloadRequest = acsRequest as SetAcsRequest;
downloadRequest.fileSize = 0;
if (!downloadRequest.url) {
- const FS_PORT = config.get("FS_PORT");
- const FS_SSL = config.get("FS_SSL_CERT");
- let hostname = config.get("FS_HOSTNAME");
- if (!hostname) {
- if (sessionContext.httpRequest.headers["host"])
- hostname = sessionContext.httpRequest.headers["host"].split(":")[0];
- else hostname = sessionContext.httpRequest.connection.localAddress;
+ let prefix = "" + config.get("FS_URL_PREFIX");
+
+ if (!prefix) {
+ const FS_PORT = +config.get("FS_PORT");
+ const ssl = !!config.get("FS_SSL_CERT");
+ const origin = getRequestOrigin(sessionContext.httpRequest);
+ let hostname = origin.localAddress;
+ if (origin.host) [hostname] = origin.host.split(":", 1);
+ prefix = (ssl ? "https" : "http") + `://${hostname}:${FS_PORT}/`;
}
- downloadRequest.url = FS_SSL ? "https://" : "http://";
- downloadRequest.url += hostname;
- if (FS_PORT !== 80) downloadRequest.url += ":" + FS_PORT;
- downloadRequest.url += "/" + encodeURI(downloadRequest.fileName);
+
+ downloadRequest.url = prefix + encodeURI(downloadRequest.fileName);
const files = localCache.getFiles(sessionContext.cacheSnapshot);
if (files[downloadRequest.fileName])
@@ -974,7 +974,7 @@ async function responseUnauthorized(
resHeaders["Connection"] = "close";
} else {
- if (sessionContext.httpRequest.socket instanceof TLSSocket) {
+ if (getRequestOrigin(sessionContext.httpRequest).encrypted) {
resHeaders["WWW-Authenticate"] = `Basic realm="${REALM}"`;
} else {
const nonce = crypto.randomBytes(16).toString("hex");
@@ -1347,12 +1347,12 @@ async function listenerAsync(
cacheSnapshot,
"cwmp.debug",
{
- remoteAddress: httpResponse.connection.remoteAddress
+ remoteAddress: getRequestOrigin(httpRequest).remoteAddress
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
- return httpResponse.connection.remoteAddress;
+ return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
);
@@ -1395,12 +1395,12 @@ async function listenerAsync(
cacheSnapshot,
"cwmp.debug",
{
- remoteAddress: httpResponse.connection.remoteAddress
+ remoteAddress: getRequestOrigin(httpRequest).remoteAddress
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
- return httpResponse.connection.remoteAddress;
+ return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
);
@@ -1429,12 +1429,12 @@ async function listenerAsync(
cacheSnapshot,
"cwmp.debug",
{
- remoteAddress: httpResponse.connection.remoteAddress
+ remoteAddress: getRequestOrigin(httpRequest).remoteAddress
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
- return httpResponse.connection.remoteAddress;
+ return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
);
diff --git a/lib/forwarded.ts b/lib/forwarded.ts
new file mode 100644
index 0000000..ddd2b3e
--- /dev/null
+++ b/lib/forwarded.ts
@@ -0,0 +1,176 @@
+/**
+ * Copyright 2013-2019 GenieACS Inc.
+ *
+ * This file is part of GenieACS.
+ *
+ * GenieACS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * GenieACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with GenieACS. If not, see .
+ */
+
+import { IncomingMessage } from "http";
+import { TLSSocket } from "tls";
+import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
+import * as config from "./config";
+
+interface RequestOrigin {
+ localAddress: string;
+ localPort: number;
+ remoteAddress: string;
+ remotePort: number;
+ host: string;
+ encrypted: boolean;
+}
+
+const FORWARDED_HEADER = "" + config.get("FORWARDED_HEADER");
+const cache = new WeakMap();
+const cidrs: [IPv4 | IPv6, number][] = [];
+
+for (const str of FORWARDED_HEADER.split(",").map(s => s.trim())) {
+ try {
+ cidrs.push(parseCIDR(str));
+ } catch (err) {
+ // Not a valid CIDR format, try parsing as IP
+ try {
+ const ip = parse(str);
+ cidrs.push([ip, ip.toByteArray().length * 8]);
+ } catch (err) {
+ // Not a valid IP either, ignore
+ }
+ }
+}
+
+function parseForwardedHeader(str: string): { [name: string]: string } {
+ str = str.toLowerCase();
+ const res: { [name: string]: string } = {};
+ let keyIdx = 0;
+ let valueIdx = -1;
+ let key: string;
+ for (let i = 0; i < str.length; ++i) {
+ const char = str.charCodeAt(i);
+ if (char === 61 /* = */) {
+ if (keyIdx >= 0) {
+ key = str.slice(keyIdx, i).trim();
+ keyIdx = -1;
+ valueIdx = i + 1;
+ }
+ } else if (char === 59 /* ; */) {
+ if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
+ valueIdx = -1;
+ keyIdx = i + 1;
+ } else if (char === 44 /* , */) {
+ if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
+ return res;
+ } else if (char === 34 /* " */) {
+ if (valueIdx >= 0) {
+ const quoteIdx = i;
+ if (!str.slice(valueIdx, quoteIdx).trim()) {
+ for (i = i + 1; i < str.length; ++i) {
+ const c = str.charCodeAt(i);
+ if (c === 92 /* \ */) ++i;
+ if (c === 34 /* " */) {
+ res[key] = JSON.parse(str.slice(quoteIdx, i + 1).trim());
+ valueIdx = -1;
+ keyIdx = i + 1;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (valueIdx >= 0) res[key] = str.slice(valueIdx).trim();
+
+ return res;
+}
+
+export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
+ let origin = cache.get(request);
+ if (!origin) {
+ const soc = request.socket;
+ origin = {
+ localAddress: soc.localAddress,
+ localPort: soc.localPort,
+ remoteAddress: soc.remoteAddress,
+ remotePort: soc.remotePort,
+ host: request.headers["host"],
+ encrypted: !!(request.socket as TLSSocket).encrypted
+ };
+
+ // const header = request.headers["forwarded"];
+ const header = 'For="[2001:db8:cafe::17]:4711";proto_=https';
+ if (header) {
+ const ip = parse(soc.remoteAddress) as IPv4;
+ if (cidrs.some(cidr => ip.match(cidr as [IPv4, number]))) {
+ const parsed = parseForwardedHeader(header);
+
+ if (parsed["proto"] === "https") {
+ origin.encrypted = true;
+ origin.localPort = 443;
+ } else if (parsed["proto"] === "http") {
+ origin.encrypted = false;
+ origin.localPort = 80;
+ }
+
+ if (parsed["host"]) {
+ origin.host = parsed["host"];
+ const [, port] = parsed["host"].split(":", 2);
+ origin.localPort = +port || origin.localPort;
+ }
+
+ if (parsed["for"]) {
+ if (parsed["for"].startsWith("[")) {
+ const i = parsed["for"].lastIndexOf("]");
+ if (i >= 0) {
+ origin.remoteAddress = parsed["for"].slice(1, i);
+ origin.remotePort =
+ parseInt(parsed["for"].slice(i + 2)) || origin.remotePort;
+ }
+ } else {
+ const i = parsed["for"].lastIndexOf(":");
+ if (i >= 0) {
+ origin.remoteAddress = parsed["for"].slice(0, i);
+ origin.remotePort =
+ parseInt(parsed["for"].slice(i + 1)) || origin.remotePort;
+ } else {
+ origin.remoteAddress = parsed["for"];
+ }
+ }
+ }
+
+ if (parsed["by"]) {
+ if (parsed["by"].startsWith("[")) {
+ const i = parsed["by"].lastIndexOf("]");
+ if (i >= 0) {
+ origin.localAddress = parsed["by"].slice(1, i);
+ origin.localPort =
+ parseInt(parsed["by"].slice(i + 2)) || origin.localPort;
+ }
+ } else {
+ const i = parsed["by"].lastIndexOf(":");
+ if (i >= 0) {
+ origin.localAddress = parsed["by"].slice(0, i);
+ origin.localPort =
+ parseInt(parsed["by"].slice(i + 1)) || origin.localPort;
+ } else {
+ origin.localAddress = parsed["by"];
+ }
+ }
+ }
+ }
+ }
+
+ cache.set(request, origin);
+ }
+ return origin;
+}
diff --git a/lib/fs.ts b/lib/fs.ts
index be98836..1eed20c 100644
--- a/lib/fs.ts
+++ b/lib/fs.ts
@@ -19,11 +19,16 @@
import * as url from "url";
import * as querystring from "querystring";
+import { IncomingMessage, ServerResponse } from "http";
import { GridFSBucket } from "mongodb";
import * as db from "./db";
import * as logger from "./logger";
+import { getRequestOrigin } from "./forwarded";
-export function listener(request, response): void {
+export function listener(
+ request: IncomingMessage,
+ response: ServerResponse
+): void {
const urlParts = url.parse(request.url, true);
if (request.method === "GET") {
const filename = querystring.unescape(urlParts.pathname.substring(1));
@@ -31,7 +36,7 @@ export function listener(request, response): void {
const log = {
message: "Fetch file",
filename: filename,
- remoteAddress: request.connection.remoteAddress
+ remoteAddress: getRequestOrigin(request).remoteAddress
};
db.filesCollection.findOne({ _id: filename }, (err, file) => {
diff --git a/lib/logger.ts b/lib/logger.ts
index ad5b0d9..0a42874 100644
--- a/lib/logger.ts
+++ b/lib/logger.ts
@@ -21,6 +21,7 @@ import * as fs from "fs";
import * as os from "os";
import * as config from "./config";
+import { getRequestOrigin } from "./forwarded";
const REOPEN_EVERY = 60000;
@@ -149,8 +150,9 @@ export function close(): void {
export function flatten(details): {} {
if (details.sessionContext) {
details.deviceId = details.sessionContext.deviceId;
- details.remoteAddress =
- details.sessionContext.httpRequest.connection.remoteAddress;
+ details.remoteAddress = getRequestOrigin(
+ details.sessionContext.httpRequest
+ ).remoteAddress;
delete details.sessionContext;
}
@@ -198,7 +200,7 @@ export function flatten(details): {} {
// For genieacs-ui
if (details.context) {
- details.remoteAddress = details.context.request.ip;
+ details.remoteAddress = getRequestOrigin(details.context.req).remoteAddress;
if (details.context.state.user)
details.user = details.context.state.user.username;
delete details.context;
diff --git a/lib/session.ts b/lib/session.ts
index 5df1137..a07e889 100644
--- a/lib/session.ts
+++ b/lib/session.ts
@@ -50,6 +50,7 @@ import {
ScriptResult,
Expression
} from "./types";
+import { getRequestOrigin } from "./forwarded";
const VALID_PARAM_TYPES = new Set([
"xsd:int",
@@ -119,7 +120,7 @@ export function configContextCallback(
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress")
- return sessionContext.httpRequest.connection.remoteAddress;
+ return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
const deviceData = sessionContext.deviceData;
const paths = deviceData.paths;
@@ -130,7 +131,7 @@ export function configContextCallback(
}
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS")
- return sessionContext.httpRequest.connection.remoteAddress;
+ return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
}
return exp;
}
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index e096d87..e903e47 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -4525,6 +4525,11 @@
}
}
},
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+ },
"irregular-plurals": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz",
diff --git a/package.json b/package.json
index e9b1f37..56debd4 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"iconv-lite": "^0.5.0",
+ "ipaddr.js": "^1.9.1",
"jsonwebtoken": "^8.5.1",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",