From 79ede00ef2164b0929504f2704b60ef4f939873d Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Fri, 6 Nov 2020 17:14:42 -0800 Subject: [PATCH] Implement XMPP connection request --- lib/api-functions.ts | 49 ++++++++++++++++------ lib/config.ts | 3 ++ lib/connection-request.ts | 88 +++++++++++++++++++++++++++++++++++++++ lib/debug.ts | 34 +++++++++++++++ lib/xmpp-client.ts | 13 +++--- 5 files changed, 170 insertions(+), 17 deletions(-) diff --git a/lib/api-functions.ts b/lib/api-functions.ts index df8a414..7ba8350 100644 --- a/lib/api-functions.ts +++ b/lib/api-functions.ts @@ -29,10 +29,14 @@ import { import { httpConnectionRequest, udpConnectionRequest, + xmppConnectionRequest, } from "./connection-request"; import { Expression, Task } from "./types"; import { flattenDevice } from "./mongodb-functions"; import { evaluate } from "./common/expression"; +import * as config from "../lib/config"; + +const XMPP_CONFIGURED = !!config.get("XMPP_JID"); export async function connectionRequest( deviceId: string, @@ -44,7 +48,11 @@ export async function connectionRequest( device = flattenDevice(res); } - let connectionRequestUrl, udpConnectionRequestAddress, username, password; + let connectionRequestUrl, + udpConnectionRequestAddress, + connReqJabberId, + username, + password; if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) { connectionRequestUrl = (device[ @@ -55,6 +63,9 @@ export async function connectionRequest( "InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress" ] || {} ).value || [""])[0]; + connReqJabberId = (( + device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {} + ).value || [""])[0]; username = (( device[ "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername" @@ -72,6 +83,8 @@ export async function connectionRequest( udpConnectionRequestAddress = (( device["Device.ManagementServer.UDPConnectionRequestAddress"] || {} ).value || [""])[0]; + connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {}) + .value || [""])[0]; username = (( device["Device.ManagementServer.ConnectionRequestUsername"] || {} ).value || [""])[0]; @@ -156,18 +169,30 @@ export async function connectionRequest( ); } - const status = await httpConnectionRequest( - connectionRequestUrl, - authExp, - CONNECTION_REQUEST_ALLOW_BASIC_AUTH, - CONNECTION_REQUEST_TIMEOUT, - debug, - deviceId - ); + let status; - if (udpProm) { - await udpProm; - return ""; + if (connReqJabberId && XMPP_CONFIGURED) { + status = await xmppConnectionRequest( + connReqJabberId, + authExp, + CONNECTION_REQUEST_TIMEOUT, + debug, + deviceId + ); + } else { + status = await httpConnectionRequest( + connectionRequestUrl, + authExp, + CONNECTION_REQUEST_ALLOW_BASIC_AUTH, + CONNECTION_REQUEST_TIMEOUT, + debug, + deviceId + ); + + if (udpProm) { + await udpProm; + return ""; + } } return status; diff --git a/lib/config.ts b/lib/config.ts index 3186f4e..55903d1 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -101,6 +101,9 @@ const options = { // Should probably never be changed DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }, + + XMPP_JID: { type: "string", default: "" }, + XMPP_PASSWORD: { type: "string", default: "" }, }; const allConfig: { [name: string]: string | number } = {}; diff --git a/lib/connection-request.ts b/lib/connection-request.ts index b3b4e15..92a6a37 100644 --- a/lib/connection-request.ts +++ b/lib/connection-request.ts @@ -26,6 +26,10 @@ import { Expression } from "./types"; import * as auth from "./auth"; import * as extensions from "./extensions"; import * as debug from "./debug"; +import XmppClient from "./xmpp-client"; +import * as config from "../lib/config"; +import { encodeEntities, parseAttrs, Element } from "./xml-parser"; +import * as logger from "../lib/logger"; async function extractAuth( exp: Expression, @@ -234,3 +238,87 @@ export async function udpConnectionRequest( } client.close(); } + +const XMPP_JID = config.get("XMPP_JID") as string; +const XMPP_PASSWORD = config.get("XMPP_PASSWORD") as string; +const XMPP_RESOURCE = crypto.randomBytes(8).toString("hex"); + +let xmppClient: XmppClient; +let xmppclientTimeout: NodeJS.Timeout; + +function xmppClientOnError(err: Error): void { + clearTimeout(xmppclientTimeout); + xmppClient.disconnect(); + xmppClient = null; + logger.error({ + message: "XMPP exception", + exception: err, + pid: process.pid, + }); +} + +function xmppClientTimeoutCallback(): void { + xmppClient.disconnect(); + xmppClient = null; +} + +export async function xmppConnectionRequest( + jid: string, + authExp: Expression, + timeout: number, + _debug: boolean, + deviceId: string +): Promise { + if (!xmppClient) { + const [host, username] = XMPP_JID.split("@").reverse(); + xmppClient = await XmppClient.connect({ + host, + username, + resource: XMPP_RESOURCE, + password: XMPP_PASSWORD, + }); + xmppClient.on("error", xmppClientOnError); + } + clearTimeout(xmppclientTimeout); + setTimeout(xmppClientTimeoutCallback, 120000); + + let username: string; + let password: string; + + [username, password, authExp] = await extractAuth(authExp, null); + while (username != null && password != null) { + const msg = `${encodeEntities( + username + )}${encodeEntities( + password + )}`; + let res: Element, rawRes: string, rawReq: string; + try { + ({ res, rawRes, rawReq } = await xmppClient.sendIqStanza( + XMPP_JID, + jid, + "get", + msg, + timeout + )); + } catch (err) { + return err.message; + } + if (_debug) { + debug.outgoingXmppStanza(deviceId, rawReq); + debug.incomingXmppStanza(deviceId, rawRes); + } + const attrs = parseAttrs(res.attrs); + const type = attrs.find((a) => a.name === "type"); + if (type && type.value === "result") return ""; + const error = res.children.find((c) => c.name === "error"); + if (!error || !error.children[0]) + return "Unexpected XMPP connection request response"; + if (error.children[0].name === "service-unavailable") + return "Device is offline"; + if (error.children[0].name !== "not-authorized") + return "Unexpected XMPP connection request response"; + [username, password, authExp] = await extractAuth(authExp, null); + } + return "Incorrect connection request credentials"; +} diff --git a/lib/debug.ts b/lib/debug.ts index 1801000..521ebe1 100644 --- a/lib/debug.ts +++ b/lib/debug.ts @@ -202,3 +202,37 @@ export function outgoingUdpMessage( appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); } + +export function outgoingXmppStanza(deviceId: string, body: string): void { + if (!DEBUG_FILE) return; + const now = new Date(); + const msg = { + event: "outgoing XMPP stanza", + timestamp: now, + deviceId: deviceId, + body: body, + }; + + if (DEBUG_FORMAT === "yaml") + appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + else if (DEBUG_FORMAT === "json") + appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); + else throw new Error(`Unrecognized DEBUG_FORMAT option`); +} + +export function incomingXmppStanza(deviceId: string, body: string): void { + if (!DEBUG_FILE) return; + const now = new Date(); + const msg = { + event: "incoming XMPP stanza", + timestamp: now, + deviceId: deviceId, + body: body, + }; + + if (DEBUG_FORMAT === "yaml") + appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + else if (DEBUG_FORMAT === "json") + appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); + else throw new Error(`Unrecognized DEBUG_FORMAT option`); +} diff --git a/lib/xmpp-client.ts b/lib/xmpp-client.ts index 876240d..81ca265 100644 --- a/lib/xmpp-client.ts +++ b/lib/xmpp-client.ts @@ -295,14 +295,14 @@ interface XmppClientOptions { resource?: string; } -export class XmppClient extends EventEmitter { +export default class XmppClient extends EventEmitter { private _socket: net.Socket; private _host: string; private _username: string; private _resource: string; private _iqStanzaCallbacks: Map< string, - (r: { rawRes: string; res: Element }) => void + (err: Error, r?: { rawRes: string; res: Element }) => void >; private constructor() { @@ -384,7 +384,7 @@ export class XmppClient extends EventEmitter { const id = attrs.find((a) => a.name === "id"); if (id) { const cb = this._iqStanzaCallbacks.get(id.value); - if (cb) cb({ rawRes: s, res: c }); + if (cb) cb(null, { rawRes: s, res: c }); } } this.emit("stanza", c, s); @@ -397,6 +397,7 @@ export class XmppClient extends EventEmitter { private _onError(err: Error): void { this._socket.end(); this.emit("error", err); + for (const cb of this._iqStanzaCallbacks.values()) cb(err); } send(msg: string): void { @@ -420,9 +421,11 @@ export class XmppClient extends EventEmitter { new Error("Did not receive IQ stanza response in a timely manner") ); }, timeout); - this._iqStanzaCallbacks.set(id, (r) => { + this._iqStanzaCallbacks.set(id, (err, r) => { + this._iqStanzaCallbacks.delete(id); clearTimeout(t); - resolve(Object.assign(r, { rawReq })); + if (err) reject(err); + else resolve(Object.assign(r, { rawReq })); }); }); }