diff --git a/CHANGELOG.md b/CHANGELOG.md
index f567ecb..335d0fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,14 @@
# Changelog
+## 2.5.0
+
+- Feature #56: add support for X.509 client certificate based authentication
+
## 2.4.1
- Fixed #296: multipart requests are not serialized on browser platform
+- Fixed #301: data is not decoded correctly when responseType is "json" (thanks antikalk)
+- Fixed #300: FormData object containing null or undefined value is not serialized correctly
## 2.4.0
diff --git a/README.md b/README.md
index 95980c0..4135de8 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ This is a fork of [Wymsee's Cordova-HTTP plugin](https://github.com/wymsee/cordo
- SSL / TLS Pinning
- CORS restrictions do not apply
+ - X.509 client certificate based authentication
- Handling of HTTP code 401 - read more at [Issue CB-2415](https://issues.apache.org/jira/browse/CB-2415)
## Updates
@@ -114,7 +115,7 @@ This defaults to `urlencoded`. You can also override the default content type he
:warning: `multipart` depends on several Web API standards which need to be supported in your web view. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.
### setRequestTimeout
-Set how long to wait for a request to respond, in seconds.
+Set the "read" timeout in seconds. This is the timeout interval to use when waiting for additional data.
```js
cordova.plugin.http.setRequestTimeout(5.0);
@@ -186,6 +187,29 @@ cordova.plugin.http.setServerTrustMode('nocheck', function() {
});
```
+### setClientAuthMode
+Configure X.509 client certificate authentication. Takes mode and options. `mode` being one of following values:
+
+* `none`: disable client certificate authentication
+* `systemstore` (only on Android): use client certificate installed in the Android system store; user will be presented with a list of all installed certificates
+* `buffer`: use given client certificate; you will need to provide an options object:
+ * `rawPkcs`: ArrayBuffer containing raw PKCS12 container with client certificate and private key
+ * `pkcsPassword`: password of the PKCS container
+
+```js
+ // enable client auth using PKCS12 container given in ArrayBuffer `myPkcs12ArrayBuffer`
+ cordova.plugin.http.setClientAuthMode('buffer', {
+ rawPkcs: myPkcs12ArrayBuffer,
+ pkcsPassword: 'mySecretPassword'
+ }, success, fail);
+
+ // enable client auth using certificate in system store (only on Android)
+ cordova.plugin.http.setClientAuthMode('systemstore', {}, success, fail);
+
+ // disable client auth
+ cordova.plugin.http.setClientAuthMode('none', {}, success, fail);
+```
+
### disableRedirect (deprecated)
This function was deprecated in 2.0.9. Use ["setFollowRedirect"](#setFollowRedirect) instead.
diff --git a/package-lock.json b/package-lock.json
index 2302d82..4e0beed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "cordova-plugin-advanced-http",
- "version": "2.4.0",
+ "version": "2.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 8548a50..c29ce3d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cordova-plugin-advanced-http",
- "version": "2.4.1",
+ "version": "2.5.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",
@@ -69,4 +69,4 @@
"wd": "1.4.1",
"xml2js": "0.4.19"
}
-}
+}
\ No newline at end of file
diff --git a/plugin.xml b/plugin.xml
index 0c08d4d..1918419 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -1,5 +1,5 @@
-
+
Advanced HTTP plugin
Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning
diff --git a/scripts/test-app.sh b/scripts/test-app.sh
index 245376b..4482e5e 100755
--- a/scripts/test-app.sh
+++ b/scripts/test-app.sh
@@ -3,8 +3,8 @@ set -e
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd ..; pwd )"
-if [ $CI == "true" ] && ([ -z $SAUCE_USERNAME ] || [ -z $SAUCE_ACCESS_KEY ]); then
- echo "Skipping CI tests, because Saucelabs credentials are not set.";
+if [ $CI == "true" ] && ([ -z $SAUCE_USERNAME ] || [ -z $SAUCE_ACCESS_KEY ]) && ([ -z $BROWSERSTACK_USERNAME ] || [ -z $BROWSERSTACK_ACCESS_KEY ]); then
+ echo "Skipping CI tests, because Saucelabs and BrowserStack credentials are not set.";
exit 0;
fi
diff --git a/src/ios/CordovaHttpPlugin.h b/src/ios/CordovaHttpPlugin.h
index 3b35e17..0e5d867 100644
--- a/src/ios/CordovaHttpPlugin.h
+++ b/src/ios/CordovaHttpPlugin.h
@@ -5,6 +5,7 @@
@interface CordovaHttpPlugin : CDVPlugin
- (void)setServerTrustMode:(CDVInvokedUrlCommand*)command;
+- (void)setClientAuthMode:(CDVInvokedUrlCommand*)command;
- (void)post:(CDVInvokedUrlCommand*)command;
- (void)put:(CDVInvokedUrlCommand*)command;
- (void)patch:(CDVInvokedUrlCommand*)command;
diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m
index 2f8c750..d7f8fcc 100644
--- a/src/ios/CordovaHttpPlugin.m
+++ b/src/ios/CordovaHttpPlugin.m
@@ -21,6 +21,7 @@
@implementation CordovaHttpPlugin {
AFSecurityPolicy *securityPolicy;
+ NSURLCredential *x509Credential;
}
- (void)pluginInitialize {
@@ -39,6 +40,33 @@
}
}
+- (void)setupAuthChallengeBlock:(AFHTTPSessionManager*)manager {
+ [manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(
+ NSURLSession * _Nonnull session,
+ NSURLAuthenticationChallenge * _Nonnull challenge,
+ NSURLCredential * _Nullable __autoreleasing * _Nullable credential
+ ) {
+ if ([challenge.protectionSpace.authenticationMethod isEqualToString: NSURLAuthenticationMethodServerTrust]) {
+ *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
+
+ if (![self->securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
+ return NSURLSessionAuthChallengeRejectProtectionSpace;
+ }
+
+ if (credential) {
+ return NSURLSessionAuthChallengeUseCredential;
+ }
+ }
+
+ if ([challenge.protectionSpace.authenticationMethod isEqualToString: NSURLAuthenticationMethodClientCertificate] && self->x509Credential) {
+ *credential = self->x509Credential;
+ return NSURLSessionAuthChallengeUseCredential;
+ }
+
+ return NSURLSessionAuthChallengePerformDefaultHandling;
+ }];
+}
+
- (void)setRequestHeaders:(NSDictionary*)headers forManager:(AFHTTPSessionManager*)manager {
[headers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[manager.requestSerializer setValue:obj forHTTPHeaderField:key];
@@ -147,7 +175,6 @@
- (void)executeRequestWithoutData:(CDVInvokedUrlCommand*)command withMethod:(NSString*) method {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
@@ -156,6 +183,7 @@
NSString *responseType = [command.arguments objectAtIndex:4];
[self setRequestSerializer: @"default" forManager: manager];
+ [self setupAuthChallengeBlock: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
@@ -199,7 +227,6 @@
- (void)executeRequestWithData:(CDVInvokedUrlCommand*)command withMethod:(NSString*)method {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *data = [command.arguments objectAtIndex:1];
@@ -210,6 +237,7 @@
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestSerializer: serializerName forManager: manager];
+ [self setupAuthChallengeBlock: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
@@ -302,6 +330,51 @@
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
+- (void)setClientAuthMode:(CDVInvokedUrlCommand*)command {
+ CDVPluginResult* pluginResult;
+ NSString *mode = [command.arguments objectAtIndex:0];
+
+ if ([mode isEqualToString:@"none"]) {
+ x509Credential = nil;
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+ }
+
+ if ([mode isEqualToString:@"systemstore"]) {
+ NSString *alias = [command.arguments objectAtIndex:1];
+
+ // TODO
+
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"mode 'systemstore' is not supported on iOS"];
+ }
+
+ if ([mode isEqualToString:@"buffer"]) {
+ CFDataRef container = (__bridge CFDataRef) [command.arguments objectAtIndex:2];
+ CFStringRef password = (__bridge CFStringRef) [command.arguments objectAtIndex:3];
+
+ const void *keys[] = { kSecImportExportPassphrase };
+ const void *values[] = { password };
+
+ CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
+ CFArrayRef items;
+ OSStatus securityError = SecPKCS12Import(container, options, &items);
+ CFRelease(options);
+
+ if (securityError != noErr) {
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR];
+ } else {
+ CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
+ SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
+
+ self->x509Credential = [NSURLCredential credentialWithIdentity:identity certificates: nil persistence:NSURLCredentialPersistenceForSession];
+ CFRelease(items);
+
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+ }
+ }
+
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+}
+
- (void)post:(CDVInvokedUrlCommand*)command {
[self executeRequestWithData: command withMethod:@"POST"];
}
@@ -332,7 +405,6 @@
- (void)uploadFiles:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
@@ -343,6 +415,7 @@
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestHeaders: headers forManager: manager];
+ [self setupAuthChallengeBlock: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
@@ -392,7 +465,6 @@
- (void)downloadFile:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- manager.securityPolicy = securityPolicy;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSString *url = [command.arguments objectAtIndex:0];
@@ -402,6 +474,7 @@
bool followRedirect = [[command.arguments objectAtIndex:4] boolValue];
[self setRequestHeaders: headers forManager: manager];
+ [self setupAuthChallengeBlock: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
diff --git a/test/e2e-specs.js b/test/e2e-specs.js
index ec1349d..b3691c3 100644
--- a/test/e2e-specs.js
+++ b/test/e2e-specs.js
@@ -3,7 +3,7 @@ const hooks = {
cordova.plugin.http.clearCookies();
helpers.enableFollowingRedirect(function() {
- // server trust mode is not supported on brpwser platform
+ // server trust mode is not supported on browser platform
if (cordova.platformId === 'browser') {
return resolve();
}
@@ -787,7 +787,7 @@ const tests = [
},
{
description: 'should decode error body even if response type is "arraybuffer"',
- expected: 'rejected: {"status": 418, ...',
+ expected: 'rejected: {"status":418, ...',
func: function (resolve, reject) {
var url = 'https://httpbin.org/status/418';
var options = { method: 'get', responseType: 'arraybuffer' };
@@ -801,7 +801,7 @@ const tests = [
},
{
description: 'should serialize FormData instance correctly when it contains string value',
- expected: 'resolved: {"status": 200, ...',
+ expected: 'resolved: {"status":200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
@@ -820,7 +820,7 @@ const tests = [
},
{
description: 'should serialize FormData instance correctly when it contains blob value',
- expected: 'resolved: {"status": 200, ...',
+ expected: 'resolved: {"status":200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
@@ -901,18 +901,72 @@ const tests = [
should.equal(null, result.data.data);
}
},
+ {
+ description: 'should decode JSON data correctly when response type is "json" #301',
+ expected: 'resolved: {"status":200,"data":{"slideshow": ... ',
+ func: function (resolve, reject) {
+ var url = 'https://httpbin.org/json';
+ var options = { method: 'get', responseType: 'json' };
+ cordova.plugin.http.sendRequest(url, options, resolve, reject);
+ },
+ validationFunc: function (driver, result) {
+ result.type.should.be.equal('resolved');
+ result.data.status.should.be.equal(200);
+ result.data.data.should.be.an('object');
+ result.data.data.slideshow.should.be.eql({
+ author: 'Yours Truly',
+ date: 'date of publication',
+ slides: [
+ {
+ title: 'Wake up to WonderWidgets!',
+ type: 'all'
+ },
+ {
+ items: [
+ 'Why WonderWidgets are great',
+ 'Who buys WonderWidgets'
+ ],
+ title: 'Overview',
+ type: 'all'
+ }
+ ],
+ title: 'Sample Slide Show'
+ });
+ }
+ },
+ {
+ description: 'should serialize FormData instance correctly when it contains null or undefined value #300',
+ expected: 'resolved: {"status":200, ...',
+ before: helpers.setMultipartSerializer,
+ func: function (resolve, reject) {
+ var ponyfills = cordova.plugin.http.ponyfills;
+ var formData = new ponyfills.FormData();
+ formData.append('myNullValue', null);
+ formData.append('myUndefinedValue', undefined);
- // 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');
- // }
- // }
+ var url = 'https://httpbin.org/anything';
+ var options = { method: 'post', data: formData };
+ cordova.plugin.http.sendRequest(url, options, resolve, reject);
+ },
+ validationFunc: function (driver, result) {
+ helpers.checkResult(result, 'resolved');
+ result.data.status.should.be.equal(200);
+ JSON.parse(result.data.data).form.should.be.eql({
+ myNullValue: 'null',
+ myUndefinedValue: 'undefined'
+ });
+ }
+ },
+ {
+ 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/e2e-tooling/caps.js b/test/e2e-tooling/caps.js
index 0548853..fa92769 100644
--- a/test/e2e-tooling/caps.js
+++ b/test/e2e-tooling/caps.js
@@ -67,8 +67,8 @@ const configs = {
app: 'HttpTestAppAndroid'
},
browserstackAndroidDevice: {
- device: 'Google Nexus 9',
- os_version: '5.1',
+ device: 'Google Nexus 6',
+ os_version: '5.0',
project: 'HTTP Test App',
autoWebview: true,
app: 'HttpTestAppAndroid'
diff --git a/www/ponyfills.js b/www/ponyfills.js
index 67b9bbc..e49e6d7 100644
--- a/www/ponyfills.js
+++ b/www/ponyfills.js
@@ -18,7 +18,7 @@ module.exports = function init(global) {
value.lastModifiedDate = new Date();
value.name = filename || '';
} else {
- value = value.toString ? value.toString() : value;
+ value = String(value);
}
this.__items.push([ name, value ]);