Compare commits

...

12 Commits

Author SHA1 Message Date
Sefa Ilkimen
b6a8acd25b release v3.2.0 2021-07-26 01:22:31 +02:00
Sefa Ilkimen
0ccf64e289 chore: add workaround for missing DX files in build-tools 31 2021-07-22 15:40:38 +02:00
Sefa Ilkimen
986ddeefd4 chore: remove obsolete mipsel workaround 2021-07-22 14:36:34 +02:00
Sefa Ilkimen
67c2c733f5 chore: enable network logging on browserstack 2021-07-15 15:34:34 +02:00
Sefa Ilkimen
d077d02062 Merge pull request #421 from silkimen/feat/#420-implement-blacklist-to-disable-TLS-protocols-on-Android
feat: #420 implement blacklist to disable unsafe SSL/TLS protocol ver…
2021-07-15 14:35:24 +02:00
Sefa Ilkimen
060794354d docu: add docu for secure socket protocol blacklisting 2021-07-15 14:19:10 +02:00
Sefa Ilkimen
4ebd259c7e Merge branch 'feat/#420-implement-blacklist-to-disable-TLS-protocols-on-Android' of https://github.com/silkimen/cordova-plugin-advanced-http into feat/#420-implement-blacklist-to-disable-TLS-protocols-on-Android 2021-07-15 12:53:39 +02:00
Sefa Ilkimen
bbd4cf01ab refactor: apply review feddback 2021-07-15 12:53:12 +02:00
Sefa Ilkimen
7eb3395aa4 Update test/e2e-specs.js
Co-authored-by: Robin Hartmann <contact.robin.hartmann@gmail.com>
2021-07-15 12:38:09 +02:00
Sefa Ilkimen
05abdfcd91 Update CHANGELOG.md
Co-authored-by: Robin Hartmann <contact.robin.hartmann@gmail.com>
2021-07-15 12:36:10 +02:00
Sefa Ilkimen
bda4eedfb9 feat: #420 implement blacklist to disable unsafe SSL/TLS protocol versions on Android 2021-07-15 04:00:35 +02:00
Sefa Ilkimen
9d6005af29 - chore: implement new test reporter to see more details on failed tests
- fix: fix broken e2e tests on Android 8+
2021-07-15 03:14:46 +02:00
16 changed files with 321 additions and 34 deletions

View File

