diff --git a/CHANGELOG.md b/CHANGELOG.md index d20a24d..e177d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog -## 0.6.0 - released 2019-10-17 +## 0.7.0 - released 2019-01-22 + +* More complete input validation for arguments to cy.ntlm command and better error reporting +* Corrected docs regarding domain and workstation arguments +* More unit tests + +## 0.6.0 - released 2019-01-17 * Fixed issue #11 - Requests other then GET are not properly send * Improved examples in README for Windows users @@ -8,7 +14,7 @@ * Validation that NTLM handshake is fully complete * The Chrome browser sends three odd requests during startup to detect network behavior. These were logged as errors since they are connecting to non-existent hosts. Those errors are now filtered with understandable debug messages. -## 0.5.0 - released 2019-10-10 +## 0.5.0 - released 2019-01-10 * Changed termination handling for common handling also on Windows. This means that the ntlm-proxy is no longer terminated from the signals when cypress exits - instead a separate binary ntlm-proxy-exit is provided that will send the quit command to the ntlm-proxy. This can then be executed directly after cypress exits, see updated README. * Improved handling of hosts on standard ports (80/443) diff --git a/README.md b/README.md index 41fcbf3..968a173 100644 --- a/README.md +++ b/README.md @@ -174,13 +174,11 @@ The ntlm command is used to configure host/user mappings. After this command, al cy.ntlm(ntlmHost, username, password, domain, [workstation]); ``` -* ntlmHost: protocol, hostname (and port if required) of the server where NTLM authentication shall be applied. This must NOT include the rest of the url (path and query). Examples: `http://localhost:4200`, `https://ntlm.acme.com` +* ntlmHost: protocol, hostname (and port if required) of the server where NTLM authentication shall be applied. This must NOT include the rest of the url (path and query) - only host level authentication is supported. Examples: `http://localhost:4200`, `https://ntlm.acme.com` * username: the username for the account to authenticate with * password: the password for the account to authenticate with (see [Security advice](#Security-advice) regarding entering passwords) -* domain: the domain for the account to authenticate with (for AD account authentication) -* workstation: the workstation for the account to authenticate with (for local machine account authentication) - -The arguments domain and workstation are mutually exclusive. For AD account authentication, set the domain argument but don't set the workstation argument (null or empty string are accepted). For local machine account authentication, set the workstation argument but don't set the domain argument (null or empty string are accepted). If both arguments are set, this command will return an error. +* domain (optional): the domain for the account to authenticate with (for AD account authentication) +* workstation (optional): the workstation name of the client The ntlm command may be called multiple times to setup multiple ntlmHosts, also with different credentials. If the ntlm command is called with the same ntlmHost again, it overwrites the credentials for that ntlmHost. diff --git a/package-lock.json b/package-lock.json index d57e98c..00c4ae8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cypress-ntlm-auth", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -75,9 +75,9 @@ "dev": true }, "ajv": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", - "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -360,9 +360,9 @@ "dev": true }, "eslint": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.0.tgz", - "integrity": "sha512-LntwyPxtOHrsJdcSwyQKVtHofPHdv+4+mFwEe91r2V13vqpM8yLr7b1sW+Oo/yheOPkWYsYlYJCkzlFAt8KV7g==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.1.tgz", + "integrity": "sha512-54NV+JkTpTu0d8+UYSA8mMKAG4XAsaOrozA9rCW7tgneg1mevcL7wIotPC+fZ0SkWwdhNqoXoxnQCTBp7UvTsg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -532,6 +532,24 @@ "vary": "~1.1.2" } }, + "express-ntlm": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/express-ntlm/-/express-ntlm-2.3.0.tgz", + "integrity": "sha512-CKn6OuIsAe3g057A4tX6+arJPWS5uztMpPv9I4L9rOjr2sHYTTchzVhvDyx8850g1enPmwszcYhBjgdGU+DY6g==", + "dev": true, + "requires": { + "async": "^0.9.0", + "underscore": "^1.7.0" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + } + } + }, "external-editor": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", @@ -669,9 +687,9 @@ } }, "globals": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", - "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", + "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", "dev": true }, "graceful-fs": { @@ -1416,17 +1434,17 @@ "dev": true }, "sinon": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", - "integrity": "sha512-WLagdMHiEsrRmee3jr6IIDntOF4kbI6N2pfbi8wkv50qaUQcBglkzkjtoOEbeJ2vf1EsrHhLI+5Ny8//WHdMoA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.3.tgz", + "integrity": "sha512-i6j7sqcLEqTYqUcMV327waI745VASvYuSuQMCjbAwlpAeuCgKZ3LtrjDxAbu+GjNQR0FEDpywtwGCIh8GicNyg==", "dev": true, "requires": { - "@sinonjs/commons": "^1.2.0", + "@sinonjs/commons": "^1.3.0", "@sinonjs/formatio": "^3.1.0", "@sinonjs/samsam": "^3.0.2", "diff": "^3.5.0", "lolex": "^3.0.0", - "nise": "^1.4.7", + "nise": "^1.4.8", "supports-color": "^5.5.0" }, "dependencies": { @@ -1498,9 +1516,9 @@ } }, "table": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", - "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.2.1.tgz", + "integrity": "sha512-qmhNs2GEHNqY5fd2Mo+8N1r2sw/rvTAAvBZTaTx+Y7PHLypqyrxr1MdIu0pLw6Xvl/Gi4ONu/sdceP8vvUjkyA==", "dev": true, "requires": { "ajv": "^6.6.1", @@ -1511,7 +1529,7 @@ }, "text-encoding": { "version": "0.6.4", - "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", "dev": true }, diff --git a/package.json b/package.json index 17ab332..22ba336 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress-ntlm-auth", - "version": "0.6.0", + "version": "0.7.0", "description": "NTLM authentication plugin for Cypress", "main": "index.js", "scripts": { @@ -43,10 +43,11 @@ "node-cleanup": "^2.1.2" }, "devDependencies": { - "eslint": "^5.12.0", + "eslint": "^5.12.1", + "express-ntlm": "^2.3.0", "is-port-reachable": "^2.0.0", "mocha": "^5.2.0", "proxyquire": "^2.1.0", - "sinon": "^7.2.2" + "sinon": "^7.2.3" } } diff --git a/src/commands/index.d.ts b/src/commands/index.d.ts new file mode 100644 index 0000000..03a0c57 --- /dev/null +++ b/src/commands/index.d.ts @@ -0,0 +1,25 @@ +/// + +declare namespace Cypress { + interface Chainable { + /** + * Adds NTLM authentication support to Cypress for a specific host. + * You can call this multiple times to register several hosts or + * change credentials. + * @example + ```js + cy.ntlm('https://ntlm.acme.com', 'TheUser', 'ThePassword', 'TheDomain'); + ``` + */ + ntlm(ntlmHost: string, username: string, password: string, domain?: string, workstation?: string): Chainable + + /** + * Reset NTLM authentication for all configured hosts. Recommended before/after tests. + * @example + ```js + cy.ntlmReset(); + ``` + */ + ntlmReset(): Chainable + } +} diff --git a/src/commands/index.js b/src/commands/index.js index 96a5e5b..9d3277a 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -2,17 +2,15 @@ /// +const configValidator = require('../util/configValidator'); /** * Adds NTLM authentication support to Cypress for a specific host. - * + * You can call this multiple times to register several hosts or + * change credentials. * @example ```js - // Enable NTLM auth for a specific host. You can call this multiple times - // to register several hosts or change credentials. cy.ntlm('https://ntlm.acme.com', 'TheUser', 'ThePassword', 'TheDomain'); - cy.visit('/'); - // Tests ... ``` */ const ntlm = (ntlmHost, username, password, domain, workstation) => { @@ -27,18 +25,23 @@ const ntlm = (ntlmHost, username, password, domain, workstation) => { throw new Error('The cypress-ntlm-auth plugin must be loaded before using this method'); } - let result; + let ntlmConfig = { + ntlmHost: ntlmHost, + username: username, + password: password, + domain: domain, + workstation: workstation + }; + let validationResult = configValidator.validate(ntlmConfig); + if (!validationResult.result) { + throw new Error(validationResult.message); + } + let result; cy.request({ method: 'POST', url: ntlmConfigApi + '/ntlm-config', - body: { - ntlmHost: ntlmHost, - username: username, - password: password, - domain: domain, - workstation: workstation - }, + body: ntlmConfig, log: false // This isn't communication with the test object, so don't show it in the test log }).then((resp) => { if (resp.status === 200) { @@ -64,10 +67,8 @@ const ntlm = (ntlmHost, username, password, domain, workstation) => { /** * Reset NTLM authentication for all configured hosts. Recommended before/after tests. - * * @example ```js - // Disables NTLM auth for all configured hosts. cy.ntlmReset(); ``` */ diff --git a/src/proxy/server.js b/src/proxy/server.js index 7810e41..6afffc5 100644 --- a/src/proxy/server.js +++ b/src/proxy/server.js @@ -13,6 +13,7 @@ const url = require('url'); const debug = require('debug')('cypress:plugin:ntlm-auth'); const portsFile = require('../util/portsFile'); +const configValidator = require('../util/configValidator'); let _ntlmHosts = {}; let _ntlmProxy; @@ -69,21 +70,6 @@ function updateConfig(config) { _ntlmHosts[targetHost] = hostConfig; } -function validateConfig(config) { - if (!config.ntlmHost || - !config.username || - !config.password || - !(config.domain || config.workstation)) { - return { ok: false, message: 'Incomplete configuration. ntlmHost, username, password and either domain or workstation are required fields.' }; - } - - let urlTest = url.parse(config.ntlmHost); - if (!urlTest.hostname || !urlTest.protocol || !urlTest.slashes) { - return { ok: false, message: 'Invalid ntlmHost, must be a valid URL (like https://www.google.com)' }; - } - - return { ok: true }; -} function shutDownProxy(keepPortsFile, exitProcess) { debug('Shutting down'); @@ -137,7 +123,7 @@ function startConfigApi(callback) { _configApp.use(bodyParser.json()); _configApp.post('/ntlm-config', (req, res) => { - let validateResult = validateConfig(req.body); + let validateResult = configValidator.validate(req.body); if (!validateResult.ok) { res.status(400).send('Config parse error. ' + validateResult.message); } else { diff --git a/src/util/configValidator.js b/src/util/configValidator.js new file mode 100644 index 0000000..a11fa85 --- /dev/null +++ b/src/util/configValidator.js @@ -0,0 +1,64 @@ + +const url = require('url'); + +module.exports = { + validate: function(config) { + if (!config.ntlmHost || + !config.username || + !config.password) { + return { ok: false, message: 'Incomplete configuration. ntlmHost, username and password are required fields.' }; + } + + let urlTest = url.parse(config.ntlmHost); + if (!urlTest.hostname || !urlTest.protocol || !urlTest.slashes) { + return { ok: false, message: 'Invalid ntlmHost, must be a valid URL (like https://www.google.com)' }; + } + if (urlTest.path && urlTest.path !== '' && urlTest.path !== '/') { + return { ok: false, message: 'Invalid ntlmHost, must not contain any path or query (https://www.google.com is ok, https://www.google.com/search is not ok)' }; + } + + if (!validateUsername(config.username)) { + return { ok: false, message: 'Username contains invalid characters or is too long.' }; + } + + if (config.domain && !validateDomainOrWorkstation(config.domain)) { + return { ok: false, message: 'Domain contains invalid characters or is too long.' }; + } + + if (config.workstation && !validateDomainOrWorkstation(config.workstation)) { + return { ok: false, message: 'Workstation contains invalid characters or is too long.' }; + } + + return { ok: true }; + } +}; + +// https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb726984(v=technet.10) +// Max 104 chars, invalid chars: " / \ [ ] : ; | = , + * ? < > +function validateUsername(username) { + if (username.length > 104) { + return false; + } + if (username.includes('"') || username.includes('/') || username.includes('\\') || + username.includes('[') || username.includes(']') || username.includes(':') || + username.includes(';') || username.includes('|') || username.includes('=') || + username.includes(',') || username.includes('+') || username.includes('*') || + username.includes('?') || username.includes('<') || username.includes('>')) { + return false; + } + return true; +} + +// https://support.microsoft.com/sv-se/help/909264/naming-conventions-in-active-directory-for-computers-domains-sites-and +// Max 15 chars, invalid chars: " / \ : | * ? < > +function validateDomainOrWorkstation(domain) { + if (domain.length > 15) { + return false; + } + if (domain.includes('"') || domain.includes('/') || domain.includes('\\') || + domain.includes(':') || domain.includes('|') || domain.includes('*') || + domain.includes('?') || domain.includes('<') || domain.includes('>')) { + return false; + } + return true; +} diff --git a/test/proxy/configApi.spec.js b/test/proxy/configApi.spec.js new file mode 100644 index 0000000..75d7bd6 --- /dev/null +++ b/test/proxy/configApi.spec.js @@ -0,0 +1,104 @@ +const proxyFacade = require('./proxyFacade'); +const sinon = require('sinon'); +const assert = require('assert'); + +const portsFile = require('../../src/util/portsFile'); + +const proxy = require('../../src/proxy/server'); + +describe('Configuration API', () => { + let configApiUrl; + let savePortsFileStub; + let portsFileExistsStub; + + before(function (done) { + portsFileExistsStub = sinon.stub(portsFile, 'exists'); + portsFileExistsStub.returns(false); + savePortsFileStub = sinon.stub(portsFile, 'save'); + savePortsFileStub.callsFake(function (ports, callback) { + return callback(); + }); + + this.timeout(15000); + proxyFacade.initMitmProxy((err) => { + if (err) { + return done(err); + } + proxy.startProxy(null, null, false, (result, err) => { + if (err) { + return done(err); + } + configApiUrl = result.configApiUrl; + return done(); + }); + }); + }); + + after(function (done) { + if (savePortsFileStub) { + savePortsFileStub.restore(); + } + if (portsFileExistsStub) { + portsFileExistsStub.restore(); + } + + proxyFacade.sendQuitCommand(configApiUrl, true, (err) => { + if (err) { + return done(err); + } + configApiUrl = null; + return done(); + }); + }); + + it('ntlm-config should return bad request if the username contains backslash', function (done) { + // Arrange + let hostConfig = { + ntlmHost: 'http://localhost:5000', + username: 'nisse\\nisse', + password: 'dummy', + domain: 'mptest' + }; + + // Act + proxyFacade.sendNtlmConfig(configApiUrl, hostConfig, (res, err) => { + assert.equal(res.statusCode, 400); + assert.equal(res.body, 'Config parse error. Username contains invalid characters or is too long.'); + return done(); + }); + }); + + it('ntlm-config should return bad request if the domain contains backslash', function (done) { + // Arrange + let hostConfig = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'dummy', + domain: 'mptest\\mptest' + }; + + // Act + proxyFacade.sendNtlmConfig(configApiUrl, hostConfig, (res, err) => { + assert.equal(res.statusCode, 400); + assert.equal(res.body, 'Config parse error. Domain contains invalid characters or is too long.'); + return done(); + }); + }); + + it('ntlm-config should return bad request if the ntlmHost includes a path', function (done) { + // Arrange + let hostConfig = { + ntlmHost: 'http://localhost:5000/search', + username: 'nisse', + password: 'dummy', + domain: 'mptest' + }; + + // Act + proxyFacade.sendNtlmConfig(configApiUrl, hostConfig, (res, err) => { + assert.equal(res.statusCode, 400); + assert.equal(res.body, 'Config parse error. Invalid ntlmHost, must not contain any path or query (https://www.google.com is ok, https://www.google.com/search is not ok)'); + return done(); + }); + }); +}); diff --git a/test/proxy/expressServer.js b/test/proxy/expressServer.js new file mode 100644 index 0000000..4aa3d0b --- /dev/null +++ b/test/proxy/expressServer.js @@ -0,0 +1,127 @@ +const http = require('http'); +const https = require('https'); +const express = require('express'); +const ntlm = require('express-ntlm'); +const bodyParser = require('body-parser'); +const fs = require('fs'); +const path = require('path'); + +const appNoAuth = express(); +const appNtlmAuth = express(); + +function initExpress(app, useNtlm) { + app.use(bodyParser.json()); + + app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + }); + + if (useNtlm) { + app.use(ntlm({})); // Enables NTLM without check of user/pass + } + + app.get('/get', (req, res) => { + let body = { + message: 'Expecting larger payload on GET', + reply: 'OK ÅÄÖéß' + }; + res.setHeader('Content-Type', 'application/json'); + res.status(200).send(JSON.stringify(body)); + }); + + app.post('/post', (req, res) => { + if (!req.body || !('ntlmHost' in req.body)) { + res.status(400).send('Invalid body'); + } + + req.body.reply = 'OK ÅÄÖéß'; + res.setHeader('Content-Type', 'application/json'); + res.status(200).send(JSON.stringify(req.body)); + }); + + app.put('/put', (req, res) => { + if (!req.body || !('ntlmHost' in req.body)) { + res.status(400).send('Invalid body'); + } + + req.body.reply = 'OK ÅÄÖéß'; + res.setHeader('Content-Type', 'application/json'); + res.status(200).send(req.body); + }); + + app.delete('/delete', (req, res) => { + if (!req.body || !('ntlmHost' in req.body)) { + res.status(400).send('Invalid body'); + } + + req.body.reply = 'OK ÅÄÖéß'; + res.setHeader('Content-Type', 'application/json'); + res.status(200).send(req.body); + }); +} + +initExpress(appNoAuth, false); +initExpress(appNtlmAuth, true); + +let httpServer; +let httpsServer; + +module.exports = { + startHttpServer: function(useNtlm, callback) { + if (useNtlm) { + httpServer = http.createServer(appNtlmAuth); + } else { + httpServer = http.createServer(appNoAuth); + } + httpServer.listen(0, '127.0.0.1', 511, (err) => { + if (err) { + throw err; + } + let url = 'http://localhost:' + httpServer.address().port; + callback(url); + }); + }, + stopHttpServer: function(callback) { + httpServer.on('close', callback); // Called when all connections have been closed + httpServer.close((err) => { + if (err) { + throw err; + } + }); + }, + startHttpsServer: function(useNtlm, callback) { + let httpMitmProxyDir = path.resolve(process.cwd(), '.http-mitm-proxy'); + if (useNtlm) { + httpsServer = https.createServer({ + key: fs.readFileSync(path.resolve(httpMitmProxyDir, 'keys/localhost.key')), + cert: fs.readFileSync(path.resolve(httpMitmProxyDir, 'certs/localhost.pem')), + ca: fs.readFileSync(path.resolve(httpMitmProxyDir, 'certs/ca.pem')) + }, appNtlmAuth); + } else { + httpsServer = https.createServer({ + key: fs.readFileSync(path.resolve(httpMitmProxyDir, 'keys/localhost.key')), + cert: fs.readFileSync(path.resolve(httpMitmProxyDir, 'certs/localhost.pem')), + ca: fs.readFileSync(path.resolve(httpMitmProxyDir, 'certs/ca.pem')) + }, appNoAuth); + } + httpsServer.listen(0, '127.0.0.1', 511, (err) => { + if (err) { + throw err; + } + let url = 'https://localhost:' + httpsServer.address().port; + callback(url); + }); + }, + stopHttpsServer: function(callback) { + httpsServer.on('close', callback); // Called when all connections have been closed + httpsServer.close((err) => { + if (err) { + throw err; + } + }); + }, +}; diff --git a/test/proxy/http.proxy.spec.js b/test/proxy/http.proxy.spec.js new file mode 100644 index 0000000..c2b64cc --- /dev/null +++ b/test/proxy/http.proxy.spec.js @@ -0,0 +1,225 @@ +const expressServer = require('./expressServer'); +const proxyFacade = require('./proxyFacade'); +const sinon = require('sinon'); +const assert = require('assert'); +const portsFile = require('../../src/util/portsFile'); +const proxy = require('../../src/proxy/server'); + +let configApiUrl; +let ntlmProxyUrl; +let httpUrl; +let savePortsFileStub; +let portsFileExistsStub; + +describe('Proxy for HTTP host with NTLM', function() { + let ntlmHostConfig; + + before('Start HTTP server and proxy', function (done) { + portsFileExistsStub = sinon.stub(portsFile, 'exists'); + portsFileExistsStub.returns(false); + savePortsFileStub = sinon.stub(portsFile, 'save'); + savePortsFileStub.callsFake(function (ports, callback) { + return callback(); + }); + + this.timeout(15000); + proxyFacade.initMitmProxy((err) => { + if (err) { + return done(err); + } + expressServer.startHttpServer(true, (url) => { + httpUrl = url; + ntlmHostConfig = { + ntlmHost: httpUrl, + username: 'nisse', + password: 'manpower', + domain: 'mptst' + }; + proxy.startProxy(null, null, false, (result, err) => { + if (err) { + return done(err); + } + configApiUrl = result.configApiUrl; + ntlmProxyUrl = result.ntlmProxyUrl; + return done(); + }); + }); + }); + }); + + after('Stop HTTP server and proxy', function(done) { + if (savePortsFileStub) { + savePortsFileStub.restore(); + } + if (portsFileExistsStub) { + portsFileExistsStub.restore(); + } + + proxyFacade.sendQuitCommand(configApiUrl, true, (err) => { + if (err) { + return done(err); + } + configApiUrl = null; + ntlmProxyUrl = null; + httpUrl = null; + expressServer.stopHttpServer((err) => { + if (err) { + return done(err); + } + return done(); + }); + }); + }); + + beforeEach('Reset NTLM config', function(done) { + proxyFacade.sendNtlmReset(configApiUrl, (err) => { + if (err) { + return done(err); + } + return done(); + }); + }); + + it('should handle authentication for GET requests', function(done) { + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'GET', '/get', null, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on GET requests', function(done) { + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'GET', '/get', null, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for POST requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'POST', '/post', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on POST requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'POST', '/post', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for PUT requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'PUT', '/put', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on PUT requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'PUT', '/put', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for DELETE requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'DELETE', '/delete', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on DELETE requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpUrl, 'DELETE', '/delete', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); +}); diff --git a/test/proxy/https.proxy.spec.js b/test/proxy/https.proxy.spec.js new file mode 100644 index 0000000..d6bb4db --- /dev/null +++ b/test/proxy/https.proxy.spec.js @@ -0,0 +1,233 @@ +const expressServer = require('./expressServer'); +const proxyFacade = require('./proxyFacade'); +const sinon = require('sinon'); +const assert = require('assert'); +const portsFile = require('../../src/util/portsFile'); +const proxy = require('../../src/proxy/server'); + +let configApiUrl; +let ntlmProxyUrl; +let httpsUrl; +let savePortsFileStub; +let portsFileExistsStub; + +/* further work on cert generation needed +describe('Proxy for HTTPS host with NTLM', function() { + let ntlmHostConfig; + + before('Start HTTPS server and proxy', function (done) { + portsFileExistsStub = sinon.stub(portsFile, 'exists'); + portsFileExistsStub.returns(false); + savePortsFileStub = sinon.stub(portsFile, 'save'); + savePortsFileStub.callsFake(function (ports, callback) { + return callback(); + }); + + this.timeout(15000); + proxyFacade.initMitmProxy((err) => { + if (err) { + return done(err); + } + proxy.startProxy(null, null, false, (result, err) => { + if (err) { + return done(err); + } + configApiUrl = result.configApiUrl; + ntlmProxyUrl = result.ntlmProxyUrl; + // Send request to fake localhost server to trigger cert generation + proxyFacade.sendRemoteRequest(ntlmProxyUrl, 'https://localhost:54321', 'GET', '/dummy/does/not/exist', null, (res, err) => { + if (err) { + return done(err); + } + expressServer.startHttpsServer(true, (url) => { + httpsUrl = url; + ntlmHostConfig = { + ntlmHost: httpsUrl, + username: 'nisse', + password: 'manpower', + domain: 'mptst' + }; + return done(); + }); + }); + }); + }); + }); + + after('Stop HTTPS server and proxy', function(done) { + if (savePortsFileStub) { + savePortsFileStub.restore(); + } + if (portsFileExistsStub) { + portsFileExistsStub.restore(); + } + + proxyFacade.sendQuitCommand(configApiUrl, true, (err) => { + if (err) { + return done(err); + } + configApiUrl = null; + ntlmProxyUrl = null; + httpsUrl = null; + expressServer.stopHttpsServer((err) => { + if (err) { + return done(err); + } + return done(); + }); + }); + }); + + beforeEach('Reset NTLM config', function(done) { + proxyFacade.sendNtlmReset(configApiUrl, (err) => { + if (err) { + return done(err); + } + return done(); + }); + }); + + it('should handle authentication for GET requests', function(done) { + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'GET', '/get', null, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on GET requests', function(done) { + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'GET', '/get', null, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for POST requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'POST', '/post', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on POST requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'POST', '/post', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for PUT requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'PUT', '/put', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on PUT requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'PUT', '/put', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); + + it('should handle authentication for DELETE requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendNtlmConfig(configApiUrl, ntlmHostConfig, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'DELETE', '/delete', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 200); + assert(res.body.length > 20); + let body = JSON.parse(res.body); + assert.strictEqual(body.ntlmHost, 'https://my.test.host/'); + assert.strictEqual(body.reply, 'OK ÅÄÖéß'); + return done(); + }); + }); + }); + + it('should return 401 for unconfigured host on DELETE requests', function(done) { + let body = { + ntlmHost: 'https://my.test.host/' + }; + + proxyFacade.sendRemoteRequest(ntlmProxyUrl, httpsUrl, 'DELETE', '/delete', body, (res, err) => { + if (err) { + return done(err); + } + assert.strictEqual(res.statusCode, 401); + return done(); + }); + }); +}); +*/ diff --git a/test/proxy/proxyFacade.js b/test/proxy/proxyFacade.js new file mode 100644 index 0000000..b359177 --- /dev/null +++ b/test/proxy/proxyFacade.js @@ -0,0 +1,244 @@ +const url = require('url'); +const fs = require('fs'); +const http = require('http'); +const https = require('https'); +const httpMitmProxy = require('http-mitm-proxy'); +const getPort = require('get-port'); + +// The MITM proxy takes a significant time to start the first time +// due to cert generation, so we ensure this is done before the +// tests are executed to avoid timeouts +let _mitmProxyInit = false; + +module.exports = { + initMitmProxy: function(callback) { + if (_mitmProxyInit) { + return callback(); + } + + let mitmOptions = { + host: 'localhost', + port: null, + keepAlive: false, + silent: true, + forceSNI: false, + }; + + const mitmProxy = httpMitmProxy(); + getPort().then((port) => { + mitmOptions.port = port; + mitmProxy.listen(mitmOptions, (err) => { + if (err) { + return callback(err); + } + mitmProxy.close(); + _mitmProxyInit = true; + return callback(); + }); + }); + }, + + sendQuitCommand: function(configApiUrl, keepPortsFile, callback) { + const configApi = url.parse(configApiUrl); + const quitBody = JSON.stringify({ keepPortsFile: keepPortsFile }); + const quitReq = http.request({ + method: 'POST', + path: '/quit', + host: configApi.hostname, + port: configApi.port, + timeout: 15000, + headers: { + 'content-type': 'application/json; charset=UTF-8', + 'content-length': Buffer.byteLength(quitBody), + }, + }, function (res) { + res.resume(); + if (res.statusCode !== 200) { + return callback(new Error( + 'Unexpected response from NTLM proxy: ' + res.statusCode)); + } + return callback(); + }); + quitReq.on('error', (err) => { + return callback(err); + }); + quitReq.write(quitBody); + quitReq.end(); + }, + + sendNtlmConfig: function(configApiUrl, hostConfig, callback) { + const configUrl = url.parse(configApiUrl); + const hostConfigJson = JSON.stringify(hostConfig); + const configReq = http.request({ + method: 'POST', + path: '/ntlm-config', + host: configUrl.hostname, + port: configUrl.port, + timeout: 15000, + headers: { + 'content-type': 'application/json; charset=UTF-8', + 'content-length': Buffer.byteLength(hostConfigJson), + }, + }, (res) => { + let responseBody; + res.setEncoding('utf8'); + res.on('data', function(chunk) { + if (!responseBody) { + responseBody = chunk; + } else { + responseBody += chunk; + } + }); + res.on('end', function() { + res.body = responseBody; + return callback(res, null); + }); +/* + if (res.statusCode !== 200) { + return callback(new Error('Unexpected response status code on config', res.statusCode)); + } else { + return callback(); + } */ + }); + configReq.on('error', (err) => { + return callback(null, err); + }); + configReq.write(hostConfigJson); + configReq.end(); + }, + + sendNtlmReset: function(configApiUrl, callback) { + const configUrl = url.parse(configApiUrl); + const configReq = http.request({ + method: 'POST', + path: '/reset', + host: configUrl.hostname, + port: configUrl.port, + timeout: 15000, + }, (res) => { + if (res.statusCode !== 200) { + return callback(new Error('Unexpected response status code on reset', res.statusCode)); + } else { + return callback(); + } + }); + configReq.on('error', (err) => { + return callback(null, err); + }); + configReq.end(); + }, + + sendRemoteRequest: function( + ntlmProxyUrl, remoteHostWithPort, method, path, body, callback) { + const proxyUrl = url.parse(ntlmProxyUrl); + const remoteHostUrl = url.parse(remoteHostWithPort); + let headers = {}; + let bodyJson; + if (body) { + bodyJson = JSON.stringify(body); + headers['content-type'] = 'application/json; charset=UTF-8'; + headers['content-length'] = Buffer.byteLength(bodyJson); + } + + let proto = remoteHostUrl.protocol === 'http:' ? http : https; + if (remoteHostUrl.protocol === 'http:') { + sendProxiedHttpRequest(method, remoteHostUrl, path, proxyUrl, headers, bodyJson, callback); + } else { + sendProxiedHttpsRequest(method, remoteHostUrl, path, proxyUrl, headers, bodyJson, callback); + } + } + +}; + +function sendProxiedHttpRequest( + method, remoteHostUrl, path, proxyUrl, headers, bodyJson, callback) { + headers['Host'] = remoteHostUrl.host; + + const proxyReq = http.request({ + method: method, + path: path, + host: proxyUrl.hostname, + port: proxyUrl.port, + timeout: 3000, + headers: headers, + }, (res) => { + let responseBody; + res.setEncoding('utf8'); + res.on('data', function(chunk) { + if (!responseBody) { + responseBody = chunk; + } else { + responseBody += chunk; + } + }); + res.on('end', function() { + res.body = responseBody; + return callback(res, null); + }); + }); + proxyReq.on('error', (err) => { + return callback(null, err); + }); + if (bodyJson) { + proxyReq.write(bodyJson); + } + proxyReq.end(); + +} + +function sendProxiedHttpsRequest( + method, remoteHostUrl, path, proxyUrl, headers, bodyJson, callback) { + var connectReq = http.request({ // establishing a tunnel + host: proxyUrl.hostname, + port: proxyUrl.port, + method: 'CONNECT', + path: remoteHostUrl.href, + }); + + connectReq.on('connect', function(res, socket, head) { + if (res.statusCode !== 200) { + return callback(null, new Error('Unexpected response code on CONNECT', res.statusCode)); + } + + var req = https.request({ + method: method, + path: path, + host: remoteHostUrl.host, + timeout: 3000, + socket: socket, // using a tunnel + agent: false, // cannot use a default agent + headers: headers, + // We can ignore the self-signed certs on the testing webserver + // Cypress will also ignore this + rejectUnauthorized: false + }, function(res) { + let responseBody; + res.setEncoding('utf8'); + res.on('data', function(chunk) { + if (!responseBody) { + responseBody = chunk; + } else { + responseBody += chunk; + } + }); + res.on('end', function() { + res.body = responseBody; + return callback(res, null); + }); + }); + + req.on('error', (err) => { + return callback(null, err); + }); + + if (bodyJson) { + req.write(bodyJson); + } + req.end(); + }); + + connectReq.on('error', (err) => { + return callback(null, err); + }); + connectReq.end(); +} diff --git a/test/proxy/server.spec.js b/test/proxy/server.spec.js index f8a29ae..bbfa62e 100644 --- a/test/proxy/server.spec.js +++ b/test/proxy/server.spec.js @@ -9,8 +9,6 @@ const http = require('http'); const express = require('express'); const bodyParser = require('body-parser'); const isPortReachable = require('is-port-reachable'); -const httpMitmProxy = require('http-mitm-proxy'); -const getPort = require('get-port'); const portsFile = require('../../src/util/portsFile'); @@ -18,70 +16,13 @@ const appDataPath = require('appdata-path'); const portsFileName = 'cypress-ntlm-auth.port'; const portsFileWithPath = path.join(appDataPath('cypress-ntlm-auth'), portsFileName); +const proxyFacade = require('./proxyFacade'); const proxy = require('../../src/proxy/server'); -// The MITM proxy takes a significant time to start the first time -// due to cert generation, so we ensure this is done before the -// tests are executed to avoid timeouts -let _mitmProxyInit = false; -function initMitmProxy(callback) { - if (_mitmProxyInit) { - return callback(); - } - - let mitmOptions = { - host: 'localhost', - port: null, - keepAlive: false, - silent: true, - forceSNI: false, - }; - - const mitmProxy = httpMitmProxy(); - getPort().then((port) => { - mitmOptions.port = port; - mitmProxy.listen(mitmOptions, (err) => { - if (err) { - return callback(err); - } - mitmProxy.close(); - _mitmProxyInit = true; - return callback(); - }); - }); -} let _configApiUrl; -function sendQuitCommand(configApiUrl, keepPortsFile, callback) { - const configApi = url.parse(configApiUrl); - const quitBody = JSON.stringify({ keepPortsFile: keepPortsFile }); - const quitReq = http.request({ - method: 'POST', - path: '/quit', - host: configApi.hostname, - port: configApi.port, - timeout: 15000, - headers: { - 'content-type': 'application/json; charset=UTF-8', - 'content-length': Buffer.byteLength(quitBody), - }, - }, function (res) { - res.resume(); - if (res.statusCode !== 200) { - return callback(new Error( - 'Unexpected response from NTLM proxy: ' + res.statusCode)); - } - return callback(); - }); - quitReq.on('error', (err) => { - return callback(err); - }); - quitReq.write(quitBody); - quitReq.end(); -} - function isProxyReachable(ports, callback) { const configUrl = url.parse(ports.configApiUrl); const proxyUrl = url.parse(ports.ntlmProxyUrl); @@ -116,7 +57,7 @@ describe('Proxy startup and shutdown', () => { before(function (done) { this.timeout(15000); - initMitmProxy(done); + proxyFacade.initMitmProxy(done); }); beforeEach(function () { @@ -146,7 +87,7 @@ describe('Proxy startup and shutdown', () => { } if (_configApiUrl) { // Shutdown the proxy listeners to allow a clean exit - sendQuitCommand(_configApiUrl, false, (err) => { + proxyFacade.sendQuitCommand(_configApiUrl, false, (err) => { if (err) { return done(err); } @@ -309,7 +250,7 @@ describe('Proxy startup and shutdown', () => { assert(portsFileExistsStub.calledOnce); assert(savePortsFileStub.calledOnce); - sendQuitCommand(result.configApiUrl, true, (err) => { + proxyFacade.sendQuitCommand(result.configApiUrl, true, (err) => { if (err) { return done(err); } @@ -350,7 +291,7 @@ describe('Proxy startup and shutdown', () => { assert(portsFileExistsStub.calledOnce); assert(savePortsFileStub.calledOnce); - sendQuitCommand(result.configApiUrl, false, (err) => { + proxyFacade.sendQuitCommand(result.configApiUrl, false, (err) => { if (err) { return done(err); } @@ -391,7 +332,7 @@ function initRemoteHost(callback) { if (err) { return callback(err); } - remoteHostWithPort = 'localhost:' + remoteHostListener.address().port; + remoteHostWithPort = 'http://localhost:' + remoteHostListener.address().port; return callback(); }); } @@ -403,7 +344,7 @@ describe('Proxy authentication', function () { before(function (done) { this.timeout(15000); - initMitmProxy((err) => { + proxyFacade.initMitmProxy((err) => { if (err) { return done(err); } @@ -432,7 +373,7 @@ describe('Proxy authentication', function () { afterEach(function (done) { if (_configApiUrl) { // Shutdown the proxy listeners to allow a clean exit - sendQuitCommand(_configApiUrl, false, (err) => { + proxyFacade.sendQuitCommand(_configApiUrl, false, (err) => { if (err) { return done(err); } @@ -473,7 +414,7 @@ describe('Proxy authentication', function () { return done(err); } _configApiUrl = result.configApiUrl; - sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', + proxyFacade.sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', null, (res, err) => { if (err) { return done(err); @@ -498,7 +439,7 @@ describe('Proxy authentication', function () { return callback(); }); const hostConfig = { - ntlmHost: 'http://' + remoteHostWithPort, + ntlmHost: remoteHostWithPort, username: 'nisse', password: 'manpower', domain: 'mnpwr', @@ -512,15 +453,17 @@ describe('Proxy authentication', function () { return done(err); } _configApiUrl = result.configApiUrl; - sendNtlmConfig(result.configApiUrl, hostConfig, (err) => { + proxyFacade.sendNtlmConfig(result.configApiUrl, hostConfig, (res, err) => { if (err) { return done(err); } - sendRemoteRequest( + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest( result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', + null, (res, err) => { if (err) { return done(err); @@ -558,11 +501,12 @@ describe('Proxy authentication', function () { return done(err); } _configApiUrl = result.configApiUrl; - sendNtlmConfig(result.configApiUrl, hostConfig, (err) => { + proxyFacade.sendNtlmConfig(result.configApiUrl, hostConfig, (res, err) => { if (err) { return done(err); } - sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', (res, err) => { + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', null, (res, err) => { if (err) { return done(err); } @@ -587,7 +531,7 @@ describe('Proxy authentication', function () { return callback(); }); const hostConfig = { - ntlmHost: 'http://' + remoteHostWithPort, + ntlmHost: remoteHostWithPort, username: 'nisse', password: 'manpower', domain: 'mnpwr', @@ -599,15 +543,16 @@ describe('Proxy authentication', function () { return done(err); } _configApiUrl = result.configApiUrl; - sendNtlmConfig(result.configApiUrl, hostConfig, (err) => { + proxyFacade.sendNtlmConfig(result.configApiUrl, hostConfig, (res, err) => { if (err) { return done(err); } - sendNtlmReset(result.configApiUrl, (err) => { + assert.strictEqual(res.statusCode, 200); + proxyFacade.sendNtlmReset(result.configApiUrl, (err) => { if (err) { return done(err); } - sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', (res, err) => { + proxyFacade.sendRemoteRequest(result.ntlmProxyUrl, remoteHostWithPort, 'GET', '/test', null, (res, err) => { if (err) { return done(err); } @@ -623,67 +568,3 @@ describe('Proxy authentication', function () { }); }); }); - -function sendNtlmConfig(configApiUrl, hostConfig, callback) { - const configUrl = url.parse(configApiUrl); - const hostConfigJson = JSON.stringify(hostConfig); - const configReq = http.request({ - method: 'POST', - path: '/ntlm-config', - host: configUrl.hostname, - port: configUrl.port, - timeout: 15000, - headers: { - 'content-type': 'application/json; charset=UTF-8', - 'content-length': Buffer.byteLength(hostConfigJson), - }, - }, (res) => { - assert.equal(res.statusCode, 200); - return callback(); - }); - configReq.on('error', (err) => { - return callback(err); - }); - configReq.write(hostConfigJson); - configReq.end(); -} - -function sendNtlmReset(configApiUrl, callback) { - const configUrl = url.parse(configApiUrl); - const configReq = http.request({ - method: 'POST', - path: '/reset', - host: configUrl.hostname, - port: configUrl.port, - timeout: 15000, - }, (res) => { - assert.equal(res.statusCode, 200); - return callback(); - }); - configReq.on('error', (err) => { - return callback(err); - }); - configReq.end(); -} - -function sendRemoteRequest( - ntlmProxyUrl, remoteHostWithPort, method, path, callback) { - const proxyUrl = url.parse(ntlmProxyUrl); - - const proxyReq = http.request({ - method: method, - path: path, - host: proxyUrl.hostname, - port: proxyUrl.port, - timeout: 15000, - headers: { - 'Host': remoteHostWithPort, - }, - }, (res) => { - return callback(res, null); - }); - proxyReq.on('error', (err) => { - return callback(null, err); - }); - proxyReq.end(); -} diff --git a/test/util/configValidator.spec.js b/test/util/configValidator.spec.js new file mode 100644 index 0000000..ec7d19b --- /dev/null +++ b/test/util/configValidator.spec.js @@ -0,0 +1,236 @@ +const assert = require('assert'); +const configValidator = require('../../src/util/configValidator'); + +describe('configValidator ntlmHost', function () { + let config; + + beforeEach(function () { + config = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'manpwr' + }; + }); + + it('Valid ntlmHost succeeds', function() { + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, true); + }); + + it('Does return error if ntlmHost contains path', function () { + // Arrange + config.ntlmHost = 'http://localhost:5000/search'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Invalid ntlmHost, must not contain any path or query (https://www.google.com is ok, https://www.google.com/search is not ok)'); + }); + + it('Does return error if ntlmHost is incomplete', function () { + // Arrange + config.ntlmHost = 'localhost:5000'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Invalid ntlmHost, must be a valid URL (like https://www.google.com)'); + }); +}); + +describe('configValidator username', function () { + let config; + + beforeEach(function () { + config = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'manpwr' + }; + }); + + it('Valid username succeeds', function() { + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, true); + }); + + it('Does return error if username is too long', function () { + // Arrange + config.username = 'a'.repeat(105); + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Username contains invalid characters or is too long.'); + }); + + it('Does return error if username contains invalid chars', function () { + // Arrange + config.username = 'a*a'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Username contains invalid characters or is too long.'); + }); +}); + +describe('configValidator domain', function() { + let config; + + beforeEach(function () { + config = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'manpwr' + }; + }); + + it('Valid domain succeeds', function() { + // Arrange + config.domain = 'mptest'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, true); + }); + + it('Does return error if domain is too long', function () { + // Arrange + config.domain = 'a'.repeat(16);; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Domain contains invalid characters or is too long.'); + }); + + it('Does return error if domain contains invalid chars', function () { + // Arrange + config.domain = 'a*a'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Domain contains invalid characters or is too long.'); + }); +}); + +describe('configValidator workstation', function() { + let config; + + beforeEach(function () { + config = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'manpwr' + }; + }); + + it('Valid workstation succeeds', function() { + // Arrange + config.workstation = 'testpc'; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, true); + }); + + it('Does return error if workstation is too long', function () { + // Arrange + let workstation = 'a'.repeat(16); + config.workstation = workstation; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Workstation contains invalid characters or is too long.'); + }); + + it('Does return error if workstation contains invalid chars', function () { + // Arrange + let workstation = 'a*a'; + config.workstation = workstation; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Workstation contains invalid characters or is too long.'); + }); + +}); + +describe('configValidator required fields', function() { + let config; + + beforeEach(function () { + config = { + ntlmHost: 'http://localhost:5000', + username: 'nisse', + password: 'manpwr' + }; + }); + + it('Does return error if ntlmHost is missing', function () { + // Arrange + delete config.ntlmHost; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Incomplete configuration. ntlmHost, username and password are required fields.'); + }); + + it('Does return error if username is missing', function () { + // Arrange + delete config.username; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Incomplete configuration. ntlmHost, username and password are required fields.'); + }); + + + it('Does return error if password is missing', function () { + // Arrange + delete config.password; + + // Act + let result = configValidator.validate(config); + + // Assert + assert.strictEqual(result.ok, false); + assert.strictEqual(result.message, 'Incomplete configuration. ntlmHost, username and password are required fields.'); + }); +});