Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 75 additions & 39 deletions housepanel-push/housepanel-push.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ try {
webSocketServer = null;
}
var http = require('http');
var https = require('https');
var fs = require('fs');
var crypto = require('crypto');

Expand Down Expand Up @@ -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++) {
Expand All @@ -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 {
Expand Down
120 changes: 120 additions & 0 deletions housepanel-push/housepanel-push.smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
1 change: 0 additions & 1 deletion housepanel-push/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"request": "^2.88.0",
"websocket": "^1.0.28"
},
"devDependencies": {},
Expand Down