diff --git a/housepanel-push/housepanel-push.js b/housepanel-push/housepanel-push.js index b63d754..fdd38c2 100644 --- a/housepanel-push/housepanel-push.js +++ b/housepanel-push/housepanel-push.js @@ -12,6 +12,7 @@ try { webSocketServer = null; } var http = require('http'); +var https = require('https'); var fs = require('fs'); var crypto = require('crypto'); @@ -122,7 +123,21 @@ function updateElements() { if ( hubs && hubs.length && config && config.housepanel_url ) { console.log('housepanel-push installed. Elements being updated from ', hubs.length,' hubs to ', config.housepanel_url); - var request = require('request'); + // Native http.request is HTTP-only; pick https.request for TLS URLs + // (the dropped `request` library did this automatically). + var urlObj = null; + try { + urlObj = new URL(config.housepanel_url); + if ( urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:' ) { + console.log('unsupported housepanel_url protocol:', urlObj.protocol); + urlObj = null; + } + } catch (urlErr) { + console.log('error parsing housepanel_url:', urlErr.message); + urlObj = null; + } + var isHttps = !!(urlObj && urlObj.protocol === 'https:'); + var requestLib = isHttps ? https : http; var num; // console.log(hubs); for (num= 0; num< hubs.length; num++) { @@ -137,46 +152,67 @@ function updateElements() { numstr = null; } - if ( numstr ) { - var parms = { url:config.housepanel_url, - form:{useajax:'doquery',id:'all',type:'all',value:'none',attr:'none',hubid:numstr}}; - request.post( parms, function (error, response, body) { - if ( error || !response || response.statusCode != 200 ) { - if ( error ) { console.log(error); } - console.log('error attempting to read hub. statusCode:', response ? response.statusCode : 'none'); - return; - } - - var newitems; - try { - newitems = JSON.parse(body); - } catch (parseError) { - console.log('error parsing housepanel doquery response:', parseError.message); - return; - } - if ( !Array.isArray(newitems) ) { - console.log('housepanel doquery response is not an array; skipping.'); - return; - } - - // pop the hub index off the stack since it was put there in doAction - var rawHubnum = newitems.pop(); - var hubnum = Number(rawHubnum); - if ( !Number.isInteger(hubnum) || hubnum < 0 || hubnum >= hubs.length ) { - console.log('Malformed or out-of-range hub index from housepanel doquery; skipping this response.'); - return; - } - - var hub = hubs[hubnum]; - if ( hub && newitems.length ) { - var hubId = hub.hubId; - console.log('success reading', newitems.length,' elements from hub ID:', hubId, - ' hub type: ', hub.hubType, ' hub name: ', hub.hubName); - newitems.forEach( function(item) { - elements.push(item); - }); + if ( numstr && urlObj ) { + var formBody = 'useajax=doquery&id=all&type=all&value=none&attr=none&hubid=' + encodeURIComponent(numstr); + var postReq = requestLib.request({ + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: (urlObj.pathname || '/') + urlObj.search, + method: 'POST', + timeout: 60000, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(formBody) } + }, function (response) { + var body = ''; + response.on('data', function (chunk) { body += chunk; }); + response.on('end', function () { + if ( response.statusCode != 200 ) { + console.log('error attempting to read hub. statusCode:', response.statusCode); + return; + } + + var newitems; + try { + newitems = JSON.parse(body); + } catch (parseError) { + console.log('error parsing housepanel doquery response:', parseError.message); + return; + } + if ( !Array.isArray(newitems) ) { + console.log('housepanel doquery response is not an array; skipping.'); + return; + } + + // pop the hub index off the stack since it was put there in doAction + var rawHubnum = newitems.pop(); + var hubnum = Number(rawHubnum); + if ( !Number.isInteger(hubnum) || hubnum < 0 || hubnum >= hubs.length ) { + console.log('Malformed or out-of-range hub index from housepanel doquery; skipping this response.'); + return; + } + + var hub = hubs[hubnum]; + if ( hub && newitems.length ) { + var hubId = hub.hubId; + console.log('success reading', newitems.length,' elements from hub ID:', hubId, + ' hub type: ', hub.hubType, ' hub name: ', hub.hubName); + newitems.forEach( function(item) { + elements.push(item); + }); + } + }); + }); + postReq.on('error', function (err) { + console.log(err); + console.log('error attempting to read hub. statusCode: none'); + }); + postReq.on('timeout', function () { + postReq.destroy(); }); + postReq.write(formBody); + postReq.end(); } } } else { diff --git a/housepanel-push/housepanel-push.smoke.js b/housepanel-push/housepanel-push.smoke.js index f542230..084ad13 100644 --- a/housepanel-push/housepanel-push.smoke.js +++ b/housepanel-push/housepanel-push.smoke.js @@ -447,7 +447,127 @@ function httpTests() { }); } +// --------------------------------------------------------------------------- +// 9. Native http/https doquery client (replaces the dropped `request` library). +// These call the real updateElements() against a local HTTP server, and stub +// https.request to prove TLS URLs do not go through http.request. +// --------------------------------------------------------------------------- +function nativeDoqueryTests() { + const httpmod = require("http"); + return new Promise(function (resolve, reject) { + const received = []; + const server = httpmod.createServer(function (req, res) { + let data = ""; + req.on("data", function (chunk) { data += chunk; }); + req.on("end", function () { + received.push({ + method: req.method, + url: req.url, + contentType: req.headers["content-type"], + body: data + }); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify([{ id: "t1", value: { on: "off" } }, 0])); + }); + }); + server.listen(0, "127.0.0.1", function () { + const port = server.address().port; + writeCfg({ + housepanel_url: "http://127.0.0.1:" + port + "/housepanel.php?keep=1", + hubs: [{ hubId: "hub-1", hubType: "ST", hubName: "Living" }] + }); + try { + push.updateElements(); + } catch (err) { + server.close(function () { reject(err); }); + return; + } + const deadline = Date.now() + 2000; + (function wait() { + if ( received.length >= 1 ) { + server.close(function () { + try { + assert.strictEqual(received.length, 1, "one hub must produce one doquery POST"); + assert.strictEqual(received[0].method, "POST"); + assert.strictEqual(received[0].url, "/housepanel.php?keep=1", "pathname and query string must be preserved"); + assert.ok((received[0].contentType || "").indexOf("application/x-www-form-urlencoded") >= 0); + assert.ok(received[0].body.indexOf("useajax=doquery") >= 0); + assert.ok(received[0].body.indexOf("id=all") >= 0); + assert.ok(received[0].body.indexOf("type=all") >= 0); + assert.ok(received[0].body.indexOf("value=none") >= 0); + assert.ok(received[0].body.indexOf("attr=none") >= 0); + assert.ok(received[0].body.indexOf("hubid=hub-1") >= 0); + resolve(); + } catch (err) { reject(err); } + }); + return; + } + if ( Date.now() > deadline ) { + server.close(function () { + reject(new Error("timed out waiting for native doquery POST")); + }); + return; + } + setTimeout(wait, 20); + })(); + }); + server.on("error", reject); + }); +} + +function httpsProtocolTest() { + const httpsmod = require("https"); + const orig = httpsmod.request; + let captured = null; + const { EventEmitter } = require("events"); + httpsmod.request = function (opts) { + captured = opts; + const fake = new EventEmitter(); + fake.write = function () { return true; }; + fake.end = function () { + process.nextTick(function () { + fake.emit("error", new Error("stubbed https")); + }); + }; + fake.destroy = function () {}; + return fake; + }; + writeCfg({ + housepanel_url: "https://example.invalid/housepanel.php?keep=1", + hubs: [{ hubId: "hub-1", hubType: "ST", hubName: "Living" }] + }); + try { + assert.doesNotThrow(function () { push.updateElements(); }); + assert.ok(captured, "https.request must be used for https housepanel_url"); + assert.strictEqual(captured.hostname, "example.invalid"); + assert.strictEqual(Number(captured.port), 443, "https default port must be 443"); + assert.strictEqual(captured.path, "/housepanel.php?keep=1"); + assert.strictEqual(captured.method, "POST"); + assert.strictEqual(captured.timeout, 60000); + } finally { + httpsmod.request = orig; + } + return new Promise(function (resolve) { setImmediate(resolve); }); +} + +function invalidUrlTest() { + writeCfg({ + housepanel_url: "not-a-valid-url", + hubs: [{ hubId: "hub-1", hubType: "ST", hubName: "Living" }] + }); + assert.doesNotThrow(function () { push.updateElements(); }, "malformed housepanel_url must not throw"); + writeCfg({ + housepanel_url: "ftp://example.com/housepanel.php", + hubs: [{ hubId: "hub-1", hubType: "ST", hubName: "Living" }] + }); + assert.doesNotThrow(function () { push.updateElements(); }, "non-http housepanel_url must not throw"); + return Promise.resolve(); +} + httpTests() + .then(nativeDoqueryTests) + .then(httpsProtocolTest) + .then(invalidUrlTest) .then(function () { process.chdir(origCwd); fs.rmSync(tmpdir, { recursive: true, force: true }); diff --git a/housepanel-push/package.json b/housepanel-push/package.json index 4d506bb..d457645 100644 --- a/housepanel-push/package.json +++ b/housepanel-push/package.json @@ -6,7 +6,6 @@ "dependencies": { "body-parser": "^1.18.3", "express": "^4.16.4", - "request": "^2.88.0", "websocket": "^1.0.28" }, "devDependencies": {},