Support Forwarded header

This commit is contained in:
Zaid Abdulla committed 2019-12-29 14:50:08 -08:00
1 parent e80109de93
commit 603f5c79ac
10 files changed
+239 -34

No files matched your search

+2 -1
View File
@@ -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 {
+6 -5
View File
@@ -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
+15 -2
View File
@@ -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"]);
+19 -19
View File
@@ -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;
}
);
+176
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<IncomingMessage, RequestOrigin>();
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;
}
+7 -2
View File
@@ -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) => {
+5 -3
View File
@@ -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;
+3 -2
View File
@@ -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;
}
+5
View File
@@ -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",
+1
View File
@@ -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",