diff --git a/.gitignore b/.gitignore index 4c4a9fbb..e6bb527a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/** test/e2e-app-template/www/certificates/*.cer +test/e2e-app-template/www/certificates/*.pkcs tags .zedstate npm-debug.log diff --git a/package.json b/package.json index 22d5991e..9c00c42c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.0.9", "description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning", "scripts": { - "updatecert": "node ./scripts/update-test-cert.js", + "updatecert": "node ./scripts/update-e2e-server-cert.js && node ./scripts/update-e2e-client-cert.js", "buildbrowser": "./scripts/build-test-app.sh --browser", "testandroid": "npm run updatecert && ./scripts/build-test-app.sh --android --emulator && ./scripts/test-app.sh --android --emulator", "testios": "npm run updatecert && ./scripts/build-test-app.sh --ios --emulator && ./scripts/test-app.sh --ios --emulator", diff --git a/scripts/update-e2e-client-cert.js b/scripts/update-e2e-client-cert.js new file mode 100644 index 00000000..8d826d07 --- /dev/null +++ b/scripts/update-e2e-client-cert.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const https = require('https'); +const path = require('path'); + +const SOURCE_URL = 'https://badssl.com/certs/badssl.com-client.p12'; +const TARGET_PATH = path.join(__dirname, '../test/e2e-app-template/www/certificates/badssl-client-cert.pkcs'); + +const downloadPkcsContainer = (source, target) => new Promise((resolve, reject) => { + const file = fs.createWriteStream(target); + + const req = https.get(source, response => { + response.pipe(file) + resolve(target); + }); + + req.on('error', error => { + return reject(error) + }); + + req.end(); +}); + +console.log(`Updating client certificate from ${SOURCE_URL}`); + +downloadPkcsContainer(SOURCE_URL, TARGET_PATH) + .catch(error => { + console.error(`Updating client certificate failed: ${error}`); + process.exit(1); + }); diff --git a/scripts/update-test-cert.js b/scripts/update-e2e-server-cert.js similarity index 81% rename from scripts/update-test-cert.js rename to scripts/update-e2e-server-cert.js index c4df08c5..ca8c49a7 100644 --- a/scripts/update-test-cert.js +++ b/scripts/update-e2e-server-cert.js @@ -30,13 +30,11 @@ const getCert = hostname => new Promise((resolve, reject) => { req.end(); }); -console.log(`Updating test certificate from ${SOURCE_HOST}`); +console.log(`Updating server certificate from ${SOURCE_HOST}`); getCert(SOURCE_HOST) - .then(cert => { - fs.writeFileSync(TARGET_PATH, cert.raw); - }) + .then(cert => fs.writeFileSync(TARGET_PATH, cert.raw)) .catch(error => { - console.error(`Updating test cert failed: ${error}`); + console.error(`Updating server certificate failed: ${error}`); process.exit(1); }); diff --git a/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java b/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java index 54c781aa..b01ac936 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java +++ b/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java @@ -6,11 +6,15 @@ import android.security.KeyChainAliasCallback; import android.util.Log; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import org.apache.cordova.CallbackContext; @@ -21,17 +25,22 @@ class CordovaClientAuth implements Runnable, KeyChainAliasCallback { private static final String TAG = "Cordova-Plugin-HTTP"; private String mode; - private String filePath; + private String aliasString; + private byte[] rawPkcs; + private String pkcsPassword; private Activity activity; private Context context; private TLSConfiguration tlsConfiguration; private CallbackContext callbackContext; - public CordovaClientAuth(final String mode, final String filePath, final Activity activity, final Context context, - final TLSConfiguration configContainer, final CallbackContext callbackContext) { + public CordovaClientAuth(final String mode, final String aliasString, final byte[] rawPkcs, + final String pkcsPassword, final Activity activity, final Context context, final TLSConfiguration configContainer, + final CallbackContext callbackContext) { this.mode = mode; - this.filePath = filePath; + this.aliasString = aliasString; + this.rawPkcs = rawPkcs; + this.pkcsPassword = pkcsPassword; this.activity = activity; this.tlsConfiguration = configContainer; this.context = context; @@ -41,15 +50,45 @@ public CordovaClientAuth(final String mode, final String filePath, final Activit @Override public void run() { if ("systemstore".equals(this.mode)) { + this.loadFromSystemStore(); + } else if ("buffer".equals(this.mode)) { + this.loadFromBuffer(); + } else { + this.disableClientAuth(); + } + } + + private void loadFromSystemStore() { + if (this.aliasString == null) { KeyChain.choosePrivateKeyAlias(this.activity, this, null, null, null, -1, null); - } else if ("file".equals(this.mode)) { - this.callbackContext.error("Not implemented, yet"); } else { - this.tlsConfiguration.setKeyManagers(null); + this.alias(this.aliasString); + } + } + + private void loadFromBuffer() { + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm); + ByteArrayInputStream stream = new ByteArrayInputStream(this.rawPkcs); + + keyStore.load(stream, this.pkcsPassword.toCharArray()); + keyManagerFactory.init(keyStore, this.pkcsPassword.toCharArray()); + + this.tlsConfiguration.setKeyManagers(keyManagerFactory.getKeyManagers()); this.callbackContext.success(); + } catch (Exception e) { + Log.e(TAG, "Couldn't load given PKCS12 container for authentication", e); + this.callbackContext.error("Couldn't load given PKCS12 container for authentication"); } } + private void disableClientAuth() { + this.tlsConfiguration.setKeyManagers(null); + this.callbackContext.success(); + } + @Override public void alias(final String alias) { try { @@ -63,10 +102,12 @@ public void alias(final String alias) { this.tlsConfiguration.setKeyManagers(new KeyManager[] { keyManager }); - this.callbackContext.success(); + this.callbackContext.success(alias); } catch (Exception e) { - Log.e(TAG, "Couldn't load private key and certificate pair for authentication", e); - this.callbackContext.error("Couldn't load private key and certificate pair for authentication"); + Log.e(TAG, "Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication", + e); + this.callbackContext.error( + "Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication"); } } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 4f242db0..06af0260 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -13,6 +13,7 @@ import org.json.JSONObject; import android.util.Log; +import android.util.Base64; import javax.net.ssl.TrustManagerFactory; @@ -149,8 +150,11 @@ private boolean setServerTrustMode(final JSONArray args, final CallbackContext c } private boolean setClientAuthMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException { - CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), args.getString(1), this.cordova.getActivity(), - this.cordova.getActivity().getApplicationContext(), this.tlsConfiguration, callbackContext); + byte[] pkcs = args.isNull(2) ? null : Base64.decode(args.getString(2), Base64.DEFAULT); + + CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), args.isNull(1) ? null : args.getString(1), + pkcs, args.getString(3), this.cordova.getActivity(), this.cordova.getActivity().getApplicationContext(), + this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(runnable); diff --git a/src/android/com/silkimen/http/TLSConfiguration.java b/src/android/com/silkimen/http/TLSConfiguration.java index 33c86343..c33df6c1 100644 --- a/src/android/com/silkimen/http/TLSConfiguration.java +++ b/src/android/com/silkimen/http/TLSConfiguration.java @@ -2,19 +2,13 @@ import java.io.IOException; import java.security.GeneralSecurityException; -import java.security.KeyStore; import java.security.SecureRandom; -import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import com.silkimen.http.TLSSocketFactory; diff --git a/test/e2e-app-template/www/index.js b/test/e2e-app-template/www/index.js index 67582445..04d0a637 100644 --- a/test/e2e-app-template/www/index.js +++ b/test/e2e-app-template/www/index.js @@ -3,17 +3,17 @@ const app = { lastResult: null, - initialize: function() { + initialize: function () { document.getElementById('nextBtn').addEventListener('click', app.onNextBtnClick); }, - printResult: function(prefix, content) { + printResult: function (prefix, content) { const text = prefix + ': ' + JSON.stringify(content); document.getElementById('resultTextarea').value += text; }, - reject: function(content) { + reject: function (content) { document.getElementById('statusInput').value = 'finished'; app.printResult('result - rejected', content); @@ -23,7 +23,7 @@ const app = { }; }, - resolve: function(content) { + resolve: function (content) { document.getElementById('statusInput').value = 'finished'; app.printResult('result - resolved', content); @@ -33,7 +33,7 @@ const app = { }; }, - throw: function(error) { + throw: function (error) { document.getElementById('statusInput').value = 'finished'; app.printResult('result - throwed', error.message); @@ -43,11 +43,11 @@ const app = { }; }, - getResult: function(cb) { + getResult: function (cb) { cb(app.lastResult); }, - runTest: function(index) { + runTest: function (index) { const testDefinition = tests[index]; const titleText = app.testIndex + ': ' + testDefinition.description; const expectedText = 'expected - ' + testDefinition.expected; @@ -57,32 +57,88 @@ const app = { document.getElementById('resultTextarea').value = ''; document.getElementById('descriptionLbl').innerText = titleText; - try { - testDefinition.func(app.resolve, app.reject); - } catch (error) { - app.throw(error); - } - }, + const onSuccessFactory = function (cbChain) { + return function () { + cbChain.shift()(cbChain); + } + }; - onBeforeTest: function(testIndex, cb) { - app.lastResult = null; + const onFailFactory = function (prefix) { + return function (errorMessage) { + app.reject(prefix + ': ' + errorMessage); + } + }; + + const onThrowedHandler = function (prefix, error) { + app.throw(new Error(prefix + ': ' + error.message)); + }; - if (hooks && hooks.onBeforeEachTest) { - return hooks.onBeforeEachTest(function() { - const testDefinition = tests[testIndex]; + const execBeforeEachTest = function (cbChain) { + const prefix = 'in before each hook'; - if (testDefinition.before) { - testDefinition.before(cb); - } else { - cb(); + try { + if (!hooks || !hooks.onBeforeEachTest) { + return onSuccessFactory(cbChain)(); } - }); - } else { - cb(); - } + + hooks.onBeforeEachTest( + onSuccessFactory(cbChain), + onFailFactory(prefix) + ); + } catch (error) { + onThrowedHandler(prefix, error); + } + }; + + const execBeforeTest = function (cbChain) { + const prefix = 'in before hook'; + + try { + if (!testDefinition.before) { + return onSuccessFactory(cbChain)(); + } + + testDefinition.before( + onSuccessFactory(cbChain), + onFailFactory(prefix) + ); + } catch (error) { + onThrowedHandler(prefix, error); + } + }; + + const execTest = function () { + try { + testDefinition.func(app.resolve, app.reject); + } catch (error) { + app.throw(error); + } + }; + + onSuccessFactory([execBeforeEachTest, execBeforeTest, execTest])(); + }, + + onBeforeTest: function (testIndex, resolve, reject) { + const runBeforeEachTest = function (resolve, reject) { + if (!hooks || !hooks.onBeforeEachTest) return resolve(); + + hooks.onBeforeEachTest(resolve, reject); + }; + + const runBeforeTest = function (testIndex, resolve, reject) { + if (!tests[testIndex].before) return resolve(); + + tests[testIndex].before(resolve, reject); + }; + + app.lastResult = null; + + runBeforeEachTest(function () { + runBeforeTest(testIndex, resolve); + }, reject); }, - onFinishedAllTests: function() { + onFinishedAllTests: function () { const titleText = 'No more tests'; const expectedText = 'You have run all available tests.'; @@ -91,13 +147,11 @@ const app = { document.getElementById('descriptionLbl').innerText = titleText; }, - onNextBtnClick: function() { + onNextBtnClick: function () { app.testIndex += 1; if (app.testIndex < tests.length) { - app.onBeforeTest(app.testIndex, function() { - app.runTest(app.testIndex); - }); + app.runTest(app.testIndex); } else { app.onFinishedAllTests(); } diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 361793b4..d1df44c7 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -1,24 +1,42 @@ const hooks = { - onBeforeEachTest: function(done) { + onBeforeEachTest: function (resolve, reject) { cordova.plugin.http.clearCookies(); - helpers.setDefaultServerTrustMode(done); + helpers.setDefaultServerTrustMode(function () { + // @TODO: not ready yet + // helpers.setNoneClientAuthMode(resolve, reject); + resolve(); + }, reject); } }; const helpers = { - setDefaultServerTrustMode: function(done) { cordova.plugin.http.setServerTrustMode('default', done, done); }, - setNoCheckServerTrustMode: function(done) { cordova.plugin.http.setServerTrustMode('nocheck', done, done); }, - setPinnedServerTrustMode: function(done) { cordova.plugin.http.setServerTrustMode('pinned', done, done); }, - setJsonSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('json')); }, - setUtf8StringSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('utf8')); }, - setUrlEncodedSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('urlencoded')); }, - getWithXhr: function(done, url) { + setDefaultServerTrustMode: function (resolve, reject) { cordova.plugin.http.setServerTrustMode('default', resolve, reject); }, + setNoCheckServerTrustMode: function (resolve, reject) { cordova.plugin.http.setServerTrustMode('nocheck', resolve, reject); }, + setPinnedServerTrustMode: function (resolve, reject) { cordova.plugin.http.setServerTrustMode('pinned', resolve, reject); }, + setNoneClientAuthMode: function (resolve, reject) { cordova.plugin.http.setClientAuthMode('none', resolve, reject); }, + setBufferClientAuthMode: function (resolve, reject) { + helpers.getWithXhr(function(pkcs) { + cordova.plugin.http.setClientAuthMode('buffer', { + rawPkcs: pkcs, + pkcsPassword: 'badssl.com' + }, resolve, reject); + }, './certificates/badssl-client-cert.pkcs', 'arraybuffer'); + }, + setJsonSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('json')); }, + setUtf8StringSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('utf8')); }, + setUrlEncodedSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('urlencoded')); }, + getWithXhr: function (done, url, type) { var xhr = new XMLHttpRequest(); - xhr.addEventListener('load', function() { - done(this.responseText); + xhr.addEventListener('load', function () { + if (!type || type === 'text') { + done(this.responseText); + } else { + done(this.response); + } }); + xhr.responseType = type; xhr.open('GET', url); xhr.send(); }, @@ -26,7 +44,7 @@ const helpers = { window.resolveLocalFileSystemURL(cordova.file.cacheDirectory, function (directoryEntry) { directoryEntry.getFile(fileName, { create: true, exclusive: false }, function (fileEntry) { fileEntry.createWriter(function (fileWriter) { - var blob = new Blob([ content ], { type: 'text/plain' }); + var blob = new Blob([content], { type: 'text/plain' }); fileWriter.onwriteend = done; fileWriter.onerror = done; @@ -38,16 +56,16 @@ const helpers = { }; const messageFactory = { - sslTrustAnchor: function() { return 'TLS connection could not be established: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.' }, - invalidCertificate: function(domain) { return 'The certificate for this server is invalid. You might be connecting to a server that is pretending to be “' + domain + '” which could put your confidential information at risk.' } + sslTrustAnchor: function () { return 'TLS connection could not be established: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.' }, + invalidCertificate: function (domain) { return 'The certificate for this server is invalid. You might be connecting to a server that is pretending to be “' + domain + '” which could put your confidential information at risk.' } } const tests = [ { description: 'should reject self signed cert (GET)', expected: 'rejected: {"status":-2, ...', - func: function(resolve, reject) { cordova.plugin.http.get('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + func: function (resolve, reject) { cordova.plugin.http.get('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } @@ -55,8 +73,8 @@ const tests = [ { description: 'should reject self signed cert (PUT)', expected: 'rejected: {"status":-2, ...', - func: function(resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + func: function (resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } @@ -64,8 +82,8 @@ const tests = [ { description: 'should reject self signed cert (POST)', expected: 'rejected: {"status":-2, ...', - func: function(resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + func: function (resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } @@ -73,8 +91,8 @@ const tests = [ { description: 'should reject self signed cert (PATCH)', expected: 'rejected: {"status":-2, ...', - func: function(resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + func: function (resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } @@ -82,8 +100,8 @@ const tests = [ { description: 'should reject self signed cert (DELETE)', expected: 'rejected: {"status":-2, ...', - func: function(resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + func: function (resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } @@ -92,8 +110,8 @@ const tests = [ description: 'should accept bad cert (GET)', expected: 'resolved: {"status":200, ...', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.get('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.should.include({ status: 200 }); } @@ -102,8 +120,8 @@ const tests = [ description: 'should accept bad cert (PUT)', expected: 'rejected: {"status":405, ... // will be rejected because PUT is not allowed', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } @@ -112,8 +130,8 @@ const tests = [ description: 'should accept bad cert (POST)', expected: 'rejected: {"status":405, ... // will be rejected because POST is not allowed', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } @@ -122,8 +140,8 @@ const tests = [ description: 'should accept bad cert (PATCH)', expected: 'rejected: {"status":405, ... // will be rejected because PATCH is not allowed', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } @@ -132,8 +150,8 @@ const tests = [ description: 'should accept bad cert (DELETE)', expected: 'rejected: {"status":405, ... // will be rejected because DELETE is not allowed', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } @@ -142,8 +160,8 @@ const tests = [ description: 'should fetch data from http://httpbin.org/ (GET)', expected: 'resolved: {"status":200, ...', before: helpers.setNoCheckServerTrustMode, - func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.should.include({ status: 200 }); } @@ -152,8 +170,8 @@ const tests = [ description: 'should send JSON object correctly (POST)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } @@ -162,8 +180,8 @@ const tests = [ description: 'should send JSON object correctly (PUT)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } @@ -172,8 +190,8 @@ const tests = [ description: 'should send JSON object correctly (PATCH)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } @@ -182,39 +200,39 @@ const tests = [ description: 'should send JSON array correctly (POST) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', [ 1, 2, 3 ], {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', [1, 2, 3], {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); - JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); + JSON.parse(result.data.data).json.should.eql([1, 2, 3]); } }, { description: 'should send JSON array correctly (PUT) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', [ 1, 2, 3 ], {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', [1, 2, 3], {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); - JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); + JSON.parse(result.data.data).json.should.eql([1, 2, 3]); } }, { description: 'should send JSON array correctly (PATCH) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', [ 1, 2, 3 ], {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', [1, 2, 3], {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.be.a('string'); - JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); + JSON.parse(result.data.data).json.should.eql([1, 2, 3]); } }, { description: 'should send url encoded data correctly (POST) #41', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } @@ -223,8 +241,8 @@ const tests = [ description: 'should send url encoded data correctly (PUT)', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, - func: function(resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.put('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } @@ -233,8 +251,8 @@ const tests = [ description: 'should send url encoded data correctly (PATCH)', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, - func: function(resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.patch('http://httpbin.org/anything', { test: 'testString' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } @@ -242,8 +260,8 @@ const tests = [ { description: 'should resolve correct URL after redirect (GET) #33', expected: 'resolved: {"status": 200, url: "http://httpbin.org/anything", ...', - func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/redirect-to?url=http://httpbin.org/anything', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/redirect-to?url=http://httpbin.org/anything', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.url.should.be.equal('http://httpbin.org/anything'); } @@ -251,12 +269,12 @@ const tests = [ { description: 'should download a file from given URL to given path in local filesystem', expected: 'resolved: {"content": "\\n\\n" ...', - func: function(resolve, reject) { + func: function (resolve, reject) { var sourceUrl = 'http://httpbin.org/xml'; var targetPath = cordova.file.cacheDirectory + 'test.xml'; - cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function(entry) { - helpers.getWithXhr(function(content) { + cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) { + helpers.getWithXhr(function (content) { resolve({ sourceUrl: sourceUrl, targetPath: targetPath, @@ -267,7 +285,7 @@ const tests = [ }, targetPath); }, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.name.should.be.equal('test.xml'); result.data.content.should.be.equal("\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWidgets are great\n \n Who buys WonderWidgets\n \n\n"); @@ -276,17 +294,17 @@ const tests = [ { description: 'should upload a file from given path in local filesystem to given URL #27', expected: 'resolved: {"status": 200, "data": "files": {"test-file.txt": "I am a dummy file. I am used ...', - func: function(resolve, reject) { + func: function (resolve, reject) { var fileName = 'test-file.txt'; var fileContent = 'I am a dummy file. I am used for testing purposes!'; var sourcePath = cordova.file.cacheDirectory + fileName; var targetUrl = 'http://httpbin.org/post'; - helpers.writeToFile(function() { + helpers.writeToFile(function () { cordova.plugin.http.uploadFile(targetUrl, {}, {}, sourcePath, fileName, resolve, reject); }, fileName, fileContent); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { var fileName = 'test-file.txt'; var fileContent = 'I am a dummy file. I am used for testing purposes!'; @@ -302,10 +320,10 @@ const tests = [ { description: 'should encode HTTP array params correctly (GET) #45', expected: 'resolved: {"status": 200, "data": "{\\"url\\":\\"http://httpbin.org/get?myArray[]=val1&myArray[]=val2&myArray[]=val3\\"}\" ...', - func: function(resolve, reject) { - cordova.plugin.http.get('http://httpbin.org/get', { myArray: [ 'val1', 'val2', 'val3' ], myString: 'testString' }, {}, resolve, reject); + func: function (resolve, reject) { + cordova.plugin.http.get('http://httpbin.org/get', { myArray: ['val1', 'val2', 'val3'], myString: 'testString' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.be.a('string'); @@ -318,10 +336,10 @@ const tests = [ { description: 'should throw on non-string values in local header object #54', expected: 'throwed: {"message": "advanced-http: header values must be strings"}', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/get', {}, { myTestHeader: 1 }, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('throwed'); result.message.should.be.equal('advanced-http: header values must be strings'); } @@ -329,10 +347,10 @@ const tests = [ { description: 'should throw an error while setting non-string value as global header #54', expected: 'throwed: "advanced-http: header values must be strings"', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.setHeader('myTestHeader', 2); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('throwed'); result.message.should.be.equal('advanced-http: header values must be strings'); } @@ -340,10 +358,10 @@ const tests = [ { description: 'should accept content-type "application/xml" #58', expected: 'resolved: {"status": 200, ...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/xml', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.status.should.be.equal(200); } @@ -351,12 +369,12 @@ const tests = [ { description: 'should send programmatically set cookies correctly (GET)', expected: 'resolved: {"status": 200, ...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.setCookie('http://httpbin.org/get', 'myCookie=myValue'); cordova.plugin.http.setCookie('http://httpbin.org/get', 'mySecondCookie=mySecondValue'); cordova.plugin.http.get('http://httpbin.org/get', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.be.a('string'); @@ -370,13 +388,13 @@ const tests = [ { description: 'should not send any cookies after running "clearCookies" (GET) #59', expected: 'resolved: {"status": 200, "data": "{\"headers\": {\"Cookie\": \"\"...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.setCookie('http://httpbin.org/get', 'myCookie=myValue'); cordova.plugin.http.setCookie('http://httpbin.org/get', 'mySecondCookie=mySecondValue'); cordova.plugin.http.clearCookies(); cordova.plugin.http.get('http://httpbin.org/get', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.be.a('string'); @@ -389,15 +407,15 @@ const tests = [ { description: 'should send programmatically set cookies correctly (DOWNLOAD) #57', expected: 'resolved: {"content":{"cookies":{"myCookie":"myValue ...', - func: function(resolve, reject) { + func: function (resolve, reject) { var sourceUrl = 'http://httpbin.org/cookies'; var targetPath = cordova.file.cacheDirectory + 'cookies.json'; cordova.plugin.http.setCookie('http://httpbin.org/get', 'myCookie=myValue'); cordova.plugin.http.setCookie('http://httpbin.org/get', 'mySecondCookie=mySecondValue'); - cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function(entry) { - helpers.getWithXhr(function(content) { + cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) { + helpers.getWithXhr(function (content) { resolve({ sourceUrl: sourceUrl, targetPath: targetPath, @@ -408,7 +426,7 @@ const tests = [ }, targetPath); }, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.name.should.be.equal('cookies.json'); result.data.content.should.be.a('string'); @@ -423,10 +441,10 @@ const tests = [ description: 'should send UTF-8 encoded raw string correctly (POST) #34', expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"this is a test string\\"...', before: helpers.setUtf8StringSerializer, - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', 'this is a test string', {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).data.should.be.equal('this is a test string'); } @@ -434,10 +452,10 @@ const tests = [ { description: 'should encode spaces in query string (params object) correctly (GET) #71', expected: 'resolved: {"status": 200, "data": "{\\"args\\": \\"query param\\": \\"and value with spaces\\"...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/get', { 'query param': 'and value with spaces' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).args['query param'].should.be.equal('and value with spaces'); } @@ -445,10 +463,10 @@ const tests = [ { description: 'should decode latin1 (iso-8859-1) encoded body correctly (GET) #72', expected: 'resolved: {"status": 200, "data": " ...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('http://www.columbia.edu/kermit/latin1.html', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.include('[¡] 161 10/01 241 A1 INVERTED EXCLAMATION MARK\n[¢] 162 10/02 242 A2 CENT SIGN'); } @@ -456,10 +474,10 @@ const tests = [ { description: 'should return empty body string correctly (GET)', expected: 'resolved: {"status": 200, "data": "" ...', - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/stream/0', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.data.should.be.equal(''); } @@ -468,10 +486,10 @@ const tests = [ description: 'should pin SSL cert correctly (GET)', expected: 'resolved: {"status": 200 ...', before: helpers.setPinnedServerTrustMode, - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('https://httpbin.org', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.status.should.be.equal(200); } @@ -480,10 +498,10 @@ const tests = [ description: 'should reject when pinned cert does not match received server cert (GET)', expected: 'rejected: {"status": -2 ...', before: helpers.setPinnedServerTrustMode, - func: function(resolve, reject) { + func: function (resolve, reject) { cordova.plugin.http.get('https://sha512.badssl.com/', {}, {}, resolve, reject); }, - validationFunc: function(driver, result, targetInfo) { + validationFunc: function (driver, result, targetInfo) { result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('sha512.badssl.com') }); } @@ -492,18 +510,18 @@ const tests = [ description: 'should send deeply structured JSON object correctly (POST) #65', expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"{\\\\"outerObj\\\\":{\\\\"innerStr\\\\":\\\\"testString\\\\",\\\\"innerArr\\\\":[1,2,3]}}\\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { outerObj: { innerStr: 'testString', innerArr: [1, 2, 3] }}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { outerObj: { innerStr: 'testString', innerArr: [1, 2, 3] } }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); - JSON.parse(result.data.data).json.should.eql({ outerObj: { innerStr: 'testString', innerArr: [1, 2, 3] }}); + JSON.parse(result.data.data).json.should.eql({ outerObj: { innerStr: 'testString', innerArr: [1, 2, 3] } }); } }, { description: 'should override header "content-type" correctly (POST) #78', expected: 'resolved: {"status": 200, "headers": "{\\"Content-Type\\": \\"text/plain\\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', {}, { 'Content-Type': 'text/plain' }, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', {}, { 'Content-Type': 'text/plain' }, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).headers['Content-Type'].should.be.equal('text/plain'); } @@ -511,8 +529,8 @@ const tests = [ { description: 'should handle error during file download correctly (DOWNLOAD) #83', expected: 'rejected: {"status": 403, "error": "There was an error downloading the file" ...', - func: function(resolve, reject) { cordova.plugin.http.downloadFile('http://httpbin.org/status/403', {}, {}, cordova.file.tempDirectory + 'testfile.txt', resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.downloadFile('http://httpbin.org/status/403', {}, {}, cordova.file.tempDirectory + 'testfile.txt', resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('rejected'); result.data.status.should.be.equal(403); result.data.error.should.be.equal('There was an error downloading the file'); @@ -521,8 +539,8 @@ const tests = [ { description: 'should handle gzip encoded response correctly', expected: 'resolved: {"status": 200, "headers": "{\\"Content-Encoding\\": \\"gzip\\" ...', - func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/gzip', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('http://httpbin.org/gzip', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.status.should.be.equal(200); JSON.parse(result.data.data).gzipped.should.be.equal(true); @@ -532,8 +550,8 @@ const tests = [ description: 'should send empty string correctly', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"\\" ...', before: helpers.setUtf8StringSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', '', {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', '', {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).data.should.be.equal(''); } @@ -542,8 +560,8 @@ const tests = [ description: 'shouldn\'t escape forward slashes #184', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"/\\" ...', before: helpers.setJsonSerializer, - func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { testString: '/' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { testString: '/' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.testString.should.be.equal('/'); } @@ -551,8 +569,8 @@ const tests = [ { description: 'should not double encode spaces in url path #195', expected: 'resolved: {"status": 200, "data": "{\\"url\\":\\"https://httpbin.org/anything/containing spaces in url\\" ...', - func: function(resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything/containing%20spaces%20in%20url', {}, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything/containing%20spaces%20in%20url', {}, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).url.should.be.equal('https://httpbin.org/anything/containing spaces in url'); } @@ -560,8 +578,8 @@ const tests = [ { description: 'should encode spaces in url query correctly', expected: 'resolved: {"status": 200, "data": "{\\"url\\":\\"https://httpbin.org/anything?query key=very long query value with spaces\\" ...', - func: function(resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything', { 'query key': 'very long query value with spaces' }, {}, resolve, reject); }, - validationFunc: function(driver, result) { + func: function (resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything', { 'query key': 'very long query value with spaces' }, {}, resolve, reject); }, + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); JSON.parse(result.data.data).url.should.be.equal('https://httpbin.org/anything?query key=very long query value with spaces'); } @@ -569,12 +587,12 @@ const tests = [ { description: 'should download a file from given HTTPS URL to given path in local filesystem #197', expected: 'resolved: {"content": "\\n\\n" ...', - func: function(resolve, reject) { + func: function (resolve, reject) { var sourceUrl = 'https://httpbin.org/xml'; var targetPath = cordova.file.cacheDirectory + 'test.xml'; - cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function(entry) { - helpers.getWithXhr(function(content) { + cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) { + helpers.getWithXhr(function (content) { resolve({ sourceUrl: sourceUrl, targetPath: targetPath, @@ -585,12 +603,23 @@ const tests = [ }, targetPath); }, reject); }, - validationFunc: function(driver, result) { + validationFunc: function (driver, result) { result.type.should.be.equal('resolved'); result.data.name.should.be.equal('test.xml'); result.data.content.should.be.equal("\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWidgets are great\n \n Who buys WonderWidgets\n \n\n"); } - } + }, + // @TODO: not ready yet + // { + // description: 'should authenticate correctly when client cert auth is configured with a PKCS12 container', + // expected: 'resolved: {"status": 200, ...', + // before: helpers.setBufferClientAuthMode, + // func: function (resolve, reject) { cordova.plugin.http.get('https://client.badssl.com/', {}, {}, resolve, reject); }, + // validationFunc: function (driver, result) { + // result.type.should.be.equal('resolved'); + // result.data.data.should.include('TLS handshake'); + // } + // } ]; if (typeof module !== 'undefined' && module.exports) { diff --git a/test/js-specs.js b/test/js-specs.js index c7aa4379..a5fef7ec 100644 --- a/test/js-specs.js +++ b/test/js-specs.js @@ -261,4 +261,69 @@ describe('Common helpers', function () { helpers.getCookieHeader('http://ilkimen.net').should.eql({ Cookie: 'cookie=value' }); }); }); + + describe('checkClientAuthOptions()', function () { + const jsUtil = require('../www/js-util'); + const messages = require('../www/messages'); + const helpers = require('../www/helpers')(jsUtil, null, messages); + + it('returns options object with empty values when mode is "none" and no options are given', () => { + helpers.checkClientAuthOptions('none').should.eql({ + alias: null, + pkcsPath: '', + pkcsPassword: '' + }); + }); + + it('returns options object with empty values when mode is "none" and random options are given', () => { + helpers.checkClientAuthOptions('none', { + alias: 'myAlias', + pkcsPath: 'myPath' + }).should.eql({ + alias: null, + pkcsPath: '', + pkcsPassword: '' + }); + }); + + it('throws an error when mode is "systemstore" and alias is not a string or undefined', () => { + (() => helpers.checkClientAuthOptions('systemstore', { alias: 1 })) + .should.throw(messages.INVALID_CLIENT_AUTH_ALIAS); + + (() => helpers.checkClientAuthOptions('systemstore', { alias: undefined })) + .should.not.throw(); + }); + + it('returns an object with null alias when mode is "systemstore" and no options object is given', () => { + helpers.checkClientAuthOptions('systemstore').should.eql({ + alias: null, + pkcsPath: '', + pkcsPassword: '' + }); + }); + + it('throws an error when mode is "file" and pkcsPath is not a string', () => { + (() => helpers.checkClientAuthOptions('file', { + pkcsPath: undefined, + pkcsPassword: 'password' + })).should.throw(messages.INVALID_CLIENT_AUTH_PKCS_PATH); + + (() => helpers.checkClientAuthOptions('file', { + pkcsPath: 1, + pkcsPassword: 'password' + })).should.throw(messages.INVALID_CLIENT_AUTH_PKCS_PATH); + }); + + it('throws an error when mode is "file" and pkcsPassword is not a string', () => { + (() => helpers.checkClientAuthOptions('file', { + pkcsPath: 'path', + pkcsPassword: undefined + })).should.throw(messages.INVALID_CLIENT_AUTH_PKCS_PASSWORD); + + (() => helpers.checkClientAuthOptions('file', { + pkcsPath: 'path', + pkcsPassword: 1 + })).should.throw(messages.INVALID_CLIENT_AUTH_PKCS_PASSWORD); + }); + }); }) diff --git a/www/helpers.js b/www/helpers.js index a9acc60c..2a7a0149 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -1,7 +1,7 @@ module.exports = function init(jsUtil, cookieHandler, messages) { var validSerializers = ['urlencoded', 'json', 'utf8']; var validCertModes = ['default', 'nocheck', 'pinned', 'legacy']; - var validClientAuthModes = ['none', 'systemstore', 'file']; + var validClientAuthModes = ['none', 'systemstore', 'buffer']; var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download']; var interface = { @@ -9,6 +9,7 @@ module.exports = function init(jsUtil, cookieHandler, messages) { checkSerializer: checkSerializer, checkSSLCertMode: checkSSLCertMode, checkClientAuthMode: checkClientAuthMode, + checkClientAuthOptions: checkClientAuthOptions, checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, checkForInvalidHeaderValue: checkForInvalidHeaderValue, injectCookieHandler: injectCookieHandler, @@ -105,6 +106,54 @@ module.exports = function init(jsUtil, cookieHandler, messages) { return checkForValidStringValue(validClientAuthModes, mode, messages.INVALID_CLIENT_AUTH_MODE); } + function checkClientAuthOptions(mode, options) { + options = options || {}; + + // none + if (mode === validClientAuthModes[0]) { + return { + alias: null, + rawPkcs: null, + pkcsPassword: '' + }; + } + + if (jsUtil.getTypeOf(options) !== 'Object') { + throw new Error(messages.INVALID_CLIENT_AUTH_OPTIONS); + } + + // systemstore + if (mode === validClientAuthModes[1]) { + if (jsUtil.getTypeOf(options.alias) !== 'String' + && jsUtil.getTypeOf(options.alias) !== 'Undefined') { + throw new Error(messages.INVALID_CLIENT_AUTH_ALIAS); + } + + return { + alias: jsUtil.getTypeOf(options.alias) === 'Undefined' ? null : options.alias, + rawPkcs: null, + pkcsPassword: '' + }; + } + + // buffer + if (mode === validClientAuthModes[2]) { + if (jsUtil.getTypeOf(options.rawPkcs) !== 'ArrayBuffer') { + throw new Error(messages.INVALID_CLIENT_AUTH_RAW_PKCS); + } + + if (jsUtil.getTypeOf(options.pkcsPassword) !== 'String') { + throw new Error(messages.INVALID_CLIENT_AUTH_PKCS_PASSWORD); + } + + return { + alias: null, + rawPkcs: options.rawPkcs, + pkcsPassword: options.pkcsPassword + } + } + } + function checkForBlacklistedHeaderKey(key) { if (key.toLowerCase() === 'cookie') { throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED); diff --git a/www/js-util.js b/www/js-util.js index a86da8bb..b41a255e 100644 --- a/www/js-util.js +++ b/www/js-util.js @@ -4,6 +4,8 @@ module.exports = { switch (Object.prototype.toString.call(object)) { case '[object Array]': return 'Array'; + case '[object ArrayBuffer]': + return 'ArrayBuffer'; case '[object Boolean]': return 'Boolean'; case '[object Function]': diff --git a/www/messages.js b/www/messages.js index 25a76d0d..3f788a7c 100644 --- a/www/messages.js +++ b/www/messages.js @@ -7,6 +7,10 @@ module.exports = { INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', INVALID_SSL_CERT_MODE: 'advanced-http: invalid SSL cert mode, supported modes are:', INVALID_CLIENT_AUTH_MODE: 'advanced-http: invalid client certificate authentication mode, supported modes are:', + INVALID_CLIENT_AUTH_OPTIONS: 'advanced-http: invalid client certificate authentication options, needs to be an object', + INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or undefined', + INVALID_CLIENT_AUTH_RAW_PKCS: 'advanced-http: invalid PKCS12 container, needs to be an array buffer', + INVALID_CLIENT_AUTH_PKCS_PASSWORD: 'advanced-http: invalid PKCS12 container password, needs to be a string', INVALID_HEADERS_VALUE: 'advanced-http: header values must be strings', INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value', INVALID_PARAMS_VALUE: 'advanced-http: invalid params object, needs to be an object with strings' diff --git a/www/public-interface.js b/www/public-interface.js index aae6fa0b..bfbbfb61 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -98,22 +98,23 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf } function setClientAuthMode() { - // filePath is an optional param var mode = arguments[0]; + var options = null; var success = arguments[1]; var failure = arguments[2]; - var filePath = null; if (arguments.length === 4) { - mode = arguments[0]; - filePath = arguments[1]; + options = arguments[1]; success = arguments[2]; failure = arguments[3]; } + mode = helpers.checkClientAuthMode(mode); + options = helpers.checkClientAuthOptions(mode, options); + helpers.handleMissingCallbacks(success, failure); - return exec(success, failure, 'CordovaHttpPlugin', 'setClientAuthMode', [helpers.checkClientAuthMode(mode), filePath]); + return exec(success, failure, 'CordovaHttpPlugin', 'setClientAuthMode', [mode, options.alias, options.rawPkcs, options.pkcsPassword]); } function disableRedirect(disable, success, failure) {