diff --git a/lib/api-functions.ts b/lib/api-functions.ts index 9c26a0a..4a486c0 100644 --- a/lib/api-functions.ts +++ b/lib/api-functions.ts @@ -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 { - 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 - } - }; - - const device = await db.devicesCollection.findOne({ _id: deviceId }, options); - if (!device) throw new Error("No such device"); - - 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; +export async function connectionRequest( + deviceId: string, + device?: {} +): Promise { + if (!device) { + const res = await db.devicesCollection.findOne({ _id: deviceId }); + if (!res) throw new Error("No such device"); + device = flattenDevice(res); } - 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 || "" + let connectionRequestUrl, udpConnectionRequestAddress, username, password; + + 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]; + } + + 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 { 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 { ] 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 { await httpConnectionRequest( connectionRequestUrl, authExp, - context, CONNECTION_REQUEST_ALLOW_BASIC_AUTH, CONNECTION_REQUEST_TIMEOUT, debug, diff --git a/lib/auth.ts b/lib/auth.ts index 32bb40f..6489303 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -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)) { diff --git a/lib/connection-request.ts b/lib/connection-request.ts index dfd9439..a7bb666 100644 --- a/lib/connection-request.ts +++ b/lib/connection-request.ts @@ -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 => { 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 { - 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(); } diff --git a/lib/cwmp.ts b/lib/cwmp.ts index 8206bb8..8362b46 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -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 => { + 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 { 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 { 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", { - remoteAddress: httpResponse.connection.remoteAddress - }); + 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", { - remoteAddress: httpResponse.connection.remoteAddress - }); + 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", { - remoteAddress: httpResponse.connection.remoteAddress - }); + 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); diff --git a/lib/local-cache.ts b/lib/local-cache.ts index b987556..783db79 100644 --- a/lib/local-cache.ts +++ b/lib/local-cache.ts @@ -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 config.get(oldOpts[key], id); + } + return null; } - return expression.evaluate(snapshot.config[key], context, now || Date.now()); + + const v = expression.evaluate(snapshot.config[key], context, now, cb); + return Array.isArray(v) ? null : v; } export function getConfigExpression(snapshotKey, key): Expression { diff --git a/lib/session.ts b/lib/session.ts index 0b0d41b..e1f5ce3 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -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][] = []; diff --git a/lib/types.ts b/lib/types.ts index e743139..ff2c3d8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -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 }; diff --git a/lib/ui/api-functions.ts b/lib/ui/api-functions.ts index 8fb3fbb..0ee5a87 100644 --- a/lib/ui/api-functions.ts +++ b/lib/ui/api-functions.ts @@ -88,7 +88,8 @@ export async function deleteResource(resource, id): Promise { 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, diff --git a/lib/ui/api.ts b/lib/ui/api.ts index c452840..291ec51 100644 --- a/lib/ui/api.ts +++ b/lib/ui/api.ts @@ -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(",");