@@ -53,8 +53,8 @@ jobs:
java-version: 1.8
- name: Update test cert for httpbin.org
run: npm run updatecert
- name: Add workaround for mipsel reference
run: sudo mkdir -p $ANDROID_HOME/ndk-bundle/toolchains/mips64el-linux-android-4.9/prebuilt/linux-x86_64
- name: Add workaround for missing DX files in build-tools 31 (https://stackoverflow.com/a/68430992)
run: ln -s $ANDROID_HOME/build-tools/31.0.0/d8 $ANDROID_HOME/build-tools/31.0.0/dx && ln -s $ANDROID_HOME/build-tools/31.0.0/lib/d8.jar $ANDROID_HOME/build-tools/31.0.0/lib/dx.jar
- name: Build test app
run: scripts/build-test-app.sh --android --device
- name: Upload artifact to BrowserStack

View File

@@ -1,5 +1,9 @@
# Changelog
## 3.2.0
- Feature #420: implement blacklist feature to disable SSL/TLS versions on Android (thanks to @MobisysGmbH)
## 3.1.1
- Fixed #372: malformed empty multipart request on Android

View File

@@ -34,6 +34,15 @@ phonegap plugin add cordova-plugin-advanced-http
cordova plugin add cordova-plugin-advanced-http
```
### Plugin Preferences
`AndroidBlacklistSecureSocketProtocols`: define a blacklist of secure socket protocols for Android. This preference allows you to disable protocols which are considered unsafe. You need to provide a comma-separated list of protocols ([check Android SSLSocket#protocols docu for protocol names](https://developer.android.com/reference/javax/net/ssl/SSLSocket#protocols)).
e.g. blacklist `SSLv3` and `TLSv1`:
```xml
<preference name="AndroidBlacklistSecureSocketProtocols" value="SSLv3,TLSv1" />
```
## Usage
### Plain Cordova

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "cordova-plugin-advanced-http",
"version": "3.1.1",
"version": "3.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "3.1.1",
"version": "3.2.0",
"engines": [
{
"name": "cordova",

View File

@@ -1,6 +1,6 @@
{
"name": "cordova-plugin-advanced-http",
"version": "3.1.1",
"version": "3.2.0",
"description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning",
"scripts": {
"updatecert": "node ./scripts/update-e2e-server-cert.js && node ./scripts/update-e2e-client-cert.js",

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-advanced-http" version="3.1.1">
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-advanced-http" version="3.2.0">
<name>Advanced HTTP plugin</name>
<description>
Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning
@@ -8,6 +8,7 @@
<engine name="cordova" version=">=4.0.0"/>
</engines>
<dependency id="cordova-plugin-file" version=">=2.0.0"/>
<preference name="AndroidBlacklistSecureSocketProtocols" default="SSLv3,TLSv1"/>
<js-module src="www/cookie-handler.js" name="cookie-handler"/>
<js-module src="www/dependency-validator.js" name="dependency-validator"/>
<js-module src="www/error-codes.js" name="error-codes"/>

View File

@@ -10,5 +10,5 @@ fi
printf 'Running e2e tests\n'
pushd $ROOT
./node_modules/.bin/mocha ./test/e2e-tooling/test.js "$@"
./node_modules/.bin/mocha --reporter ./test/e2e-tooling/reporter.js ./test/e2e-tooling/test.js "$@"
popd

View File

@@ -47,6 +47,13 @@ public class CordovaHttpPlugin extends CordovaPlugin implements Observer {
this.tlsConfiguration.setHostnameVerifier(null);
this.tlsConfiguration.setTrustManagers(tmf.getTrustManagers());
if (this.preferences.contains("androidblacklistsecuresocketprotocols")) {
this.tlsConfiguration.setBlacklistedProtocols(
this.preferences.getString("androidblacklistsecuresocketprotocols", "").split(",")
);
}
} catch (Exception e) {
Log.e(TAG, "An error occured while loading system's CA certificates", e);
}

View File

@@ -13,9 +13,10 @@ import javax.net.ssl.TrustManager;
import com.silkimen.http.TLSSocketFactory;
public class TLSConfiguration {
private TrustManager[] trustManagers;
private KeyManager[] keyManagers;
private HostnameVerifier hostnameVerifier;
private TrustManager[] trustManagers = null;
private KeyManager[] keyManagers = null;
private HostnameVerifier hostnameVerifier = null;
private String[] blacklistedProtocols = {};
private SSLSocketFactory socketFactory;
@@ -33,6 +34,11 @@ public class TLSConfiguration {
this.socketFactory = null;
}
public void setBlacklistedProtocols(String[] protocols) {
this.blacklistedProtocols = protocols;
this.socketFactory = null;
}
public HostnameVerifier getHostnameVerifier() {
return this.hostnameVerifier;
}
@@ -46,12 +52,7 @@ public class TLSConfiguration {
SSLContext context = SSLContext.getInstance("TLS");
context.init(this.keyManagers, this.trustManagers, new SecureRandom());
if (android.os.Build.VERSION.SDK_INT < 20) {
this.socketFactory = new TLSSocketFactory(context);
} else {
this.socketFactory = context.getSocketFactory();
}
this.socketFactory = new TLSSocketFactory(context, this.blacklistedProtocols);
return this.socketFactory;
} catch (GeneralSecurityException e) {

View File

@@ -5,6 +5,9 @@ import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
@@ -12,9 +15,11 @@ import javax.net.ssl.SSLSocketFactory;
public class TLSSocketFactory extends SSLSocketFactory {
private SSLSocketFactory delegate;
private String[] blacklistedProtocols;
public TLSSocketFactory(SSLContext context) {
delegate = context.getSocketFactory();
public TLSSocketFactory(SSLContext context, String[] blacklistedProtocols) {
this.delegate = context.getSocketFactory();
this.blacklistedProtocols = Arrays.stream(blacklistedProtocols).map(String::trim).toArray(String[]::new);
}
@Override
@@ -55,9 +60,18 @@ public class TLSSocketFactory extends SSLSocketFactory {
}
private Socket enableTLSOnSocket(Socket socket) {
if (socket != null && (socket instanceof SSLSocket)) {
((SSLSocket) socket).setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" });
if (socket == null || !(socket instanceof SSLSocket)) {
return socket;
}
String[] supported = ((SSLSocket) socket).getSupportedProtocols();
String[] filtered = Arrays.stream(supported).filter(
val -> Arrays.stream(this.blacklistedProtocols).noneMatch(val::equals)
).toArray(String[]::new);
((SSLSocket) socket).setEnabledProtocols(filtered);
return socket;
}
}

View File

@@ -27,4 +27,5 @@
<allow-intent href="itms-apps:*" />
</platform>
<preference name="AndroidPersistentFileLocation" value="Internal" />
<preference name="AndroidBlacklistSecureSocketProtocols" value="SSLv3,TLSv1" />
</widget>

View File

@@ -3,5 +3,7 @@
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">httpbin.org</domain>
<domain includeSubdomains="true">httpbingo.org</domain>
<domain includeSubdomains="true">www.columbia.edu</domain>
</domain-config>
</network-security-config>

View File

@@ -104,9 +104,13 @@ const helpers = {
return buffer;
},
isTlsBlacklistSupported: function () {
return window.cordova && window.cordova.platformId === 'android';
}
};
const messageFactory = {
handshakeFailed: function() { return 'TLS connection could not be established: javax.net.ssl.SSLHandshakeException: Handshake failed' },
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.' }
}
@@ -1014,8 +1018,7 @@ const tests = [
before: helpers.setRawSerializer,
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
skip();
return;
return skip();
}
var targetUrl = 'http://httpbin.org/post';
@@ -1036,8 +1039,7 @@ const tests = [
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
skip();
return;
return skip();
}
var url = 'https://httpbin.org/drip?duration=2&numbytes=10&code=200';
var options = { method: 'get', responseType: 'blob' };
@@ -1064,8 +1066,7 @@ const tests = [
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
skip();
return;
return skip();
}
var sourceUrl = 'http://httpbin.org/xml';
var targetPath = cordova.file.cacheDirectory + 'test.xml';
@@ -1097,8 +1098,7 @@ const tests = [
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
skip();
return;
return skip();
}
@@ -1148,6 +1148,21 @@ const tests = [
}
}
},
{
description: 'should reject connecting to server with blacklisted SSL version #420',
expected: 'rejected: {"status":-2, ...',
func: function (resolve, reject, skip) {
if (!helpers.isTlsBlacklistSupported()) {
return skip();
}
cordova.plugin.http.get('https://tls-v1-0.badssl.com:1010/', {}, {}, resolve, reject);
},
validationFunc: function (driver, result) {
result.type.should.be.equal('rejected');
result.data.should.be.eql({ status: -2, error: messageFactory.handshakeFailed() });
}
},
];
if (typeof module !== 'undefined' && module.exports) {

View File

@@ -64,14 +64,16 @@ const configs = {
os_version: '14',
project: 'HTTP Test App',
autoWebview: true,
app: 'HttpTestAppAndroid'
app: 'HttpTestAppAndroid',
'browserstack.networkLogs': true
},
browserstackAndroidDevice: {
device: 'Google Nexus 6',
os_version: '6.0',
project: 'HTTP Test App',
autoWebview: true,
app: 'HttpTestAppAndroid'
app: 'HttpTestAppAndroid',
'browserstack.networkLogs': true
}
};

View File

@@ -0,0 +1,223 @@
'use strict';
const Mocha = require('mocha');
const milliseconds = require('ms');
const Base = Mocha.reporters.Base;
const color = Base.color;
const {
isString,
stringify,
inherits
} = Mocha.utils;
const {
EVENT_RUN_BEGIN,
EVENT_RUN_END,
EVENT_TEST_FAIL,
EVENT_TEST_PASS,
EVENT_TEST_PENDING,
EVENT_SUITE_BEGIN,
EVENT_SUITE_END
} = Mocha.Runner.constants;
function Spec(runner, options) {
Base.call(this, runner, options);
var self = this;
var indents = 0;
var n = 0;
function indent() {
return Array(indents).join(' ');
}
runner.on(EVENT_RUN_BEGIN, function() {
Base.consoleLog();
});
runner.on(EVENT_SUITE_BEGIN, function(suite) {
++indents;
Base.consoleLog(color('suite', '%s%s'), indent(), suite.title);
});
runner.on(EVENT_SUITE_END, function() {
--indents;
if (indents === 1) {
Base.consoleLog();
}
});
runner.on(EVENT_TEST_PENDING, function(test) {
var fmt = indent() + color('pending', ' - %s');
Base.consoleLog(fmt, test.title);
});
runner.on(EVENT_TEST_PASS, function(test) {
var fmt;
if (test.speed === 'fast') {
fmt =
indent() +
color('checkmark', ' ' + Base.symbols.ok) +
color('pass', ' %s');
Base.consoleLog(fmt, test.title);
} else {
fmt =
indent() +
color('checkmark', ' ' + Base.symbols.ok) +
color('pass', ' %s') +
color(test.speed, ' (%dms)');
Base.consoleLog(fmt, test.title, test.duration);
}
});
runner.on(EVENT_TEST_FAIL, function(test) {
Base.consoleLog(indent() + color('fail', ' %d) %s'), ++n, test.title);
});
runner.once(EVENT_RUN_END, self.epilogue.bind(self));
}
/**
* Inherit from `Base.prototype`.
*/
inherits(Spec, Base);
Spec.description = 'custom reporter for HTTP plugin testing';
Spec.prototype.epilogue = function() {
var stats = this.stats;
var fmt;
Base.consoleLog();
// passes
fmt =
color('bright pass', ' ') +
color('green', ' %d passing') +
color('light', ' (%s)');
Base.consoleLog(fmt, stats.passes || 0, milliseconds(stats.duration));
// pending
if (stats.pending) {
fmt = color('pending', ' ') + color('pending', ' %d pending');
Base.consoleLog(fmt, stats.pending);
}
// failures
if (stats.failures) {
fmt = color('fail', ' %d failing');
Base.consoleLog(fmt, stats.failures);
this.showList(this.failures);
Base.consoleLog();
}
Base.consoleLog();
};
Spec.prototype.showList = function(failures) {
var multipleErr, multipleTest;
var self = this;
Base.consoleLog();
failures.forEach(function(test, i) {
// format
var fmt =
color('error title', ' %s) %s:\n') +
color('error message', ' %s') +
color('error stack', '\n%s\n');
// msg
var msg;
var err;
if (test.err && test.err.multiple) {
if (multipleTest !== test) {
multipleTest = test;
multipleErr = [test.err].concat(test.err.multiple);
}
err = multipleErr.shift();
} else {
err = test.err;
}
var message;
if (err.message && typeof err.message.toString === 'function') {
message = err.message + '';
} else if (typeof err.inspect === 'function') {
message = err.inspect() + '';
} else {
message = '';
}
var stack = err.stack || message;
var index = message ? stack.indexOf(message) : -1;
if (index === -1) {
msg = message;
} else {
index += message.length;
msg = stack.slice(0, index);
// remove msg from stack
stack = stack.slice(index + 1);
}
// uncaught
if (err.uncaught) {
msg = 'Uncaught ' + msg;
}
// explicitly show diff
if (Base.showDiff(err)) {
self.stringifyDiffObjs(err);
fmt =
color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n');
var match = message.match(/^([^:]+): expected/);
msg = '\n ' + color('error message', match ? match[1] : msg);
msg += Base.generateDiff(err.actual, err.expected);
}
// indent stack trace
stack = stack.replace(/^/gm, ' ');
// indented test title
var testTitle = '';
test.titlePath().forEach(function(str, index) {
if (index !== 0) {
testTitle += '\n ';
}
for (var i = 0; i < index; i++) {
testTitle += ' ';
}
testTitle += str;
});
Base.consoleLog(fmt, i + 1, testTitle, msg, stack);
self.showDetails(err);
});
};
Spec.prototype.stringifyDiffObjs = function(err) {
if (!isString(err.actual) || !isString(err.expected)) {
err.actual = stringify(err.actual);
err.expected = stringify(err.expected);
}
}
Spec.prototype.showDetails = function(err) {
if (!err.details) {
return;
}
const details = JSON
.stringify(err.details, null, 2)
.replace(/^/gm, ' ');
Base.consoleLog(
color('error stack', '\n Details:\n%s'),
details
);
}
exports = module.exports = Spec;

View File

@@ -7,6 +7,9 @@ const testDefinitions = require('../e2e-specs');
global.should = chai.should();
let driver;
let allPassed = true;
describe('Advanced HTTP e2e test suite', function () {
const isSauceLabs = !!process.env.SAUCE_USERNAME;
const isBrowserStack = !!process.env.BROWSERSTACK_USERNAME;
@@ -17,9 +20,6 @@ describe('Advanced HTTP e2e test suite', function () {
const targetInfo = { isSauceLabs, isBrowserStack, isDevice, isAndroid };
const environment = isSauceLabs ? 'saucelabs' : isBrowserStack ? 'browserstack' : 'local';
let driver;
let allPassed = true;
this.timeout(15000);
this.slow(4000);
@@ -121,7 +121,15 @@ async function waitToBeFinished(driver, timeout) {
async function validateResult(driver, validationFunc, targetInfo) {
const result = await driver.safeExecute('app.lastResult');
validationFunc(driver, result, targetInfo);
try {
validationFunc(driver, result, targetInfo);
} catch (error) {
allPassed = false;
error.details = result;
throw error;
}
}
async function checkSkipped(driver) {