Compare commits

...

73 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
Sefa Ilkimen
94126b9021 release v3.1.1 2021-07-07 12:41:00 +02:00
Sefa Ilkimen
007d5c609f chore: reduce false positives in e2e tests 2021-07-07 05:07:34 +02:00
Sefa Ilkimen
badf6dcdc2 fix: #372 [Bug] Android: malformed empty multipart requests 2021-07-07 03:16:43 +02:00
Sefa Ilkimen
c081060a9e fix: broken e2e tests caused by wrong plugin entries in app template 2021-07-07 03:15:21 +02:00
Sefa Ilkimen
2ce130133c - chore: update Travis CI badge
- chore: update Travis CI Xcode image version
2021-07-06 22:40:58 +02:00
Sefa Ilkimen
cfcc572ca0 chore(CI): remove slack integration 2021-07-06 19:08:54 +02:00
Sefa Ilkimen
d7688b485d fix: e2e test returns false positive due to test timing (request finished to early) 2021-03-24 06:54:59 +01:00
Sefa Ilkimen
81ba667e37 fix: broken e2e spec 2021-03-24 06:43:42 +01:00
Sefa Ilkimen
5ad1967b46 Merge branch 'master' of https://github.com/silkimen/cordova-plugin-advanced-http 2021-03-24 06:24:39 +01:00
Sefa Ilkimen
90ed474b29 docs: update changelog 2021-03-24 06:24:20 +01:00
Sefa Ilkimen
1947906c4c Merge pull request #399 from avargaskun/master
Fix: Memory leak on iOS
2021-03-24 06:22:08 +01:00
Sefa Ilkimen
b25b07a2a7 chore: update appium caps for local testing 2021-03-24 06:18:33 +01:00
Sefa Ilkimen
a01625ecfb chore: update requested appium version for saucelabs build 2021-03-24 06:06:58 +01:00
Sefa Ilkimen
b03ae7d6d1 chore: update appium capabilites for testing on saucelabs 2021-03-24 05:53:22 +01:00
Sefa Ilkimen
fdaea418be chore: update capabilities for e2e tests running on browser stack
- increase test device OS versions
2021-03-24 05:35:45 +01:00
Sefa Ilkimen
d84df0fb00 chore: update travis CI pipeline
- use node 14
- use `xcode12.2` image for iOS build
2021-03-24 05:25:20 +01:00
Sefa Ilkimen
baedd60329 chore: update cordova cli and cordova platform versions in e2e template 2021-03-24 05:12:00 +01:00
Antonio Vargas Garcia
80b22d4202 Fix: Memory leak on iOS
- Requests are leaking instances of the AFHTTPSessionManager
- Over time this causes iOS to terminate the app
- Inspiration for the fix: https://stackoverflow.com/a/41345142
2021-02-10 22:16:41 -08:00
Sefa Ilkimen
89ac260ef6 Merge pull request #390 from silkimen/dependabot/npm_and_yarn/ini-1.3.8
chore(deps): bump ini from 1.3.5 to 1.3.8
2021-01-18 03:58:02 +01:00
dependabot[bot]
6a266218fb chore(deps): bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-18 02:49:36 +00:00
Sefa Ilkimen
6050830423 Merge pull request #389 from ath0mas/fix/344-redirect-specs
Resolve #344 restore and fix redirect specs
2021-01-18 03:31:31 +01:00
Sefa Ilkimen
95c9eb85d0 Merge pull request #394 from ath0mas/feature/docs-local-testing
docs: update readme to include Local Testing
2021-01-18 02:28:45 +01:00
Alexis THOMAS
7e3ff25195 ignore idea files 2021-01-17 21:31:13 +01:00
Alexis THOMAS
8a6b9e583e docs: update readme to include Local Testing 2021-01-17 21:26:11 +01:00
Alexis THOMAS
b32dfc7143 fix #344: "disableRedirect" removed since 3.0, use "setFollowRedirect" 2021-01-17 18:26:37 +01:00
Alexis THOMAS
a72bf758c6 docs: link to go-httpbin 2020-12-10 00:06:32 +01:00
Alexis THOMAS
45f9cbfaa4 fix #344: change redirect specs to httpbingo.org 2020-12-10 00:04:18 +01:00
Sefa Ilkimen
f8f52e1e97 chore: add FUNDING.yml 2020-12-09 00:54:33 +01:00
Sefa Ilkimen
9b995724f4 chore: use node v12 in Travis CI builds 2020-10-19 04:32:01 +02:00
Sefa Ilkimen
25a0a9a7ae fix: #372 [Bug] Android: malformed empty multipart requests 2020-10-19 04:11:49 +02:00
Sefa Ilkimen
e44def06a5 refactor: some minor cleanup and refactoring 2020-10-19 01:35:05 +02:00
Sefa Ilkimen
3d288951bf chore: update dev dependencies 2020-10-19 00:19:48 +02:00
Sefa Ilkimen
f39063cec9 chore: disable CodeQL for Java 2020-10-19 00:10:49 +02:00
Sefa Ilkimen
1bcb8016ff Setup CodeQL Analysis 2020-10-18 23:58:36 +02:00
Sefa Ilkimen
6339b9b83d release v3.1.0 2020-10-16 00:58:40 +02:00
Sefa Ilkimen
389534d661 chore: update changelog 2020-10-13 00:46:48 +02:00
Sefa Ilkimen
c10722eca0 Merge branch 'mmig-feature-abort' 2020-10-13 00:23:34 +02:00
Sefa Ilkimen
6a60058fc6 chore: auto fix some indentations 2020-10-13 00:22:42 +02:00
russa
fcedfae1ac FIX do not evaluate test result if test is skipped 2020-10-09 16:51:43 +02:00
russa
aca165b900 added unsupported warning for abort for Android versions < 6 2020-10-08 21:17:26 +02:00
russa
09c2b383ff skip abort-tests if Android version is < 6 2020-10-08 21:10:45 +02:00
russa
6918a2ed15 added support for skipping tests 2020-10-08 21:08:30 +02:00
russa
5b827d500d improve handling for race condition that request finished before adding it to pending requests map 2020-10-08 21:06:56 +02:00
russa
1c27b62148 added configuration for allowing http (cleartext) requests on Android 7 and later (sdk >= 24) 2020-10-08 18:12:59 +02:00
Sefa Ilkimen
f33a911e7e Merge pull request #364 from silkimen/dependabot/npm_and_yarn/bl-4.0.3
chore(deps): bump bl from 4.0.2 to 4.0.3
2020-10-01 02:20:41 +02:00
russa
097faad07a removed unsupported warning for aborting requests on ios from README 2020-09-25 18:59:45 +02:00
russa
62c400c6db added support for aborting requests for ios platform 2020-09-25 18:56:15 +02:00
russa
f823d24438 added tests for abort 2020-09-16 19:51:29 +02:00
russa
64a7148444 FIX use same error message for abort as on android platform 2020-09-16 19:48:23 +02:00
russa
f6736d9150 added usage description for abort() 2020-09-16 18:37:15 +02:00
russa
bc90ae85fb added 'unsupported' feedback for abort() on ios platform 2020-09-16 18:36:55 +02:00
russa
389e860125 added support for abort() on browser platform 2020-09-16 18:35:01 +02:00
russa
2367d264c1 added support for abort() on android platform 2020-09-16 18:05:30 +02:00
russa
269d5d4c8a added abort() function to js-module 2020-09-16 17:44:59 +02:00
dependabot[bot]
b6ee4de379 chore(deps): bump bl from 4.0.2 to 4.0.3
Bumps [bl](https://github.com/rvagg/bl) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v4.0.2...v4.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-02 15:37:00 +00:00
Sefa Ilkimen
5f327dc82a release v3.0.1 2020-08-18 12:52:01 +02:00
Sefa Ilkimen
1639efe8d0 fix: #355 [Bug] [Browser] responseType "json" not working with valid JSON response 2020-08-18 02:16:03 +02:00
Sefa Ilkimen
9bb0c58e35 chore: bump version and update readme 2020-08-17 15:59:23 +02:00
Sefa Ilkimen
57562a0dcf fix: #359 [Bug] [Android] memory leakage leads to app crashes 2020-08-17 03:02:05 +02:00
Sefa Ilkimen
ad4079625e Merge pull request #354 from silkimen/dependabot/npm_and_yarn/lodash-4.17.19
chore(deps): bump lodash from 4.17.15 to 4.17.19
2020-08-17 00:47:29 +02:00
dependabot[bot]
98d3d38e07 chore(deps): bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-18 20:24:07 +00:00
34 changed files with 10165 additions and 2260 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: silkimen

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

62
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 20 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ npm-debug.log
/temp
/android-sdk-macosx.zip
/android-sdk-macosx/**
.idea/
*.iml

View File

@@ -1,7 +1,3 @@
notifications:
slack:
secure: twDT06GAiu0jsKizow7TcghZj70KbuuTrlo02QGmbSxBk2rJfsXSrHAsA3+s/9Q4mudENk6na7fs1aPCxz+u2etUGp+2PaJKVKR5n3jrNNt3SnYeWsBgVo7o7H1aLXatX3a+TdPXh1F5gQ4Ycr93nTYbW/077jsOholwbOHDZE3VcU9dzNPwFaEvhrDbr/ei3tef0ZiM1qxIad74TgwWMKClwai3I7HCVkZOPsyV+ve6cdIJ8Dt47JzFUHSW3SZuoe5Kywxvp0VvMo/QAJw95y3edNafx4EXHwbaN71rpGWSJXIKSZzcSQalZJ9DxGYspIBkWvGsNuQRzG9CzIoNQK10iERlIVC5vKDfKX22gayOQPSDkswJzIduylBUC8zdTPCndXyNEM/Lrj6hg+ksFWN58vYNPgfUeiga7X+LV5HytftsMFW+xx2kbnGeU8doGeX8Q8G7h9OIkHCTTG7R0ldYMIqTm8YJGPkRIv4OReC5ZOhiZD+wSg4KQ0wmMeRi+hyn+I5UPnKEOHAIN8FmLNCZFbgr1wuPFp9xnJIOcumQnQVZ2t6vk6IjIbwhYPWCnf7Sr4BvJxE8eyiLrEaXK0FiPb3My9wK9tLFjj1zdD7e4+SLq+WFMeCxp2eXOGF0Bu+2VK2tGjgWhaudaIpjbRQAAQ5nPa43h16NruEvNWI=
cache:
directories:
- node_modules
@@ -15,10 +11,11 @@ matrix:
language: objective-c
sudo: false
os: osx
osx_image: xcode10.1
osx_image: xcode12.5
before_install:
- export LANG=en_US.UTF-8
- export LANG=en_US.UTF-8 &&
nvm use 14
install:
- npm install
@@ -46,7 +43,7 @@ matrix:
before_install:
- export LANG=en_US.UTF-8 &&
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - &&
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - &&
sudo apt-get install -y nodejs
- yes | sdkmanager --update

View File

@@ -1,5 +1,23 @@
# 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
- Fixed #399: memory leakage leads to app crashes on iOS (thanks avargaskun)
## 3.1.0
- Feature #272: add support for aborting requests (thanks russaa)
## 3.0.1
- Fixed #359: memory leakage leads to app crashes on Android
- Fixed #355: responseType "json" not working with valid JSON response on browser (thanks millerg6711)
## 3.0.0
- Feature #158: support removing headers which were previously set via "setHeader"

View File

@@ -4,7 +4,7 @@ Cordova Advanced HTTP
[![MIT Licence](https://img.shields.io/badge/license-MIT-blue?style=flat)](https://opensource.org/licenses/mit-license.php)
[![downloads/month](https://img.shields.io/npm/dm/cordova-plugin-advanced-http.svg)](https://www.npmjs.com/package/cordova-plugin-advanced-http)
[![Travis Build Status](https://img.shields.io/travis/silkimen/cordova-plugin-advanced-http/master?label=Travis%20CI)](https://travis-ci.org/silkimen/cordova-plugin-advanced-http)
[![Travis Build Status](https://img.shields.io/travis/com/silkimen/cordova-plugin-advanced-http?label=Travis%20CI)](https://travis-ci.com/silkimen/cordova-plugin-advanced-http)
[![GitHub Build Status](https://img.shields.io/github/workflow/status/silkimen/cordova-plugin-advanced-http/Cordova%20HTTP%20Plugin%20CI/master?label=GitHub%20Actions)](https://github.com/silkimen/cordova-plugin-advanced-http/actions)
@@ -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
@@ -404,6 +413,46 @@ cordova.plugin.http.downloadFile("https://google.com/", {
});
```
### abort<a name="abort"></a>
Abort a HTTP request. Takes the `requestId` which is returned by [sendRequest](#sendRequest) and its shorthand functions ([post](#post), [get](#get), [put](#put), [patch](#patch), [delete](#delete), [head](#head), [uploadFile](#uploadFile) and [downloadFile](#downloadFile)).
If the request already has finished, the request will finish normally and the abort call result will be `{ aborted: false }`.
If the request is still in progress, the request's `failure` callback will be invoked with response `{ status: -8 }`, and the abort call result `{ aborted: true }`.
:warning: Not supported for Android < 6 (API level < 23). For Android 5.1 and below, calling `abort(reqestId)` will have no effect, i.e. the requests will finish as if the request was not cancelled.
```js
// start a request and get its requestId
var requestId = cordova.plugin.http.downloadFile("https://google.com/", {
id: '12',
message: 'test'
}, { Authorization: 'OAuth2: token' }, 'file:///somepicture.jpg', function(entry) {
// prints the filename
console.log(entry.name);
// prints the filePath
console.log(entry.fullPath);
}, function(response) {
// if request was actually aborted, failure callback with status -8 will be invoked
if(response.status === -8){
console.log('download aborted');
} else {
console.error(response.error);
}
});
//...
// abort request
cordova.plugin.http.abort(requestId, function(result) {
// prints if request was aborted: true | false
console.log(result.aborted);
}, function(response) {
console.error(response.error);
});
```
## Browser support<a name="browserSupport"></a>
This plugin supports a very restricted set of functions on the browser platform.
@@ -437,6 +486,28 @@ This plugin uses amazing cloud services to maintain quality. CI Builds and E2E t
* [BrowserStack](https://www.browserstack.com/)
* [Sauce Labs](https://saucelabs.com/)
* [httpbin.org](https://httpbin.org/)
* [go-httpbin](https://httpbingo.org/)
### Local Testing
First, install current package with `npm install` to fetch dev dependencies.
Then, to execute Javascript tests:
```shell
npm run testjs
```
And, to execute E2E tests:
- setup local Android sdk and emulators, or Xcode and simulators for iOS
- launch emulator or simulator
- install [Appium](http://appium.io/) (see [Getting Started](https://github.com/appium/appium/blob/HEAD/docs/en/about-appium/getting-started.md))
- start `appium`
- run
- updating client and server certificates, building test app, and running e2e tests
```shell
npm run testandroid
npm run testios
```
## Contribute & Develop

11147
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "cordova-plugin-advanced-http",
"version": "3.0.0",
"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",
@@ -60,8 +60,8 @@
"devDependencies": {
"chai": "4.2.0",
"colors": "1.4.0",
"cordova": "9.0.0",
"mocha": "8.0.1",
"cordova": "10.0.0",
"mocha": "8.2.0",
"umd-tough-cookie": "2.4.3",
"wd": "1.12.1",
"xml2js": "0.4.23"

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.0.0">
<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"/>
@@ -74,6 +75,7 @@
<source-file src="src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java" target-dir="src/com/silkimen/cordovahttp"/>
<source-file src="src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java" target-dir="src/com/silkimen/cordovahttp"/>
<source-file src="src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java" target-dir="src/com/silkimen/cordovahttp"/>
<source-file src="src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java" target-dir="src/com/silkimen/cordovahttp"/>
<source-file src="src/android/com/silkimen/cordovahttp/CordovaServerTrust.java" target-dir="src/com/silkimen/cordovahttp"/>
<source-file src="src/android/com/silkimen/http/HttpBodyDecoder.java" target-dir="src/com/silkimen/http"/>
<source-file src="src/android/com/silkimen/http/HttpRequest.java" target-dir="src/com/silkimen/http"/>

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

@@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
@@ -18,8 +19,6 @@ import com.silkimen.http.HttpRequest.HttpRequestException;
import com.silkimen.http.JsonUtils;
import com.silkimen.http.TLSConfiguration;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -39,11 +38,11 @@ abstract class CordovaHttpBase implements Runnable {
protected int timeout;
protected boolean followRedirects;
protected TLSConfiguration tlsConfiguration;
protected CallbackContext callbackContext;
protected CordovaObservableCallbackContext callbackContext;
public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout,
boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
CallbackContext callbackContext) {
CordovaObservableCallbackContext callbackContext) {
this.method = method;
this.url = url;
@@ -58,7 +57,7 @@ abstract class CordovaHttpBase implements Runnable {
}
public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects,
String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) {
String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) {
this.method = method;
this.url = url;
@@ -73,30 +72,39 @@ abstract class CordovaHttpBase implements Runnable {
@Override
public void run() {
CordovaHttpResponse response = new CordovaHttpResponse();
HttpRequest request = null;
try {
HttpRequest request = this.createRequest();
request = this.createRequest();
this.prepareRequest(request);
this.sendBody(request);
this.processResponse(request, response);
request.disconnect();
} catch (HttpRequestException e) {
if (e.getCause() instanceof SSLException) {
Throwable cause = e.getCause();
String message = cause.getMessage();
if (cause instanceof SSLException) {
response.setStatus(-2);
response.setErrorMessage("TLS connection could not be established: " + e.getMessage());
Log.w(TAG, "TLS connection could not be established", e);
} else if (e.getCause() instanceof UnknownHostException) {
} else if (cause instanceof UnknownHostException) {
response.setStatus(-3);
response.setErrorMessage("Host could not be resolved: " + e.getMessage());
Log.w(TAG, "Host could not be resolved", e);
} else if (e.getCause() instanceof SocketTimeoutException) {
} else if (cause instanceof SocketTimeoutException) {
response.setStatus(-4);
response.setErrorMessage("Request timed out: " + e.getMessage());
Log.w(TAG, "Request timed out", e);
} else if (cause instanceof InterruptedIOException && "thread interrupted".equals(message.toLowerCase())) {
this.setAborted(request, response);
} else {
response.setStatus(-1);
response.setErrorMessage("There was an error with the request: " + e.getCause().getMessage());
response.setErrorMessage("There was an error with the request: " + message);
Log.w(TAG, "Generic request error", e);
}
} catch (InterruptedException ie) {
this.setAborted(request, response);
} catch (Exception e) {
response.setStatus(-1);
response.setErrorMessage(e.getMessage());
@@ -146,7 +154,7 @@ abstract class CordovaHttpBase implements Runnable {
} else if ("urlencoded".equals(this.serializer)) {
// intentionally left blank, because content type is set in HttpRequest.form()
} else if ("multipart".equals(this.serializer)) {
request.contentType("multipart/form-data");
// intentionally left blank, because content type is set in HttpRequest.part()
}
}
@@ -179,6 +187,12 @@ abstract class CordovaHttpBase implements Runnable {
request.part(name, fileNames.getString(i), types.getString(i), new ByteArrayInputStream(bytes));
}
}
// prevent sending malformed empty multipart requests (#372)
if (buffers.length() == 0) {
request.contentType("multipart/form-data; boundary=00content0boundary00");
request.send("\r\n--00content0boundary00--\r\n");
}
}
}
@@ -201,4 +215,19 @@ abstract class CordovaHttpBase implements Runnable {
response.setErrorMessage(HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset()));
}
}
protected void setAborted(HttpRequest request, CordovaHttpResponse response) {
response.setStatus(-8);
response.setErrorMessage("Request was aborted");
if (request != null) {
try {
request.disconnect();
} catch(Exception any){
Log.w(TAG, "Failed to close aborted request", any);
}
}
Log.i(TAG, "Request was aborted");
}
}

View File

@@ -9,7 +9,6 @@ import javax.net.ssl.SSLSocketFactory;
import com.silkimen.http.HttpRequest;
import com.silkimen.http.TLSConfiguration;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.file.FileUtils;
import org.json.JSONObject;
@@ -17,7 +16,7 @@ class CordovaHttpDownload extends CordovaHttpBase {
private String filePath;
public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects,
TLSConfiguration tlsConfiguration, CallbackContext callbackContext) {
TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) {
super("GET", url, headers, timeout, followRedirects, "text", tlsConfiguration, callbackContext);
this.filePath = filePath;

View File

@@ -5,20 +5,19 @@ import javax.net.ssl.SSLSocketFactory;
import com.silkimen.http.TLSConfiguration;
import org.apache.cordova.CallbackContext;
import org.json.JSONObject;
class CordovaHttpOperation extends CordovaHttpBase {
public CordovaHttpOperation(String method, String url, String serializer, Object data, JSONObject headers,
int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
CallbackContext callbackContext) {
CordovaObservableCallbackContext callbackContext) {
super(method, url, serializer, data, headers, timeout, followRedirects, responseType, tlsConfiguration,
callbackContext);
}
public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects,
String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) {
String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) {
super(method, url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext);
}

View File

@@ -1,6 +1,10 @@
package com.silkimen.cordovahttp;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.Future;
import com.silkimen.http.TLSConfiguration;
@@ -17,17 +21,22 @@ import android.util.Base64;
import javax.net.ssl.TrustManagerFactory;
public class CordovaHttpPlugin extends CordovaPlugin {
public class CordovaHttpPlugin extends CordovaPlugin implements Observer {
private static final String TAG = "Cordova-Plugin-HTTP";
private TLSConfiguration tlsConfiguration;
private HashMap<Integer, Future<?>> reqMap;
private final Object reqMapLock = new Object();
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
this.tlsConfiguration = new TLSConfiguration();
this.reqMap = new HashMap<Integer, Future<?>>();
try {
KeyStore store = KeyStore.getInstance("AndroidCAStore");
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
@@ -38,6 +47,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
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);
}
@@ -73,6 +89,8 @@ public class CordovaHttpPlugin extends CordovaPlugin {
return this.setServerTrustMode(args, callbackContext);
} else if ("setClientAuthMode".equals(action)) {
return this.setClientAuthMode(args, callbackContext);
} else if ("abort".equals(action)) {
return this.abort(args, callbackContext);
} else {
return false;
}
@@ -87,10 +105,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(3);
String responseType = args.getString(4);
CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect,
responseType, this.tlsConfiguration, callbackContext);
Integer reqId = args.getInt(5);
CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
cordova.getThreadPool().execute(request);
CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect,
responseType, this.tlsConfiguration, observableCallbackContext);
startRequest(reqId, observableCallbackContext, request);
return true;
}
@@ -106,10 +127,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(5);
String responseType = args.getString(6);
CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers,
timeout, followRedirect, responseType, this.tlsConfiguration, callbackContext);
Integer reqId = args.getInt(7);
CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
cordova.getThreadPool().execute(request);
CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers,
timeout, followRedirect, responseType, this.tlsConfiguration, observableCallbackContext);
startRequest(reqId, observableCallbackContext, request);
return true;
}
@@ -123,10 +147,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(5);
String responseType = args.getString(6);
CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect,
responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), callbackContext);
Integer reqId = args.getInt(7);
CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
cordova.getThreadPool().execute(upload);
CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect,
responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), observableCallbackContext);
startRequest(reqId, observableCallbackContext, upload);
return true;
}
@@ -138,14 +165,25 @@ public class CordovaHttpPlugin extends CordovaPlugin {
int timeout = args.getInt(3) * 1000;
boolean followRedirect = args.getBoolean(4);
CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect,
this.tlsConfiguration, callbackContext);
Integer reqId = args.getInt(5);
CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
cordova.getThreadPool().execute(download);
CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect,
this.tlsConfiguration, observableCallbackContext);
startRequest(reqId, observableCallbackContext, download);
return true;
}
private void startRequest(Integer reqId, CordovaObservableCallbackContext observableCallbackContext, CordovaHttpBase request) {
synchronized (reqMapLock) {
observableCallbackContext.setObserver(this);
Future<?> task = cordova.getThreadPool().submit(request);
this.addReq(reqId, task, observableCallbackContext);
}
}
private boolean setServerTrustMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
CordovaServerTrust runnable = new CordovaServerTrust(args.getString(0), this.cordova.getActivity(),
this.tlsConfiguration, callbackContext);
@@ -166,4 +204,45 @@ public class CordovaHttpPlugin extends CordovaPlugin {
return true;
}
private boolean abort(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
int reqId = args.getInt(0);
boolean result = false;
// NOTE no synchronized (reqMapLock), since even if the req was already removed from reqMap,
// the worst that would happen calling task.cancel(true) is a result of false
// (i.e. same result as locking & not finding the req in reqMap)
Future<?> task = this.reqMap.get(reqId);
if (task != null && !task.isDone()) {
result = task.cancel(true);
}
callbackContext.success(new JSONObject().put("aborted", result));
return true;
}
private void addReq(final Integer reqId, final Future<?> task, final CordovaObservableCallbackContext observableCallbackContext) {
synchronized (reqMapLock) {
if (!task.isDone()){
this.reqMap.put(reqId, task);
}
}
}
private void removeReq(final Integer reqId) {
synchronized (reqMapLock) {
this.reqMap.remove(reqId);
}
}
@Override
public void update(Observable o, Object arg) {
synchronized (reqMapLock) {
CordovaObservableCallbackContext c = (CordovaObservableCallbackContext) arg;
if (c.getCallbackContext().isFinished()) {
removeReq(c.getRequestId());
}
}
}
}

View File

@@ -17,7 +17,6 @@ import java.net.URI;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -28,7 +27,7 @@ class CordovaHttpUpload extends CordovaHttpBase {
public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout,
boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
Context applicationContext, CallbackContext callbackContext) {
Context applicationContext, CordovaObservableCallbackContext callbackContext) {
super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext);
this.filePaths = filePaths;

View File

@@ -0,0 +1,58 @@
package com.silkimen.cordovahttp;
import org.apache.cordova.CallbackContext;
import org.json.JSONObject;
import java.util.Observer;
public class CordovaObservableCallbackContext {
private CallbackContext callbackContext;
private Integer requestId;
private Observer observer;
public CordovaObservableCallbackContext(CallbackContext callbackContext, Integer requestId) {
this.callbackContext = callbackContext;
this.requestId = requestId;
}
public void success(JSONObject message) {
this.callbackContext.success(message);
this.notifyObserver();
}
public void error(JSONObject message) {
this.callbackContext.error(message);
this.notifyObserver();
}
public Integer getRequestId() {
return this.requestId;
}
public CallbackContext getCallbackContext() {
return callbackContext;
}
public Observer getObserver() {
return observer;
}
protected void notifyObserver() {
if(this.observer != null){
this.observer.update(null, this);
}
}
/**
* Set an observer that is notified, when {@link #success(JSONObject)}
* or {@link #error(JSONObject)} are called.
*
* NOTE the observer is notified with
* <pre>observer.update(null, cordovaObservableCallbackContext)</pre>
* @param observer
*/
public void setObserver(Observer observer) {
this.observer = observer;
}
}

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

