diff --git a/README.md b/README.md index 5eb4ed3..b1bbb0d 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,8 @@ Any use of the source code and related documents of this repository in applicati * 2022-03-14: 1.8.15 - fix: make subscribe node useable on other ports than 443 (Bug457112). fix: remove an uncaught exception which was introduced with version 1.8.14 (Bug454078). * 2022-04-26: 1.8.16 - fix: possible connection break on heavy load for commands: create, delete, write. -* 2022-05-28: 1.8.17 - feat: added support for IPv6. +* 2022-05-05: 1.8.17 - feat: added support for IPv6. +* 2022-05-06: 1.8.18 - fix: possible node crash on browsing with bad credentials ## About diff --git a/ctrlx-config-subscription.js b/ctrlx-config-subscription.js index 4025c57..ffff2f9 100644 --- a/ctrlx-config-subscription.js +++ b/ctrlx-config-subscription.js @@ -24,7 +24,7 @@ * */ - const CtrlxProblemError = require('./lib/CtrlxProblemError'); +const CtrlxProblemError = require('./lib/CtrlxProblemError'); module.exports = function(RED) { 'use strict'; diff --git a/ctrlx-config.js b/ctrlx-config.js index 783e059..29b6fae 100644 --- a/ctrlx-config.js +++ b/ctrlx-config.js @@ -38,8 +38,8 @@ module.exports = function(RED) { // https://discourse.nodered.org/t/create-an-admin-configuration-api-https-endpoint/4423/7 // https://discourse.nodered.org/t/accessing-server-side-from-client-side/26022/4 - RED.httpAdmin.get('/ctrlx/browse', function(req, res/*, next*/) { - //console.log(req.query); + RED.httpAdmin.get('/ctrlx/browse', function(req, res) { + // console.log(req.query); const id = req.query.id; const username = req.query.username; @@ -54,9 +54,9 @@ module.exports = function(RED) { let ctrlx = new CtrlxCore(hostname, username, password); ctrlx.logIn() - .then(() => ctrlx.datalayerBrowse(path) ) + .then(() => ctrlx.datalayerBrowse(path)) .then((data) => { - if (!data || !data.value ) { + if (!data || !data.value) { return res.end('[]'); } res.end(JSON.stringify(data.value)); @@ -70,7 +70,10 @@ module.exports = function(RED) { return res.end(err.message); } }) - .finally(() => ctrlx.logOut()); + .finally(() => { + // We have to catch, because this fails for bad credentials + ctrlx.logOut().catch((err) => RED.log.warn('Failed to log out when trying to browse with error ' + err.message)); + }); } else if (id) { @@ -93,7 +96,7 @@ module.exports = function(RED) { return res.end(err.message); } - if (!data || !data.value ) { + if (!data || !data.value) { return res.end('[]'); } @@ -150,127 +153,127 @@ module.exports = function(RED) { this.register = function(ctrlxNode) { node.users[ctrlxNode.id] = ctrlxNode; if (Object.keys(node.users).length === 1) { - node.connect(); + node.connect(); } }; // Unregister of attached ctrlX node. We log out of ctrlX when the last node unregistered. this.deregister = function(ctrlxNode, done) { - delete node.users[ctrlxNode.id]; - if (node.closing) { - return done(); - } - if (Object.keys(node.users).length === 0) { - if (node.ctrlX) { - node.ctrlX.logOut() - .then(() => done()) - .catch(() => done()); - } - } else { - done(); + delete node.users[ctrlxNode.id]; + if (node.closing) { + return done(); + } + if (Object.keys(node.users).length === 0) { + if (node.ctrlX) { + node.ctrlX.logOut() + .then(() => done()) + .catch(() => done()); } + } else { + done(); + } }; // This function performs the login. Will be automatically called as soon as // the first node registers. - this.connect = function () { + this.connect = function() { if (!node.connected && !node.connecting) { node.connecting = true; try { node.ctrlX.logIn() - .then((data) => { - node.connecting = false; - node.connected = true; - - if (node.debug) { - node.log('Successfully logged in to: ' + node.hostname); - node.log('Token will expire at ' + new Date(data.token_expireTime).toLocaleString() + ' local time'); - } - - for (let id in node.users) { - if (Object.prototype.hasOwnProperty.call(node.users, id)) { - node.users[id].setStatus({fill: 'green', shape: 'dot', text: 'authenticated'}); + .then((data) => { + node.connecting = false; + node.connected = true; + + if (node.debug) { + node.log('Successfully logged in to: ' + node.hostname); + node.log('Token will expire at ' + new Date(data.token_expireTime).toLocaleString() + ' local time'); } - } - // Now execute all the pending requests - for (let id in node.pendingRequests) { - if (Object.prototype.hasOwnProperty.call(node.pendingRequests, id)) { + for (let id in node.users) { + if (Object.prototype.hasOwnProperty.call(node.users, id)) { + node.users[id].setStatus({ fill: 'green', shape: 'dot', text: 'authenticated' }); + } + } - switch(node.pendingRequests[id].method) { - case 'READ': { - node.datalayerRead(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); - break; - } - case 'READ_WITH_ARG': { - node.datalayerReadWithArg(id, node.pendingRequests[id].path, node.pendingRequests[id].arg, node.pendingRequests[id].callback); - break; - } - case 'WRITE': { - node.datalayerWrite(id, node.pendingRequests[id].path, node.pendingRequests[id].data, node.pendingRequests[id].callback); - break; - } - case 'CREATE': { - node.datalayerCreate(id, node.pendingRequests[id].path, node.pendingRequests[id].data, node.pendingRequests[id].callback); - break; - } - case 'DELETE': { - node.datalayerDelete(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); - break; - } - case 'METADATA': { - node.datalayerReadMetadata(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); - break; + // Now execute all the pending requests + for (let id in node.pendingRequests) { + if (Object.prototype.hasOwnProperty.call(node.pendingRequests, id)) { + + switch (node.pendingRequests[id].method) { + case 'READ': { + node.datalayerRead(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); + break; + } + case 'READ_WITH_ARG': { + node.datalayerReadWithArg(id, node.pendingRequests[id].path, node.pendingRequests[id].arg, node.pendingRequests[id].callback); + break; + } + case 'WRITE': { + node.datalayerWrite(id, node.pendingRequests[id].path, node.pendingRequests[id].data, node.pendingRequests[id].callback); + break; + } + case 'CREATE': { + node.datalayerCreate(id, node.pendingRequests[id].path, node.pendingRequests[id].data, node.pendingRequests[id].callback); + break; + } + case 'DELETE': { + node.datalayerDelete(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); + break; + } + case 'METADATA': { + node.datalayerReadMetadata(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); + break; + } + case 'BROWSE': { + node.datalayerBrowse(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); + break; + } + case 'SUBSCRIBE': { + node.datalayerSubscribe(id, node.pendingRequests[id].paths, node.pendingRequests[id].publishIntervalMs, node.pendingRequests[id].callback); + break; + } + default: { + node.error('internal error: received invalid pending request!'); + } } - case 'BROWSE': { - node.datalayerBrowse(id, node.pendingRequests[id].path, node.pendingRequests[id].callback); - break; - } - case 'SUBSCRIBE': { - node.datalayerSubscribe(id, node.pendingRequests[id].paths, node.pendingRequests[id].publishIntervalMs, node.pendingRequests[id].callback); - break; - } - default: { - node.error('internal error: received invalid pending request!'); - } - } - delete node.pendingRequests[id]; + delete node.pendingRequests[id]; + } } - } - }) - .catch((err) => { - node.connecting = false; - node.connected = false; + }) + .catch((err) => { + node.connecting = false; + node.connected = false; - if (node.debug) { - node.log('Failed to log in to ' + node.hostname + ' with error ' + err.message); - } + if (node.debug) { + node.log('Failed to log in to ' + node.hostname + ' with error ' + err.message); + } - for (let id in node.users) { - if (Object.prototype.hasOwnProperty.call(node.users, id)) { - node.users[id].setStatus({fill: 'red', shape: 'ring', text: 'authentication failed'}); + for (let id in node.users) { + if (Object.prototype.hasOwnProperty.call(node.users, id)) { + node.users[id].setStatus({ fill: 'red', shape: 'ring', text: 'authentication failed' }); + } } - } - // Now cancel all the pending requests - for (let id in node.pendingRequests) { - if (Object.prototype.hasOwnProperty.call(node.pendingRequests, id)) { - node.pendingRequests[id].callback(err, null); - delete node.pendingRequests[id]; + // Now cancel all the pending requests + for (let id in node.pendingRequests) { + if (Object.prototype.hasOwnProperty.call(node.pendingRequests, id)) { + node.pendingRequests[id].callback(err, null); + delete node.pendingRequests[id]; + } } - } - // Try again, except if the node has been closed in the meanwhile. E.g. because - // the node has been deleted or the flow has been reployed with new settings. - if (!node.closing) { - setTimeout(node.connect, 5000); - } + // Try again, except if the node has been closed in the meanwhile. E.g. because + // the node has been deleted or the flow has been reployed with new settings. + if (!node.closing) { + setTimeout(node.connect, 5000); + } - }); + }); - }catch(err) { + } catch (err) { node.error(err); } } @@ -440,8 +443,8 @@ module.exports = function(RED) { RED.nodes.registerType("ctrlx-config", CtrlxConfig, { credentials: { - username: {type:"text"}, - password: {type:"password"} - } + username: { type: "text" }, + password: { type: "password" } + } }); }; diff --git a/ctrlx-datalayer-request.js b/ctrlx-datalayer-request.js index 7b4b64f..e310f00 100644 --- a/ctrlx-datalayer-request.js +++ b/ctrlx-datalayer-request.js @@ -25,7 +25,7 @@ */ - module.exports = function(RED) { +module.exports = function(RED) { 'use strict'; let mustache = require("mustache"); @@ -69,13 +69,13 @@ if (this.configNode) { - node.status({fill: "red", shape: "ring", text: "not logged in"}); + node.status({ fill: "red", shape: "ring", text: "not logged in" }); // // Input handler // node.on("input", function(msg, send, done) { - node.status({fill: "blue", shape: "dot", text: "requesting"}); + node.status({ fill: "blue", shape: "dot", text: "requesting" }); // Prepare the path let path = node.path || msg.path; @@ -109,11 +109,11 @@ let timeout = node.reqTimeout; if (msg.requestTimeout !== undefined) { if (isNaN(msg.requestTimeout)) { - node.warn("msg.requestTimeout is given as NaN"); + node.warn("msg.requestTimeout is given as NaN"); } else if (msg.requestTimeout < 1) { - node.warn(RED._("msg.requestTimeout is given as negative value")); + node.warn(RED._("msg.requestTimeout is given as negative value")); } else { - timeout = msg.requestTimeout; + timeout = msg.requestTimeout; } } node.configNode.setTimeout(timeout); @@ -130,57 +130,57 @@ // let func = function(err, data) { - if (err) { - if (done) { - done(err); // Node-RED 1.0 compatible - } else { - node.error(err, msg); // Node-RED 0.x compatible - } - node.status({fill: "red", shape: "ring", text: "request failed"}); - node.configNode.logAdditionalDebugErrorInfo(node, err); - return; + if (err) { + if (done) { + done(err); // Node-RED 1.0 compatible + } else { + node.error(err, msg); // Node-RED 0.x compatible } + node.status({ fill: "red", shape: "ring", text: "request failed" }); + node.configNode.logAdditionalDebugErrorInfo(node, err); + return; + } - // For maximum backwards compatibility, check that send exists. - // If this node is installed in Node-RED 0.x, it will need to - // fallback to using `node.send` - send = send || function() { node.send.apply(node, arguments) } + // For maximum backwards compatibility, check that send exists. + // If this node is installed in Node-RED 0.x, it will need to + // fallback to using `node.send` + send = send || function() { node.send.apply(node, arguments) } - // Return only expected output data. Option 'v1' is for backward compatibility to - // deprecated Data Layer API v1. - switch(node.payloadFormat) { - case 'v1': - if (data.type === 'object') { - msg.payload = data.value; - } else { - msg.payload = data; - } - break; - case 'value': + // Return only expected output data. Option 'v1' is for backward compatibility to + // deprecated Data Layer API v1. + switch (node.payloadFormat) { + case 'v1': + if (data.type === 'object') { msg.payload = data.value; - break; - case 'value_type': + } else { msg.payload = data; - break; - } - send(msg); - - // Once finished, call 'done'. - // This call is wrapped in a check that 'done' exists - // so the node will work in earlier versions of Node-RED (<1.0) - if (done) { - done(); - } - - node.status({fill: "green", shape: "dot", text: "request successful"}); + } + break; + case 'value': + msg.payload = data.value; + break; + case 'value_type': + msg.payload = data; + break; } + send(msg); - if (method === 'READ_WITH_ARG') { - node.configNode.datalayerReadWithArg(node, path, msg.payload, func); - } else { - node.configNode.datalayerRead(node, path, func); + // Once finished, call 'done'. + // This call is wrapped in a check that 'done' exists + // so the node will work in earlier versions of Node-RED (<1.0) + if (done) { + done(); } + node.status({ fill: "green", shape: "dot", text: "request successful" }); + } + + if (method === 'READ_WITH_ARG') { + node.configNode.datalayerReadWithArg(node, path, msg.payload, func); + } else { + node.configNode.datalayerRead(node, path, func); + } + } else if (method === 'WRITE') { // @@ -190,7 +190,7 @@ // Return only expected output data. Option 'v1' is for backward compatibility to // deprecated Data Layer API v1. let payload = {}; - switch(node.payloadFormat) { + switch (node.payloadFormat) { case 'v1': if (msg.payload.type === 'undefined') { payload.value = msg.payload; @@ -216,7 +216,7 @@ } else { node.error(err, msg); // Node-RED 0.x compatible } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); node.configNode.logAdditionalDebugErrorInfo(node, err); return; } @@ -228,7 +228,7 @@ if (done) { done(); } - node.status({fill: "green", shape: "dot", text: "request successful"}); + node.status({ fill: "green", shape: "dot", text: "request successful" }); }); } else if (method === 'CREATE') { @@ -245,7 +245,7 @@ } else { node.error(err, msg); // Node-RED 0.x compatible } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); node.configNode.logAdditionalDebugErrorInfo(node, err); return; } @@ -254,7 +254,7 @@ // Return only expected output data. Option 'v1' is for backward compatibility to // deprecated Data Layer API v1. - switch(node.payloadFormat) { + switch (node.payloadFormat) { case 'v1': if (data.type === 'object') { msg.payload = data.value; @@ -274,7 +274,7 @@ if (done) { done(); } - node.status({fill: "green", shape: "dot", text: "request successful"}); + node.status({ fill: "green", shape: "dot", text: "request successful" }); }); } else if (method === 'DELETE') { @@ -291,7 +291,7 @@ } else { node.error(err, msg); // Node-RED 0.x compatible } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); node.configNode.logAdditionalDebugErrorInfo(node, err); return; } @@ -304,7 +304,7 @@ done(); } - node.status({fill: "green", shape: "dot", text: "request successful"}); + node.status({ fill: "green", shape: "dot", text: "request successful" }); }); } else if (method === 'METADATA') { @@ -320,7 +320,7 @@ } else { node.error(err, msg); // Node-RED 0.x compatible } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); node.configNode.logAdditionalDebugErrorInfo(node, err); return; } @@ -334,10 +334,10 @@ done(); } - node.status({fill: "green", shape: "dot", text: "request successful"}); - }); + node.status({ fill: "green", shape: "dot", text: "request successful" }); + }); - }else if (method === 'BROWSE') { + } else if (method === 'BROWSE') { // // BROWSE // @@ -350,7 +350,7 @@ } else { node.error(err, msg); // Node-RED 0.x compatible } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); node.configNode.logAdditionalDebugErrorInfo(node, err); return; } @@ -359,7 +359,7 @@ // Return only expected output data. Option 'v1' is for backward compatibility to // deprecated Data Layer API v1. - switch(node.payloadFormat) { + switch (node.payloadFormat) { case 'v1': msg.payload = data; break; @@ -376,16 +376,16 @@ done(); } - node.status({fill: "green", shape: "dot", text: "request successful"}); - }); + node.status({ fill: "green", shape: "dot", text: "request successful" }); + }); - }else { + } else { if (done) { done('Method property of node unknown or not implemented:' + node.method); } else { node.error('Method property of node unknown or not implemented:' + node.method, msg); } - node.status({fill: "red", shape: "ring", text: "request failed"}); + node.status({ fill: "red", shape: "ring", text: "request failed" }); } }); @@ -402,7 +402,7 @@ // Register this node at the config node to receive updates on state change. The config node also // provides all functionality, that is used in the handlers above. if (this.configNode.connected) { - node.status({fill:"green", shape:"dot", text:"authenticated"}); + node.status({ fill: "green", shape: "dot", text: "authenticated" }); } node.configNode.register(node); diff --git a/ctrlx-datalayer-subscribe.js b/ctrlx-datalayer-subscribe.js index 68981fe..e3fe4f1 100644 --- a/ctrlx-datalayer-subscribe.js +++ b/ctrlx-datalayer-subscribe.js @@ -27,7 +27,7 @@ const CtrlxProblemError = require('./lib/CtrlxProblemError'); - module.exports = function(RED) { +module.exports = function(RED) { 'use strict'; const CtrlxDatalayerSubscription = require('./lib/CtrlxDatalayerSubscription'); @@ -56,10 +56,10 @@ const CtrlxProblemError = require('./lib/CtrlxProblemError'); if (this.configSubscription) { - node.status({fill: 'red', shape: 'ring', text: 'not logged in'}); + node.status({ fill: 'red', shape: 'ring', text: 'not logged in' }); if (this.configSubscription.configNodeDevice.connected) { - node.status({fill:'green', shape:'dot', text:'authenticated'}); + node.status({ fill: 'green', shape: 'dot', text: 'authenticated' }); } @@ -70,15 +70,15 @@ const CtrlxProblemError = require('./lib/CtrlxProblemError'); if (err) { if (err.message) { - node.status({fill: 'red', shape: 'ring', text: `subscription failed: ${err.message}`}); + node.status({ fill: 'red', shape: 'ring', text: `subscription failed: ${err.message}` }); node.error(err.message); } else { - node.status({fill: 'red', shape: 'ring', text: `subscription failed`}); + node.status({ fill: 'red', shape: 'ring', text: `subscription failed` }); node.error('unknown error'); } } else { node.eventCounter++; - node.status({fill: 'green', shape: 'dot', text: `received data #${node.eventCounter}`}); + node.status({ fill: 'green', shape: 'dot', text: `received data #${node.eventCounter}` }); node.send({ topic: data.node, payload: data.value, diff --git a/lib/CtrlxCore.js b/lib/CtrlxCore.js index e2cf2f5..4e85b34 100644 --- a/lib/CtrlxCore.js +++ b/lib/CtrlxCore.js @@ -121,6 +121,19 @@ class CtrlxCore { * Private Methods * -------------------------------------------------------------------------*/ + /** + * This is a helper function to remove trailing slashes from a hostname. + * e.g.: + * - https://myhostname.de/ --> https://myhostname.de + * + * @static + * @param {string} hostname - The hostname. + * @returns Returns the hostname without trailing slash. + * @memberof CtrlxCore + */ + static _removeTrailingSlash(hostname) { + return hostname.replace(/\/+$/, ''); + } /** @@ -140,12 +153,21 @@ class CtrlxCore { */ static _parseHost(host) { - // Plain IPv4 or IPv6 (without port)? + // Check for 'localhost' (invariant case) + if (host.toLowerCase() === 'localhost') { + // just forward 'localhost' as hostname and default to https + return { + 'hostname': 'localhost', + 'port': 443 + } + } + + // Check for IPv4 or IPv6 (without port) let ipVersion = net.isIP(host); // IPv4 if (ipVersion === 4) { - // just forward the host as hostname and default to https + // just forward the IPv4 as hostname and default to https return { 'hostname': host, 'port': 443 @@ -154,13 +176,16 @@ class CtrlxCore { // IPv6 if (ipVersion === 6) { - // just forward the host as hostname and default to https + // just forward the IPv6 as hostname and default to https return { 'hostname': host.replace(/\[|\]/gi, ''), // Remove square brackets for IPv6 'port': 443 } } + // Remove trailing slashes + host = CtrlxCore._removeTrailingSlash(host); + // Use a regular expression with 2 capture groups (hostname & port) to match let reg = /([a-z0-9\-._~%]+|\[[a-z0-9\-._~%!$&'()*+,;=:]+\])(?::([0-9]+))?$/igm; let m = reg.exec(host); @@ -177,9 +202,9 @@ class CtrlxCore { let hostname = m[1] || host; // Check for IPv6 - let possibleIPv6 = hostname.replace(/\[|\]/gi, ''); + let possibleIPv6 = hostname.replace(/\[|\]/gi, ''); // Remove square brackets for IPv6 if (net.isIPv6(possibleIPv6)) { - hostname = possibleIPv6; // Remove square brackets for IPv6 + hostname = possibleIPv6; } return { diff --git a/lib/CtrlxProblemError.js b/lib/CtrlxProblemError.js index b8b4f98..4967d92 100644 --- a/lib/CtrlxProblemError.js +++ b/lib/CtrlxProblemError.js @@ -76,6 +76,21 @@ class CtrlxProblemError extends Error { return ctrlXProblemError; } + /** + * A static factory method to construct a CtrlxProblemError from an empty http response. + * + * @static + * @param {object} response - The empty http response. + * @returns {CtrlxProblemError} + * @memberof CtrlxProblemError + */ + static fromEmptyHttpResponse(response) { + let ctrlXProblemError = new CtrlxProblemError(`[${response.statusCode}] ${response.statusMessage}`, 'about:blank'); + ctrlXProblemError._status = response.statusCode; + + return ctrlXProblemError; + } + /** * A static factory method to construct a CtrlxProblemError from a http request response. @@ -88,6 +103,11 @@ class CtrlxProblemError extends Error { */ static fromHttpResponse(response, data) { + // If no additional data (body) available + if (!data) { + return CtrlxProblemError.fromEmptyHttpResponse(response); + } + try { // Try to parse the server response and use the provided data to fill the // error class. @@ -110,10 +130,7 @@ class CtrlxProblemError extends Error { // We could not parse the problem returned in the body of the response. Let's use as much information, that // we have to generate an error object. - let ctrlXProblemError = new CtrlxProblemError(`[${response.statusCode}] ${response.statusMessage}`, 'about:blank'); - ctrlXProblemError._status = response.statusCode; - - return ctrlXProblemError; + return CtrlxProblemError.fromEmptyHttpResponse(response); } } diff --git a/package.json b/package.json index 84e06c0..bb43fc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-ctrlx-automation", - "version": "1.8.17", + "version": "1.8.18", "description": "Node-RED nodes for ctrlX AUTOMATION", "repository": { "type": "git", diff --git a/test/CtrlxCore.test.js b/test/CtrlxCore.test.js index 4057f7c..bb0a4af 100644 --- a/test/CtrlxCore.test.js +++ b/test/CtrlxCore.test.js @@ -79,12 +79,24 @@ describe('CtrlxCore', function() { expect(CtrlxCore._parseHost('[::1]')).to.deep.equal({ 'hostname': '::1', 'port': 443 }) expect(CtrlxCore._parseHost('[::1]:8443')).to.deep.equal({ 'hostname': '::1', 'port': 8443 }) - // domain + // domain: localhost expect(CtrlxCore._parseHost('localhost')).to.deep.equal({ 'hostname': 'localhost', 'port': 443 }) + expect(CtrlxCore._parseHost('LOCALHOST')).to.deep.equal({ 'hostname': 'localhost', 'port': 443 }) + expect(CtrlxCore._parseHost('LoCaLhOsT')).to.deep.equal({ 'hostname': 'localhost', 'port': 443 }) + + // domain: localhost:port expect(CtrlxCore._parseHost('localhost:443')).to.deep.equal({ 'hostname': 'localhost', 'port': 443 }) expect(CtrlxCore._parseHost('localhost:8443')).to.deep.equal({ 'hostname': 'localhost', 'port': 8443 }) - expect(CtrlxCore._parseHost('ctrlx-server.com:8443')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 8443 }) + + // domain: other expect(CtrlxCore._parseHost('ctrlx-server.com')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 443 }) + expect(CtrlxCore._parseHost('ctrlx-server.com:8443')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 8443 }) + + // domain with trailing slash(es) + expect(CtrlxCore._parseHost('ctrlx-server.com/')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 443 }) + expect(CtrlxCore._parseHost('ctrlx-server.com//')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 443 }) + expect(CtrlxCore._parseHost('ctrlx-server.com///')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 443 }) + expect(CtrlxCore._parseHost('https://ctrlx-server.com/')).to.deep.equal({ 'hostname': 'ctrlx-server.com', 'port': 443 }) done(); });