Expose all device params to config expressions

This commit is contained in:
Zaid Abdulla committed 2019-11-27 17:00:03 -08:00
1 parent 75ff56f3ae
commit add4c84132
9 files changed
+254 -137

No files matched your search

+77 -54
View File
@@ -31,57 +31,77 @@ import {
udpConnectionRequest
} from "./connection-request";
import { Expression, Task } from "./types";
import { flattenDevice } from "./mongodb-functions";
import { evaluate } from "./common/expression";
export async function connectionRequest(deviceId): Promise<void> {
const options = {
projection: {
_deviceId: 1,
"Device.ManagementServer.ConnectionRequestURL._value": 1,
"Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
"Device.ManagementServer.ConnectionRequestUsername._value": 1,
"Device.ManagementServer.ConnectionRequestPassword._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
export async function connectionRequest(
deviceId: string,
device?: {}
): Promise<void> {
if (!device) {
const res = await db.devicesCollection.findOne({ _id: deviceId });
if (!res) throw new Error("No such device");
device = flattenDevice(res);
}
};
const device = await db.devicesCollection.findOne({ _id: deviceId }, options);
if (!device) throw new Error("No such device");
let connectionRequestUrl, udpConnectionRequestAddress, username, password;
let managementServer,
connectionRequestUrl,
udpConnectionRequestAddress,
username,
password;
if (device.Device)
// TR-181 data model
managementServer = device.Device.ManagementServer;
// TR-098 data model
else managementServer = device.InternetGatewayDevice.ManagementServer;
if (managementServer.ConnectionRequestURL)
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
if (managementServer.UDPConnectionRequestAddress) {
udpConnectionRequestAddress =
managementServer.UDPConnectionRequestAddress._value;
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {}
).value || [""])[0];
username = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {}
).value || [""])[0];
password = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {}
).value || [""])[0];
} else {
connectionRequestUrl = (device[
"Device.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
).value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
}
if (managementServer.ConnectionRequestUsername)
username = managementServer.ConnectionRequestUsername._value;
if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value;
const context = {
id: device["_id"],
serialNumber: device["_deviceId"]["SerialNumber"],
productClass: device["_deviceId"]["ProductClass"],
oui: device["_deviceId"]["OUI"],
remoteAddress: connectionRequestUrl
? parse(connectionRequestUrl).host
: null,
username: username || "",
password: password || ""
const remoteAddress = parse(connectionRequestUrl).host;
const evalCallback = (exp): Expression => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
let name = exp[1];
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress") return remoteAddress;
else if (name === "username") return username;
else if (name === "password") return password;
const p = device[name];
if (p && p.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
else if (exp[1] === "USERNAME") return username;
else if (exp[1] === "PASSWORD") return password;
}
return exp;
};
const snapshot = await getCurrentSnapshot();
@@ -89,20 +109,23 @@ export async function connectionRequest(deviceId): Promise<void> {
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
snapshot,
"cwmp.udpConnectionRequestPort",
context,
now
{},
now,
evalCallback
);
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
snapshot,
"cwmp.connectionRequestTimeout",
context,
now
{},
now,
evalCallback
);
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
snapshot,
"cwmp.connectionRequestAllowBasicAuth",
context,
now
{},
now,
evalCallback
);
let authExp: Expression = getConfigExpression(
snapshot,
@@ -118,14 +141,15 @@ export async function connectionRequest(deviceId): Promise<void> {
] as Expression;
}
const debug = !!getConfig(snapshot, "cwmp.debug", context, now);
authExp = evaluate(authExp, {}, now, evalCallback);
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
let udpProm;
if (udpConnectionRequestAddress) {
udpProm = udpConnectionRequest(
udpConnectionRequestAddress,
authExp,
context,
UDP_CONNECTION_REQUEST_PORT,
debug,
deviceId
@@ -136,7 +160,6 @@ export async function connectionRequest(deviceId): Promise<void> {
await httpConnectionRequest(
connectionRequestUrl,
authExp,
context,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
CONNECTION_REQUEST_TIMEOUT,
debug,
+3 -1
View File
@@ -26,8 +26,10 @@ function parseHeaderFeilds(str: string): {} {
let part;
while ((part = parts.shift()) != null) {
const name = part.split("=", 1)[0];
if (name.length === part.length)
if (name.length === part.length) {
if (!part.trim()) continue;
throw new Error("Unable to parse auth header");
}
let value = part.slice(name.length + 1);
if (!/^\s*"/.test(value)) {
+7 -27
View File
@@ -29,15 +29,13 @@ import * as debug from "./debug";
async function extractAuth(
exp: Expression,
context: {},
now: number,
dflt: any
): Promise<[string, string, Expression]> {
let username, password;
const _exp = await evaluateAsync(
exp,
context,
now,
{},
0,
async (e: Expression): Promise<Expression> => {
if (!username && Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "EXT") {
@@ -50,8 +48,8 @@ async function extractAuth(
return fault ? null : value;
} else if (e[1] === "AUTH") {
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
username = e[2];
password = e[3];
username = e[2] || "";
password = e[3] || "";
}
return dflt;
}
@@ -95,13 +93,11 @@ function httpGet(
export async function httpConnectionRequest(
address: string,
authExp: Expression,
context: {},
allowBasicAuth: boolean,
timeout: number,
_debug: boolean,
deviceId: string
): Promise<void> {
const now = Date.now();
const options: http.RequestOptions = parse(address);
if (options.protocol !== "http:")
throw new Error("Invalid connection request URL or protocol");
@@ -163,12 +159,7 @@ export async function httpConnectionRequest(
authHeader = auth.parseWwwAuthenticateHeader(
res.headers["www-authenticate"]
);
[username, password, authExp] = await extractAuth(
authExp,
context,
now,
false
);
[username, password, authExp] = await extractAuth(authExp, false);
} else {
throw new Error(
`Unexpected response code from device: ${res.statusCode}`
@@ -181,7 +172,6 @@ export async function httpConnectionRequest(
export async function udpConnectionRequest(
address: string,
authExp: Expression,
context: {},
sourcePort: number = 0,
_debug: boolean,
deviceId: string
@@ -202,12 +192,7 @@ export async function udpConnectionRequest(
let username: string;
let password: string;
[username, password, authExp] = await extractAuth(
authExp,
context,
now,
null
);
[username, password, authExp] = await extractAuth(authExp, null);
if (username == null) username = "";
if (password == null) password = "";
@@ -233,12 +218,7 @@ export async function udpConnectionRequest(
});
}
[username, password, authExp] = await extractAuth(
authExp,
context,
now,
null
);
[username, password, authExp] = await extractAuth(authExp, null);
}
client.close();
}
+64 -30
View File
@@ -103,12 +103,12 @@ async function authenticate(
authentication["body"] = body;
}
const now = Date.now();
const res = await evaluateAsync(
authExpression,
sessionContext.configContext,
now,
{},
sessionContext.timestamp,
async (e: Expression): Promise<Expression> => {
e = session.configContextCallback(sessionContext, e);
if (Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "EXT") {
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
@@ -294,7 +294,9 @@ async function inform(sessionContext: SessionContext, rpc): Promise<void> {
const cookiesPath = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.cookiesPath",
sessionContext.configContext
{},
sessionContext.timestamp,
e => session.configContextCallback(sessionContext, e)
);
if (cookiesPath) {
@@ -384,7 +386,9 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
const RETRY_DELAY = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.retryDelay",
sessionContext.configContext
{},
sessionContext.timestamp,
e => session.configContextCallback(sessionContext, e)
);
if (sessionContext.faults) {
@@ -1263,9 +1267,19 @@ async function listenerAsync(
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, msg);
} else {
const cacheSnapshot = await localCache.getCurrentSnapshot();
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
const d = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: httpResponse.connection.remoteAddress
});
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return httpResponse.connection.remoteAddress;
return e;
}
);
if (d) debug.outgoingHttpResponse(httpResponse, null, msg);
}
httpResponse.end(msg);
@@ -1314,9 +1328,19 @@ async function listenerAsync(
}
} else {
const cacheSnapshot = await localCache.getCurrentSnapshot();
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
const d = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: httpResponse.connection.remoteAddress
});
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return httpResponse.connection.remoteAddress;
return e;
}
);
if (d) debug.outgoingHttpResponse(httpResponse, null, error.message);
}
httpResponse.end(error.message);
@@ -1347,9 +1371,19 @@ async function listenerAsync(
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
httpResponse.writeHead(400, { Connection: "close" });
const cacheSnapshot = await localCache.getCurrentSnapshot();
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
const d = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: httpResponse.connection.remoteAddress
});
},
Date.now(),
e => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return httpResponse.connection.remoteAddress;
return e;
}
);
if (d)
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
httpResponse.end(_body);
@@ -1361,31 +1395,13 @@ async function listenerAsync(
const cacheSnapshot = await localCache.getCurrentSnapshot();
const configContext = {
id: deviceId,
serialNumber: rpc.cpeRequest.deviceId["SerialNumber"],
productClass: rpc.cpeRequest.deviceId["ProductClass"],
oui: rpc.cpeRequest.deviceId["OUI"],
remoteAddress: httpRequest.connection.remoteAddress
};
const sessionTimeout =
rpc.sessionTimeout ||
localCache.getConfig(cacheSnapshot, "cwmp.sessionTimeout", configContext);
const _sessionContext = session.init(
deviceId,
rpc.cwmpVersion,
sessionTimeout
rpc.sessionTimeout
);
_sessionContext.cacheSnapshot = cacheSnapshot;
_sessionContext.configContext = configContext;
_sessionContext.debug = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
configContext
);
_sessionContext.httpRequest = httpRequest;
_sessionContext.httpResponse = httpResponse;
@@ -1434,6 +1450,24 @@ async function listenerAsync(
_sessionContext.new = true;
}
if (!_sessionContext.timeout) {
_sessionContext.timeout = +localCache.getConfig(
cacheSnapshot,
"cwmp.sessionTimeout",
{},
_sessionContext.timestamp,
e => session.configContextCallback(_sessionContext, e)
);
}
_sessionContext.debug = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{},
_sessionContext.timestamp,
e => session.configContextCallback(_sessionContext, e)
);
const authenticated = await authenticate(_sessionContext, bodyStr);
if (!authenticated) return responseUnauthorized(_sessionContext);
+19 -8
View File
@@ -461,10 +461,11 @@ export function getFiles(
}
export function getConfig(
snapshotKey,
key,
context?,
now?
snapshotKey: string,
key: string,
context: {},
now: number,
cb?: (Expression) => Expression
): string | number | boolean | null {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
@@ -488,11 +489,21 @@ export function getConfig(
};
if (!(key in snapshot.config)) {
if (key in oldOpts)
return config.get(oldOpts[key], context ? context.id : null);
else return null;
if (key in oldOpts) {
let id;
if (context && context["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return expression.evaluate(snapshot.config[key], context, now || Date.now());
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
}
export function getConfigExpression(snapshotKey, key): Expression {
+57 -10
View File
@@ -47,7 +47,8 @@ import {
InformRequest,
TransferCompleteRequest,
Operation,
ScriptResult
ScriptResult,
Expression
} from "./types";
const VALID_PARAM_TYPES = new Set([
@@ -105,6 +106,34 @@ function generateRpcId(sessionContext: SessionContext): string {
);
}
export function configContextCallback(
sessionContext: SessionContext,
exp: Expression
): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
let name = exp[1];
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress")
return sessionContext.httpRequest.connection.remoteAddress;
const deviceData = sessionContext.deviceData;
const paths = deviceData.paths;
const path = paths.get(Path.parse(name));
if (path) {
const attrs = deviceData.attributes.get(path, 1);
if (attrs && attrs.value && attrs.value[1]) return attrs.value[1][0];
}
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS")
return sessionContext.httpRequest.connection.remoteAddress;
}
return exp;
}
export async function inform(
sessionContext: SessionContext,
rpcReq: InformRequest
@@ -413,7 +442,9 @@ export async function timeoutOperations(
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadTimeout",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
@@ -448,7 +479,9 @@ export function addProvisions(
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
) * 2;
delete sessionContext.syncState;
@@ -501,7 +534,9 @@ export function clearProvisions(sessionContext: SessionContext): void {
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
) * 2;
if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) {
@@ -1080,7 +1115,9 @@ export async function rpcRequest(
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
) * 2;
if (sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)) {
@@ -1523,7 +1560,9 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
const GPN_NEXT_LEVEL = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpnNextLevel",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
);
const paths = Array.from(syncState.gpn.keys()).sort(
@@ -1582,7 +1621,9 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
);
const parameterNames: string[] = [];
@@ -1643,19 +1684,25 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
);
const DATETIME_MILLISECONDS = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
);
const BOOLEAN_LITERAL = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.booleanLiteral",
sessionContext.configContext
{},
sessionContext.timestamp,
e => configContextCallback(sessionContext, e)
);
const parameterValues: [Path, string | number | boolean, string][] = [];
-1
View File
@@ -131,7 +131,6 @@ export interface SessionContext {
faults?: { [channel: string]: SessionFault };
retries?: { [channel: string]: number };
cacheSnapshot?: string;
configContext?: { [name: string]: any };
httpResponse?: ServerResponse;
httpRequest?: IncomingMessage;
faultsTouched?: { [channel: string]: boolean };
+3 -2
View File
@@ -88,7 +88,8 @@ export async function deleteResource(resource, id): Promise<void> {
export async function postTasks(
deviceId,
tasks,
timeout
timeout,
device
): Promise<{ connectionRequest: string; tasks: any[] }> {
for (const task of tasks) {
delete task._id;
@@ -103,7 +104,7 @@ export async function postTasks(
await del(`${deviceId}_tasks_faults_operations`);
try {
await connectionRequest(deviceId);
await connectionRequest(deviceId, device);
} catch (err) {
return {
connectionRequest: err.message,
+24 -4
View File
@@ -30,6 +30,7 @@ import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache";
import Authorizer from "../common/authorizer";
import { ping } from "../ping";
import * as url from "url";
const router = new Router();
export default router;
@@ -481,8 +482,9 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
}
const devices = await db.query("devices", filter);
if (!devices.length) return void next();
const device = devices[0];
const validate = authorizer.getValidator("devices", devices[0]);
const validate = authorizer.getValidator("devices", device);
for (const t of ctx.request.body) {
if (!validate("task", t)) {
logUnauthorizedWarning(log);
@@ -490,16 +492,34 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
}
}
// TODO Pass context and timestamp
const onlineThreshold = getConfig(
ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold"
"cwmp.deviceOnlineThreshold",
{},
Date.now(),
exp => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = device[exp[1]];
if (p && p.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p && p.value) return url.parse(p.value[0]).host;
}
return null;
}
}
return exp;
}
);
const res = await apiFunctions.postTasks(
ctx.params.id,
ctx.request.body,
onlineThreshold
onlineThreshold,
device
);
log.tasks = res.tasks.map(t => t._id).join(",");