@@ -3,6 +3,8 @@ var pluginId = module.id.slice(0, module.id.lastIndexOf('.'));
var cordovaProxy = require('cordova/exec/proxy');
var jsUtil = require(pluginId + '.js-util');
var reqMap = {};
function serializeJsonData(data) {
try {
return JSON.stringify(data);
@@ -20,7 +22,7 @@ function serializePrimitive(key, value) {
}
function serializeArray(key, values) {
return values.map(function(value) {
return values.map(function (value) {
return encodeURIComponent(key) + '[]=' + encodeURIComponent(value);
}).join('&');
}
@@ -28,7 +30,7 @@ function serializeArray(key, values) {
function serializeParams(params) {
if (params === null) return '';
return Object.keys(params).map(function(key) {
return Object.keys(params).map(function (key) {
if (jsUtil.getTypeOf(params[key]) === 'Array') {
return serializeArray(key, params[key]);
}
@@ -38,11 +40,11 @@ function serializeParams(params) {
}
function decodeB64(dataString) {
var binarString = atob(dataString);
var bytes = new Uint8Array(binarString.length);
var binaryString = atob(dataString);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binarString.length; ++i) {
bytes[i] = binarString.charCodeAt(i);
for (var i = 0; i < binaryString.length; ++i) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
@@ -60,7 +62,7 @@ function processMultipartData(data) {
var type = data.types[i];
if (fileName) {
fd.append(name, new Blob([decodeB64(buffer)], {type: type}), fileName);
fd.append(name, new Blob([decodeB64(buffer)], { type: type }), fileName);
} else {
// we assume it's plain text if no filename was given
fd.append(name, atob(buffer));
@@ -115,10 +117,17 @@ function createXhrFailureObject(xhr) {
return obj;
}
function injectRequestIdHandler(reqId, cb) {
return function (response) {
delete reqMap[reqId];
cb(response);
}
}
function getHeaderValue(headers, headerName) {
let result = null;
Object.keys(headers).forEach(function(key) {
Object.keys(headers).forEach(function (key) {
if (key.toLowerCase() === headerName.toLowerCase()) {
result = headers[key];
}
@@ -134,7 +143,7 @@ function setDefaultContentType(headers, contentType) {
}
function setHeaders(xhr, headers) {
Object.keys(headers).forEach(function(key) {
Object.keys(headers).forEach(function (key) {
if (key.toLowerCase() === 'cookie') return;
xhr.setRequestHeader(key, headers[key]);
@@ -142,7 +151,7 @@ function setHeaders(xhr, headers) {
}
function sendRequest(method, withData, opts, success, failure) {
var data, serializer, headers, timeout, followRedirect, responseType;
var data, serializer, headers, timeout, followRedirect, responseType, reqId;
var url = opts[0];
if (withData) {
@@ -152,25 +161,31 @@ function sendRequest(method, withData, opts, success, failure) {
timeout = opts[4];
followRedirect = opts[5];
responseType = opts[6];
reqId = opts[7];
} else {
headers = opts[1];
timeout = opts[2];
followRedirect = opts[3];
responseType = opts[4];
reqId = opts[5];
}
var onSuccess = injectRequestIdHandler(reqId, success);
var onFail = injectRequestIdHandler(reqId, failure);
var processedData = null;
var xhr = new XMLHttpRequest();
reqMap[reqId] = xhr;
xhr.open(method, url);
if (headers.Cookie && headers.Cookie.length > 0) {
return failure('advanced-http: custom cookies not supported on browser platform');
return onFail('advanced-http: custom cookies not supported on browser platform');
}
if (!followRedirect) {
return failure('advanced-http: disabling follow redirect not supported on browser platform');
return onFail('advanced-http: disabling follow redirect not supported on browser platform');
}
switch (serializer) {
@@ -179,7 +194,7 @@ function sendRequest(method, withData, opts, success, failure) {
processedData = serializeJsonData(data);
if (processedData === null) {
return failure('advanced-http: failed serializing data');
return onFail('advanced-http: failed serializing data');
}
break;
@@ -196,7 +211,7 @@ function sendRequest(method, withData, opts, success, failure) {
case 'multipart':
const contentType = getHeaderValue(headers, 'Content-Type');
// intentionally don't set a default content type
// it's set by the browser together with the content disposition string
if (contentType) {
@@ -212,16 +227,26 @@ function sendRequest(method, withData, opts, success, failure) {
break;
}
// requesting text instead of JSON because it's parsed in the response handler
xhr.responseType = responseType === 'json' ? 'text' : responseType;
xhr.timeout = timeout * 1000;
xhr.responseType = responseType;
setHeaders(xhr, headers);
xhr.onerror = function () {
return failure(createXhrFailureObject(xhr));
return onFail(createXhrFailureObject(xhr));
};
xhr.ontimeout = function () {
return failure({
xhr.onabort = function () {
return onFail({
status: -8,
error: 'Request was aborted',
url: url,
headers: {}
});
};
xhr.ontimeout = function () {
return onFail({
status: -4,
error: 'Request timed out',
url: url,
@@ -233,15 +258,28 @@ function sendRequest(method, withData, opts, success, failure) {
if (xhr.readyState !== xhr.DONE) return;
if (xhr.status < 200 || xhr.status > 299) {
return failure(createXhrFailureObject(xhr));
return onFail(createXhrFailureObject(xhr));
}
return success(createXhrSuccessObject(xhr));
return onSuccess(createXhrSuccessObject(xhr));
};
xhr.send(processedData);
}
function abort(opts, success, failure) {
var reqId = opts[0];
var result = false;
var xhr = reqMap[reqId];
if(xhr && xhr.readyState !== xhr.DONE){
xhr.abort();
result = true;
}
success({aborted: result});
}
var browserInterface = {
get: function (success, failure, opts) {
return sendRequest('get', false, opts, success, failure);
@@ -261,6 +299,9 @@ var browserInterface = {
patch: function (success, failure, opts) {
return sendRequest('patch', true, opts, success, failure);
},
abort: function (success, failure, opts) {
return abort(opts, success, failure);
},
uploadFile: function (success, failure, opts) {
return failure('advanced-http: function "uploadFile" not supported on browser platform');
},

View File

@@ -15,5 +15,6 @@
- (void)options:(CDVInvokedUrlCommand*)command;
- (void)uploadFiles:(CDVInvokedUrlCommand*)command;
- (void)downloadFile:(CDVInvokedUrlCommand*)command;
- (void)abort:(CDVInvokedUrlCommand*)command;
@end

View File

@@ -9,6 +9,8 @@
@interface CordovaHttpPlugin()
- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task;
- (void)removeRequest:(NSNumber*)reqId;
- (void)setRequestHeaders:(NSDictionary*)headers forManager:(AFHTTPSessionManager*)manager;
- (void)handleSuccess:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response andData:(id)data;
- (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error;
@@ -22,10 +24,20 @@
@implementation CordovaHttpPlugin {
AFSecurityPolicy *securityPolicy;
NSURLCredential *x509Credential;
NSMutableDictionary *reqDict;
}
- (void)pluginInitialize {
securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
reqDict = [NSMutableDictionary dictionary];
}
- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task {
[reqDict setObject:task forKey:reqId];
}
- (void)removeRequest:(NSNumber*)reqId {
[reqDict removeObjectForKey:reqId];
}
- (void)setRequestSerializer:(NSString*)serializerName forManager:(AFHTTPSessionManager*)manager {
@@ -52,7 +64,7 @@
if (![self->securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
return NSURLSessionAuthChallengeRejectProtectionSpace;
}
if (credential) {
return NSURLSessionAuthChallengeUseCredential;
}
@@ -62,7 +74,7 @@
*credential = self->x509Credential;
return NSURLSessionAuthChallengeUseCredential;
}
return NSURLSessionAuthChallengePerformDefaultHandling;
}];
}
@@ -111,14 +123,21 @@
}
- (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error {
bool aborted = error.code == NSURLErrorCancelled;
if(aborted){
[dictionary setObject:[NSNumber numberWithInt:-8] forKey:@"status"];
[dictionary setObject:@"Request was aborted" forKey:@"error"];
}
if (response != nil) {
[dictionary setValue:response.URL.absoluteString forKey:@"url"];
[dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"];
[dictionary setObject:[self copyHeaderFields:response.allHeaderFields] forKey:@"headers"];
if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) {
[dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"];
if(!aborted){
[dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"];
if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) {
[dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"];
}
}
} else {
} else if(!aborted) {
[dictionary setObject:[self getStatusCode:error] forKey:@"status"];
[dictionary setObject:[error localizedDescription] forKey:@"error"];
}
@@ -181,6 +200,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
NSString *responseType = [command.arguments objectAtIndex:4];
NSNumber *reqId = [command.arguments objectAtIndex:5];
[self setRequestSerializer: @"default" forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -194,30 +214,37 @@
@try {
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
// no 'body' for HEAD request, omitting 'data'
if ([method isEqualToString:@"HEAD"]) {
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:nil];
} else {
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
}
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[manager invalidateSessionCancelingTasks:YES];
};
void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[manager invalidateSessionCancelingTasks:YES];
};
[manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure];
NSURLSessionDataTask *task = [manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure];
[self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -235,6 +262,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
NSNumber *reqId = [command.arguments objectAtIndex:7];
[self setRequestSerializer: serializerName forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -245,30 +273,32 @@
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
void (^constructBody)(id<AFMultipartFormData>) = ^(id<AFMultipartFormData> formData) {
NSArray *buffers = [data mutableArrayValueForKey:@"buffers"];
NSArray *fileNames = [data mutableArrayValueForKey:@"fileNames"];
NSArray *names = [data mutableArrayValueForKey:@"names"];
NSArray *types = [data mutableArrayValueForKey:@"types"];
NSError *error;
for (int i = 0; i < [buffers count]; ++i) {
NSData *decodedBuffer = [[NSData alloc] initWithBase64EncodedString:[buffers objectAtIndex:i] options:0];
NSString *fileName = [fileNames objectAtIndex:i];
NSString *partName = [names objectAtIndex:i];
NSString *partType = [types objectAtIndex:i];
if (![fileName isEqual:[NSNull null]]) {
[formData appendPartWithFileData:decodedBuffer name:partName fileName:fileName mimeType:partType];
} else {
[formData appendPartWithFormData:decodedBuffer name:[names objectAtIndex:i]];
}
}
if (error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithInt:400] forKey:@"status"];
[dictionary setObject:@"Could not add part to multipart request body." forKey:@"error"];
@@ -278,30 +308,38 @@
return;
}
};
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[manager invalidateSessionCancelingTasks:YES];
};
void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[manager invalidateSessionCancelingTasks:YES];
};
NSURLSessionDataTask *task;
if ([serializerName isEqualToString:@"multipart"]) {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure];
task = [manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure];
} else {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure];
task = [manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure];
}
[self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -333,27 +371,27 @@
- (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);
@@ -367,7 +405,7 @@
self->x509Credential = [NSURLCredential credentialWithIdentity:identity certificates: nil persistence:NSURLCredentialPersistenceForSession];
CFRelease(items);
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
}
}
@@ -413,6 +451,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
NSNumber *reqId = [command.arguments objectAtIndex:7];
[self setRequestHeaders: headers forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -424,7 +463,7 @@
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
NSURLSessionDataTask *task = [manager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
NSError *error;
for (int i = 0; i < [filePaths count]; i++) {
NSString *filePath = (NSString *) [filePaths objectAtIndex:i];
@@ -433,6 +472,8 @@
[formData appendPartWithFileURL:fileURL name:uploadName error:&error];
}
if (error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithInt:500] forKey:@"status"];
[dictionary setObject:@"Could not add file to post body." forKey:@"error"];
@@ -442,6 +483,8 @@
return;
}
} progress:nil success:^(NSURLSessionTask *task, id responseObject) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
@@ -449,6 +492,8 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
@@ -456,6 +501,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
[self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -472,6 +518,7 @@
NSString *filePath = [command.arguments objectAtIndex: 2];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:3] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:4] boolValue];
NSNumber *reqId = [command.arguments objectAtIndex:5];
[self setRequestHeaders: headers forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -486,7 +533,8 @@
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) {
NSURLSessionDataTask *task = [manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) {
[weakSelf removeRequest:reqId];
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -547,6 +595,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
[weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
[dictionary setObject:@"There was an error downloading the file" forKey:@"error"];
@@ -555,6 +604,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
[self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -562,4 +612,32 @@
}
}
- (void)abort:(CDVInvokedUrlCommand*)command {
NSNumber *reqId = [command.arguments objectAtIndex:0];
CDVPluginResult *pluginResult;
bool removed = false;
NSURLSessionDataTask *task = [reqDict objectForKey:reqId];
if(task){
@try{
[task cancel];
removed = true;
} @catch (NSException *exception) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setValue:exception.userInfo forKey:@"error"];
[dictionary setObject:[NSNumber numberWithInt:-1] forKey:@"status"];
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
}
}
if(!pluginResult){
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithBool:removed] forKey:@"aborted"];
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
}
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
@end

View File

@@ -16,15 +16,16 @@
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:networkSecurityConfig="@xml/network_security_config" />
</edit-config>
<resource-file src="network_security_config.xml" target="app/src/main/res/xml/network_security_config.xml" />
<allow-intent href="market:*" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
</platform>
<engine name="android" spec="8.1.0" />
<engine name="browser" spec="6.0.0" />
<engine name="ios" spec="5.1.0" />
<plugin name="cordova-plugin-file" spec="6.0.2" />
<preference name="AndroidPersistentFileLocation" value="Internal" />
<preference name="AndroidBlacklistSecureSocketProtocols" value="SSLv3,TLSv1" />
</widget>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<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

@@ -11,16 +11,25 @@
"author": "Sefa Ilkimen",
"license": "Apache-2.0",
"dependencies": {
"cordova": "9.0.0",
"cordova-android": "8.1.0",
"cordova": "10.0.0",
"cordova-android": "9.0.0",
"cordova-browser": "6.0.0",
"cordova-ios": "5.1.0"
"cordova-ios": "6.2.0",
"cordova-plugin-device": "2.0.3",
"cordova-plugin-wkwebview-file-xhr": "3.0.0"
},
"cordova": {
"platforms": [
"android",
"ios"
]
"ios",
"browser"
],
"plugins": {
"cordova-plugin-device": {},
"cordova-plugin-wkwebview-file-xhr": {}
}
},
"devDependencies": {}
"devDependencies": {
"cordova-plugin-device": "2.0.3"
}
}

View File

@@ -10,12 +10,12 @@ const app = {
var onlyFlaggedTests = [];
var enabledTests = [];
tests.forEach(function (test) {
if (test.only) {
onlyFlaggedTests.push(test);
}
if (!test.disabled) {
enabledTests.push(test);
}
@@ -50,6 +50,16 @@ const app = {
};
},
skip: function (content) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - skipped', content);
app.lastResult = {
type: 'skipped',
data: content
};
},
throw: function (error) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - throwed', error.message);
@@ -126,7 +136,7 @@ const app = {
const execTest = function () {
try {
testDefinition.func(app.resolve, app.reject);
testDefinition.func(app.resolve, app.reject, app.skip);
} catch (error) {
app.throw(error);
}

View File

@@ -83,10 +83,34 @@ const helpers = {
}
result.type.should.be.equal(expected);
},
isAbortSupported: function () {
if (window.cordova && window.cordova.platformId === 'android') {
var version = device.version; // NOTE will throw error if cordova is present without cordova-plugin-device
var major = parseInt(/^(\d+)(\.|$)/.exec(version)[1], 10);
return isFinite(major) && major >= 6;
}
return true;
},
getAbortDelay: function () { return 0; },
getDemoArrayBuffer: function(size) {
var demoText = [73, 39, 109, 32, 97, 32, 100, 117, 109, 109, 121, 32, 102, 105, 108, 101, 33, 32, 73, 39, 109, 32, 117, 115, 101, 100, 32, 102, 111, 114, 32, 116, 101, 115, 116, 105, 110, 103, 32, 112, 117, 114, 112, 111, 115, 101, 115, 46, 32, 82, 97, 110, 100, 111, 109, 32, 100, 97, 116, 97, 32, 105, 115, 32, 102, 111, 108, 108, 111, 119, 105, 110, 103, 58, 32];
var buffer = new ArrayBuffer(size);
var view = new Uint8Array(buffer);
for (var i = 0; i < size; ++i) {
view[i] = demoText[i];
}
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.' }
}
@@ -288,25 +312,25 @@ const tests = [
JSON.parse(result.data.data).form.should.eql({ test: 'testString' });
}
},
// {
// 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) {
// result.type.should.be.equal('resolved');
// result.data.url.should.be.equal('http://httpbin.org/anything');
// }
// },
// {
// description: 'should not follow 302 redirect when following redirects is disabled',
// expected: 'rejected: {"status": 302, ...',
// before: function (resolve, reject) { cordova.plugin.http.disableRedirect(true, resolve, reject) },
// 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('rejected');
// result.data.status.should.be.equal(302);
// }
// },
{
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://httpbingo.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');
}
},
{
description: 'should not follow 302 redirect when following redirects is disabled',
expected: 'rejected: {"status": 302, ...',
before: function (resolve, reject) { cordova.plugin.http.setFollowRedirect(false); resolve(); },
func: function (resolve, reject) { cordova.plugin.http.get('http://httpbingo.org/redirect-to?url=http://httpbin.org/anything', {}, {}, resolve, reject); },
validationFunc: function (driver, result) {
result.type.should.be.equal('rejected');
result.data.status.should.be.equal(302);
}
},
{
description: 'should download a file from given URL to given path in local filesystem',
expected: 'resolved: {"content": "<?xml version=\'1.0\' encoding=\'us-ascii\'?>\\n\\n<!-- A SAMPLE set of slides -->" ...',
@@ -900,7 +924,7 @@ const tests = [
},
validationFunc: function (driver, result) {
result.type.should.be.equal('resolved');
should.equal(null, result.data.data);
should.equal(true, result.data.data === null || result.data.data === undefined);
}
},
{
@@ -988,6 +1012,157 @@ const tests = [
JSON.parse(result.data.data).cookies.should.be.eql({});
}
},
{
description: 'should be able to abort (POST)',
expected: 'rejected: {"status":-8, "error": "Request ...}',
before: helpers.setRawSerializer,
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
return skip();
}
var targetUrl = 'http://httpbin.org/post';
var fileContent = helpers.getDemoArrayBuffer(10000);
var reqId = cordova.plugin.http.post(targetUrl, fileContent, {}, resolve, reject);
setTimeout(function () {
cordova.plugin.http.abort(reqId);
}, helpers.getAbortDelay());
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'rejected');
result.data.status.should.be.equal(-8);
}
},
{
description: 'should be able to abort (GET)',
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
return skip();
}
var url = 'https://httpbin.org/drip?duration=2&numbytes=10&code=200';
var options = { method: 'get', responseType: 'blob' };
var success = function (response) {
resolve({
isBlob: response.data.constructor === Blob,
type: response.data.type,
byteLength: response.data.size
});
};
var reqId = cordova.plugin.http.sendRequest(url, options, success, reject);
setTimeout(function () {
cordova.plugin.http.abort(reqId);
}, helpers.getAbortDelay());
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'rejected');
result.data.status.should.be.equal(-8);
}
},
{
description: 'should be able to abort downloading a file',
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
return skip();
}
var sourceUrl = 'http://httpbin.org/xml';
var targetPath = cordova.file.cacheDirectory + 'test.xml';
var reqId = cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) {
helpers.getWithXhr(function (content) {
resolve({
sourceUrl: sourceUrl,
targetPath: targetPath,
fullPath: entry.fullPath,
name: entry.name,
content: content
});
}, targetPath);
}, reject);
setTimeout(function () {
cordova.plugin.http.abort(reqId);
}, helpers.getAbortDelay());
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'rejected');
result.data.status.should.be.equal(-8);
}
},
{
description: 'should be able to abort uploading a file',
expected: 'rejected: {"status":-8, "error": "Request ...}',
func: function (resolve, reject, skip) {
if (!helpers.isAbortSupported()) {
return skip();
}
var fileName = 'test-file.txt';
var fileContent = helpers.getDemoArrayBuffer(10000);
var sourcePath = cordova.file.cacheDirectory + fileName;
var targetUrl = 'http://httpbin.org/post';
helpers.writeToFile(function () {
var reqId = cordova.plugin.http.uploadFile(targetUrl, {}, {}, sourcePath, fileName, resolve, reject);
setTimeout(function () {
cordova.plugin.http.abort(reqId);
}, helpers.getAbortDelay());
}, fileName, fileContent);
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'rejected');
result.data.status.should.be.equal(-8);
}
},
{
description: 'should not send malformed request when FormData is empty #372',
expected: 'resolved: {"status":200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
var formData = new ponyfills.FormData();
var url = 'http://httpbin.org/anything';
var options = { method: 'post', data: formData };
cordova.plugin.http.sendRequest(url, options, resolve, reject);
},
validationFunc: function (driver, result, targetInfo) {
helpers.checkResult(result, 'resolved');
var parsed = JSON.parse(result.data.data);
if (targetInfo.isAndroid) {
// boundary should be sent correctly on Android
parsed.headers['Content-Type'].should.be.equal('multipart/form-data; boundary=00content0boundary00');
} else {
// falling back to empty url encoded request on iOS
parsed.headers['Content-Type'].should.be.equal('application/x-www-form-urlencoded');
}
}
},
{
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

@@ -14,7 +14,7 @@ const configs = {
},
localIosEmulator: {
platformName: 'iOS',
platformVersion: '13.2',
platformVersion: '14.3',
automationName: 'XCUITest',
deviceName: 'iPhone 8',
autoWebview: true,
@@ -22,7 +22,7 @@ const configs = {
},
localAndroidEmulator: {
platformName: 'Android',
platformVersion: '5',
platformVersion: '9',
deviceName: 'Android Emulator',
autoWebview: true,
fullReset: true,
@@ -32,27 +32,27 @@ const configs = {
// testing on SauceLabs
saucelabsIosDevice: {
browserName: '',
'appium-version': '1.9.1',
'appium-version': '1.20.1',
platformName: 'iOS',
platformVersion: '10.3',
platformVersion: '14.3',
deviceName: 'iPhone 6',
autoWebview: true,
app: 'sauce-storage:HttpDemo.app.zip'
},
saucelabsIosEmulator: {
browserName: '',
'appium-version': '1.9.1',
'appium-version': '1.20.1',
platformName: 'iOS',
platformVersion: '10.3',
platformVersion: '14.3',
deviceName: 'iPhone Simulator',
autoWebview: true,
app: 'sauce-storage:HttpDemo.app.zip'
},
saucelabsAndroidEmulator: {
browserName: '',
'appium-version': '1.9.1',
'appium-version': '1.20.1',
platformName: 'Android',
platformVersion: '5.1',
platformVersion: '8.0',
deviceName: 'Android Emulator',
autoWebview: true,
app: 'sauce-storage:HttpDemo.apk'
@@ -60,18 +60,20 @@ const configs = {
// testing on BrowserStack
browserstackIosDevice: {
device: 'iPhone 7',
os_version: '10',
device: 'iPhone 12',
os_version: '14',
project: 'HTTP Test App',
autoWebview: true,
app: 'HttpTestAppAndroid'
app: 'HttpTestAppAndroid',
'browserstack.networkLogs': true
},
browserstackAndroidDevice: {
device: 'Google Nexus 6',
os_version: '5.0',
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);
@@ -49,12 +49,19 @@ describe('Advanced HTTP e2e test suite', function () {
});
const defineTestForMocha = (test, index) => {
it(index + ': ' + test.description, async () => {
it(index + ': ' + test.description, async function () {
await clickNext(driver);
await validateTestIndex(driver, index);
await validateTestTitle(driver, test.description);
await waitToBeFinished(driver, test.timeout || 10000);
await validateResult(driver, test.validationFunc, targetInfo);
const skipped = await checkSkipped(driver);
if (skipped) {
this.skip();
} else {
await validateResult(driver, test.validationFunc, targetInfo);
}
});
};
@@ -114,7 +121,20 @@ 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) {
const result = await driver.safeExecute('app.lastResult');
return result.type === 'skipped';
}
function sleep(ms) {

View File

@@ -676,6 +676,20 @@ describe('Common helpers', function () {
})
});
});
describe('nextRequestId()', function () {
const helpers = require('../www/helpers')(null, null, null, null, null, null);
it('returns number requestIds', () => {
helpers.nextRequestId().should.be.a('number');
});
it('returns unique requestIds', () => {
const ids = [helpers.nextRequestId(), helpers.nextRequestId(), helpers.nextRequestId()];
const set = new Set(ids);
ids.should.to.deep.equal(Array.from(set));
});
});
});
describe('Dependency Validator', function () {

View File

@@ -6,4 +6,5 @@ module.exports = {
UNSUPPORTED_URL: -5,
NOT_CONNECTED: -6,
POST_PROCESSING_FAILED: -7,
ABORTED: -8,
};

View File

@@ -5,6 +5,13 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'options', 'upload', 'download'];
var validResponseTypes = ['text', 'json', 'arraybuffer', 'blob'];
var nextRequestId = (function(){
var currReqId = 0;
return function nextRequestId() {
return ++currReqId;
}
})();
var interface = {
b64EncodeUnicode: b64EncodeUnicode,
checkClientAuthMode: checkClientAuthMode,
@@ -24,6 +31,7 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
injectCookieHandler: injectCookieHandler,
injectFileEntryHandler: injectFileEntryHandler,
injectRawResponseHandler: injectRawResponseHandler,
nextRequestId: nextRequestId,
};
// expose all functions for testing purposes

View File

@@ -26,6 +26,7 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
options: options,
uploadFile: uploadFile,
downloadFile: downloadFile,
abort: abort,
ErrorCode: errorCodes,
ponyfills: ponyfills
};
@@ -143,23 +144,31 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
var onFail = helpers.injectCookieHandler(url, failure);
var onSuccess = helpers.injectCookieHandler(url, helpers.injectRawResponseHandler(options.responseType, success, failure));
var reqId = helpers.nextRequestId();
switch (options.method) {
case 'post':
case 'put':
case 'patch':
return helpers.processData(options.data, options.serializer, function (data) {
exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType]);
helpers.processData(options.data, options.serializer, function (data) {
exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType, reqId]);
});
break;
case 'upload':
var fileOptions = helpers.checkUploadFileOptions(options.filePath, options.name);
return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType]);
exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType, reqId]);
break;
case 'download':
var filePath = helpers.checkDownloadFilePath(options.filePath);
var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success));
return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect]);
exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect, reqId]);
break;
default:
return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType]);
exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType, reqId]);
break;
}
return reqId;
}
function post(url, data, headers, success, failure) {
@@ -198,5 +207,9 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure);
}
function abort(requestId , success, failure) {
return exec(success, failure, 'CordovaHttpPlugin', 'abort', [requestId]);
}
return publicInterface;
}