summaryrefslogtreecommitdiff
path: root/StoneIsland/plugins/cordova-plugin-advanced-http/src/android
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2020-09-21 18:11:51 +0200
committerJules Laplace <julescarbon@gmail.com>2020-09-21 18:11:51 +0200
commita810d0248dd9f1099ce15809f8e1e75eedbff8e6 (patch)
tree2eeebe5dbbe0e9005b89806a5b9a88d47f54ed1a /StoneIsland/plugins/cordova-plugin-advanced-http/src/android
parentd906f7303e70adaa75523d8bfc5b46523ccfffa0 (diff)
plugins
Diffstat (limited to 'StoneIsland/plugins/cordova-plugin-advanced-http/src/android')
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java113
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java205
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java42
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java25
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java169
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java100
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java92
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java124
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpBodyDecoder.java55
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpRequest.java3095
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/JsonUtils.java58
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/KeyChainKeyManager.java57
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSConfiguration.java63
-rw-r--r--StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSSocketFactory.java63
14 files changed, 4261 insertions, 0 deletions
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java
new file mode 100644
index 00000000..b01ac936
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java
@@ -0,0 +1,113 @@
+package com.silkimen.cordovahttp;
+
+import android.app.Activity;
+import android.content.Context;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+
+import org.apache.cordova.CallbackContext;
+
+import com.silkimen.http.KeyChainKeyManager;
+import com.silkimen.http.TLSConfiguration;
+
+class CordovaClientAuth implements Runnable, KeyChainAliasCallback {
+ private static final String TAG = "Cordova-Plugin-HTTP";
+
+ private String mode;
+ private String aliasString;
+ private byte[] rawPkcs;
+ private String pkcsPassword;
+ private Activity activity;
+ private Context context;
+ private TLSConfiguration tlsConfiguration;
+ private CallbackContext callbackContext;
+
+ public CordovaClientAuth(final String mode, final String aliasString, final byte[] rawPkcs,
+ final String pkcsPassword, final Activity activity, final Context context, final TLSConfiguration configContainer,
+ final CallbackContext callbackContext) {
+
+ this.mode = mode;
+ this.aliasString = aliasString;
+ this.rawPkcs = rawPkcs;
+ this.pkcsPassword = pkcsPassword;
+ this.activity = activity;
+ this.tlsConfiguration = configContainer;
+ this.context = context;
+ this.callbackContext = callbackContext;
+ }
+
+ @Override
+ public void run() {
+ if ("systemstore".equals(this.mode)) {
+ this.loadFromSystemStore();
+ } else if ("buffer".equals(this.mode)) {
+ this.loadFromBuffer();
+ } else {
+ this.disableClientAuth();
+ }
+ }
+
+ private void loadFromSystemStore() {
+ if (this.aliasString == null) {
+ KeyChain.choosePrivateKeyAlias(this.activity, this, null, null, null, -1, null);
+ } else {
+ this.alias(this.aliasString);
+ }
+ }
+
+ private void loadFromBuffer() {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm);
+ ByteArrayInputStream stream = new ByteArrayInputStream(this.rawPkcs);
+
+ keyStore.load(stream, this.pkcsPassword.toCharArray());
+ keyManagerFactory.init(keyStore, this.pkcsPassword.toCharArray());
+
+ this.tlsConfiguration.setKeyManagers(keyManagerFactory.getKeyManagers());
+ this.callbackContext.success();
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't load given PKCS12 container for authentication", e);
+ this.callbackContext.error("Couldn't load given PKCS12 container for authentication");
+ }
+ }
+
+ private void disableClientAuth() {
+ this.tlsConfiguration.setKeyManagers(null);
+ this.callbackContext.success();
+ }
+
+ @Override
+ public void alias(final String alias) {
+ try {
+ if (alias == null) {
+ throw new Exception("Couldn't get a consent for private key access");
+ }
+
+ PrivateKey key = KeyChain.getPrivateKey(this.context, alias);
+ X509Certificate[] chain = KeyChain.getCertificateChain(this.context, alias);
+ KeyManager keyManager = new KeyChainKeyManager(alias, key, chain);
+
+ this.tlsConfiguration.setKeyManagers(new KeyManager[] { keyManager });
+
+ this.callbackContext.success(alias);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication",
+ e);
+ this.callbackContext.error(
+ "Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication");
+ }
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java
new file mode 100644
index 00000000..e56be1c0
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java
@@ -0,0 +1,205 @@
+package com.silkimen.cordovahttp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import java.nio.ByteBuffer;
+
+import javax.net.ssl.SSLException;
+
+import com.silkimen.http.HttpBodyDecoder;
+import com.silkimen.http.HttpRequest;
+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;
+
+import android.util.Base64;
+import android.util.Log;
+
+abstract class CordovaHttpBase implements Runnable {
+ protected static final String TAG = "Cordova-Plugin-HTTP";
+
+ protected String method;
+ protected String url;
+ protected String serializer = "none";
+ protected String responseType;
+ protected Object data;
+ protected JSONObject headers;
+ protected int timeout;
+ protected boolean followRedirects;
+ protected TLSConfiguration tlsConfiguration;
+ protected CallbackContext callbackContext;
+
+ public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout,
+ boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
+ CallbackContext callbackContext) {
+
+ this.method = method;
+ this.url = url;
+ this.serializer = serializer;
+ this.data = data;
+ this.headers = headers;
+ this.timeout = timeout;
+ this.followRedirects = followRedirects;
+ this.responseType = responseType;
+ this.tlsConfiguration = tlsConfiguration;
+ this.callbackContext = callbackContext;
+ }
+
+ public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects,
+ String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) {
+
+ this.method = method;
+ this.url = url;
+ this.headers = headers;
+ this.timeout = timeout;
+ this.followRedirects = followRedirects;
+ this.responseType = responseType;
+ this.tlsConfiguration = tlsConfiguration;
+ this.callbackContext = callbackContext;
+ }
+
+ @Override
+ public void run() {
+ CordovaHttpResponse response = new CordovaHttpResponse();
+
+ try {
+ HttpRequest request = this.createRequest();
+ this.prepareRequest(request);
+ this.sendBody(request);
+ this.processResponse(request, response);
+ request.disconnect();
+ } catch (HttpRequestException e) {
+ if (e.getCause() 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) {
+ 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) {
+ response.setStatus(-4);
+ response.setErrorMessage("Request timed out: " + e.getMessage());
+ Log.w(TAG, "Request timed out", e);
+ } else {
+ response.setStatus(-1);
+ response.setErrorMessage("There was an error with the request: " + e.getCause().getMessage());
+ Log.w(TAG, "Generic request error", e);
+ }
+ } catch (Exception e) {
+ response.setStatus(-1);
+ response.setErrorMessage(e.getMessage());
+ Log.e(TAG, "An unexpected error occured", e);
+ }
+
+ try {
+ if (response.hasFailed()) {
+ this.callbackContext.error(response.toJSON());
+ } else {
+ this.callbackContext.success(response.toJSON());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "An unexpected error occured while creating HTTP response object", e);
+ }
+ }
+
+ protected HttpRequest createRequest() throws JSONException {
+ return new HttpRequest(this.url, this.method);
+ }
+
+ protected void prepareRequest(HttpRequest request) throws JSONException, IOException {
+ request.followRedirects(this.followRedirects);
+ request.readTimeout(this.timeout);
+ request.acceptCharset("UTF-8");
+ request.uncompress(true);
+
+ if (this.tlsConfiguration.getHostnameVerifier() != null) {
+ request.setHostnameVerifier(this.tlsConfiguration.getHostnameVerifier());
+ }
+
+ request.setSSLSocketFactory(this.tlsConfiguration.getTLSSocketFactory());
+
+ // setup content type before applying headers, so user can override it
+ this.setContentType(request);
+
+ request.headers(JsonUtils.getStringMap(this.headers));
+ }
+
+ protected void setContentType(HttpRequest request) {
+ if ("json".equals(this.serializer)) {
+ request.contentType("application/json", "UTF-8");
+ } else if ("utf8".equals(this.serializer)) {
+ request.contentType("text/plain", "UTF-8");
+ } else if ("raw".equals(this.serializer)) {
+ request.contentType("application/octet-stream");
+ } 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");
+ }
+ }
+
+ protected void sendBody(HttpRequest request) throws Exception {
+ if (this.data == null) {
+ return;
+ }
+
+ if ("json".equals(this.serializer)) {
+ request.send(this.data.toString());
+ } else if ("utf8".equals(this.serializer)) {
+ request.send(((JSONObject) this.data).getString("text"));
+ } else if ("raw".equals(this.serializer)) {
+ request.send(Base64.decode((String)this.data, Base64.DEFAULT));
+ } else if ("urlencoded".equals(this.serializer)) {
+ request.form(JsonUtils.getObjectMap((JSONObject) this.data));
+ } else if ("multipart".equals(this.serializer)) {
+ JSONArray buffers = ((JSONObject) this.data).getJSONArray("buffers");
+ JSONArray names = ((JSONObject) this.data).getJSONArray("names");
+ JSONArray fileNames = ((JSONObject) this.data).getJSONArray("fileNames");
+ JSONArray types = ((JSONObject) this.data).getJSONArray("types");
+
+ for (int i = 0; i < buffers.length(); ++i) {
+ byte[] bytes = Base64.decode(buffers.getString(i), Base64.DEFAULT);
+ String name = names.getString(i);
+
+ if (fileNames.isNull(i)) {
+ request.part(name, new String(bytes, "UTF-8"));
+ } else {
+ request.part(name, fileNames.getString(i), types.getString(i), new ByteArrayInputStream(bytes));
+ }
+ }
+ }
+ }
+
+ protected void processResponse(HttpRequest request, CordovaHttpResponse response) throws Exception {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ request.receive(outputStream);
+
+ response.setStatus(request.code());
+ response.setUrl(request.url().toString());
+ response.setHeaders(request.headers());
+
+ if (request.code() >= 200 && request.code() < 300) {
+ if ("text".equals(this.responseType) || "json".equals(this.responseType)) {
+ String decoded = HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset());
+ response.setBody(decoded);
+ } else {
+ response.setData(outputStream.toByteArray());
+ }
+ } else {
+ response.setErrorMessage(HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset()));
+ }
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java
new file mode 100644
index 00000000..d89db82c
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java
@@ -0,0 +1,42 @@
+package com.silkimen.cordovahttp;
+
+import java.io.File;
+import java.net.URI;
+
+import javax.net.ssl.HostnameVerifier;
+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;
+
+class CordovaHttpDownload extends CordovaHttpBase {
+ private String filePath;
+
+ public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects,
+ TLSConfiguration tlsConfiguration, CallbackContext callbackContext) {
+
+ super("GET", url, headers, timeout, followRedirects, "text", tlsConfiguration, callbackContext);
+ this.filePath = filePath;
+ }
+
+ @Override
+ protected void processResponse(HttpRequest request, CordovaHttpResponse response) throws Exception {
+ response.setStatus(request.code());
+ response.setUrl(request.url().toString());
+ response.setHeaders(request.headers());
+
+ if (request.code() >= 200 && request.code() < 300) {
+ File file = new File(new URI(this.filePath));
+ JSONObject fileEntry = FileUtils.getFilePlugin().getEntryForFile(file);
+
+ request.receive(file);
+ response.setFileEntry(fileEntry);
+ } else {
+ response.setErrorMessage("There was an error downloading the file");
+ }
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java
new file mode 100644
index 00000000..5f17e5d8
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java
@@ -0,0 +1,25 @@
+package com.silkimen.cordovahttp;
+
+import javax.net.ssl.HostnameVerifier;
+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) {
+
+ 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) {
+
+ super(method, url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext);
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java
new file mode 100644
index 00000000..297dba38
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java
@@ -0,0 +1,169 @@
+package com.silkimen.cordovahttp;
+
+import java.security.KeyStore;
+
+import com.silkimen.http.TLSConfiguration;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Log;
+import android.util.Base64;
+
+import javax.net.ssl.TrustManagerFactory;
+
+public class CordovaHttpPlugin extends CordovaPlugin {
+ private static final String TAG = "Cordova-Plugin-HTTP";
+
+ private TLSConfiguration tlsConfiguration;
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+
+ this.tlsConfiguration = new TLSConfiguration();
+
+ try {
+ KeyStore store = KeyStore.getInstance("AndroidCAStore");
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+
+ store.load(null);
+ tmf.init(store);
+
+ this.tlsConfiguration.setHostnameVerifier(null);
+ this.tlsConfiguration.setTrustManagers(tmf.getTrustManagers());
+ } catch (Exception e) {
+ Log.e(TAG, "An error occured while loading system's CA certificates", e);
+ }
+ }
+
+ @Override
+ public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext)
+ throws JSONException {
+
+ if (action == null) {
+ return false;
+ }
+
+ if ("get".equals(action)) {
+ return this.executeHttpRequestWithoutData(action, args, callbackContext);
+ } else if ("head".equals(action)) {
+ return this.executeHttpRequestWithoutData(action, args, callbackContext);
+ } else if ("delete".equals(action)) {
+ return this.executeHttpRequestWithoutData(action, args, callbackContext);
+ } else if ("options".equals(action)) {
+ return this.executeHttpRequestWithoutData(action, args, callbackContext);
+ } else if ("post".equals(action)) {
+ return this.executeHttpRequestWithData(action, args, callbackContext);
+ } else if ("put".equals(action)) {
+ return this.executeHttpRequestWithData(action, args, callbackContext);
+ } else if ("patch".equals(action)) {
+ return this.executeHttpRequestWithData(action, args, callbackContext);
+ } else if ("uploadFiles".equals(action)) {
+ return this.uploadFiles(args, callbackContext);
+ } else if ("downloadFile".equals(action)) {
+ return this.downloadFile(args, callbackContext);
+ } else if ("setServerTrustMode".equals(action)) {
+ return this.setServerTrustMode(args, callbackContext);
+ } else if ("setClientAuthMode".equals(action)) {
+ return this.setClientAuthMode(args, callbackContext);
+ } else {
+ return false;
+ }
+ }
+
+ private boolean executeHttpRequestWithoutData(final String method, final JSONArray args,
+ final CallbackContext callbackContext) throws JSONException {
+
+ String url = args.getString(0);
+ JSONObject headers = args.getJSONObject(1);
+ int timeout = args.getInt(2) * 1000;
+ boolean followRedirect = args.getBoolean(3);
+ String responseType = args.getString(4);
+
+ CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect,
+ responseType, this.tlsConfiguration, callbackContext);
+
+ cordova.getThreadPool().execute(request);
+
+ return true;
+ }
+
+ private boolean executeHttpRequestWithData(final String method, final JSONArray args,
+ final CallbackContext callbackContext) throws JSONException {
+
+ String url = args.getString(0);
+ Object data = args.get(1);
+ String serializer = args.getString(2);
+ JSONObject headers = args.getJSONObject(3);
+ int timeout = args.getInt(4) * 1000;
+ 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);
+
+ cordova.getThreadPool().execute(request);
+
+ return true;
+ }
+
+ private boolean uploadFiles(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ String url = args.getString(0);
+ JSONObject headers = args.getJSONObject(1);
+ JSONArray filePaths = args.getJSONArray(2);
+ JSONArray uploadNames = args.getJSONArray(3);
+ int timeout = args.getInt(4) * 1000;
+ 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);
+
+ cordova.getThreadPool().execute(upload);
+
+ return true;
+ }
+
+ private boolean downloadFile(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ String url = args.getString(0);
+ JSONObject headers = args.getJSONObject(1);
+ String filePath = args.getString(2);
+ int timeout = args.getInt(3) * 1000;
+ boolean followRedirect = args.getBoolean(4);
+
+ CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect,
+ this.tlsConfiguration, callbackContext);
+
+ cordova.getThreadPool().execute(download);
+
+ return true;
+ }
+
+ private boolean setServerTrustMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ CordovaServerTrust runnable = new CordovaServerTrust(args.getString(0), this.cordova.getActivity(),
+ this.tlsConfiguration, callbackContext);
+
+ cordova.getThreadPool().execute(runnable);
+
+ return true;
+ }
+
+ private boolean setClientAuthMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ byte[] pkcs = args.isNull(2) ? null : Base64.decode(args.getString(2), Base64.DEFAULT);
+
+ CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), args.isNull(1) ? null : args.getString(1),
+ pkcs, args.getString(3), this.cordova.getActivity(), this.cordova.getActivity().getApplicationContext(),
+ this.tlsConfiguration, callbackContext);
+
+ cordova.getThreadPool().execute(runnable);
+
+ return true;
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java
new file mode 100644
index 00000000..e6051bf3
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java
@@ -0,0 +1,100 @@
+package com.silkimen.cordovahttp;
+
+import java.nio.ByteBuffer;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Base64;
+
+class CordovaHttpResponse {
+ private int status;
+ private String url;
+ private Map<String, List<String>> headers;
+ private String body;
+ private byte[] rawData;
+ private JSONObject fileEntry;
+ private boolean hasFailed;
+ private boolean isFileOperation;
+ private boolean isRawResponse;
+ private String error;
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public void setHeaders(Map<String, List<String>> headers) {
+ this.headers = headers;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public void setData(byte[] rawData) {
+ this.isRawResponse = true;
+ this.rawData = rawData;
+ }
+
+ public void setFileEntry(JSONObject entry) {
+ this.isFileOperation = true;
+ this.fileEntry = entry;
+ }
+
+ public void setErrorMessage(String message) {
+ this.hasFailed = true;
+ this.error = message;
+ }
+
+ public boolean hasFailed() {
+ return this.hasFailed;
+ }
+
+ public JSONObject toJSON() throws JSONException {
+ JSONObject json = new JSONObject();
+
+ json.put("status", this.status);
+ json.put("url", this.url);
+
+ if (this.headers != null && !this.headers.isEmpty()) {
+ json.put("headers", new JSONObject(getFilteredHeaders()));
+ }
+
+ if (this.hasFailed) {
+ json.put("error", this.error);
+ } else if (this.isFileOperation) {
+ json.put("file", this.fileEntry);
+ } else if (this.isRawResponse) {
+ json.put("data", Base64.encodeToString(this.rawData, Base64.DEFAULT));
+ } else {
+ json.put("data", this.body);
+ }
+
+ return json;
+ }
+
+ private Map<String, String> getFilteredHeaders() throws JSONException {
+ Map<String, String> filteredHeaders = new HashMap<String, String>();
+
+ for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
+ String key = entry.getKey();
+ List<String> value = entry.getValue();
+
+ if ((key != null) && (!value.isEmpty())) {
+ filteredHeaders.put(key.toLowerCase(), TextUtils.join(", ", value));
+ }
+ }
+
+ return filteredHeaders;
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java
new file mode 100644
index 00000000..dcbcd068
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java
@@ -0,0 +1,92 @@
+package com.silkimen.cordovahttp;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.webkit.MimeTypeMap;
+
+import com.silkimen.http.HttpRequest;
+import com.silkimen.http.TLSConfiguration;
+
+import java.io.File;
+import java.io.InputStream;
+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;
+
+class CordovaHttpUpload extends CordovaHttpBase {
+ private JSONArray filePaths;
+ private JSONArray uploadNames;
+ private Context applicationContext;
+
+ public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout,
+ boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
+ Context applicationContext, CallbackContext callbackContext) {
+
+ super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext);
+ this.filePaths = filePaths;
+ this.uploadNames = uploadNames;
+ this.applicationContext = applicationContext;
+ }
+
+ @Override
+ protected void sendBody(HttpRequest request) throws Exception {
+ for (int i = 0; i < this.filePaths.length(); ++i) {
+ String uploadName = this.uploadNames.getString(i);
+ String filePath = this.filePaths.getString(i);
+
+ Uri fileUri = Uri.parse(filePath);
+
+ // File Scheme
+ if (ContentResolver.SCHEME_FILE.equals(fileUri.getScheme())) {
+ File file = new File(new URI(filePath));
+ String fileName = file.getName().trim();
+ String mimeType = this.getMimeTypeFromFileName(fileName);
+
+ request.part(uploadName, fileName, mimeType, file);
+ }
+
+ // Content Scheme
+ if (ContentResolver.SCHEME_CONTENT.equals(fileUri.getScheme())) {
+ InputStream inputStream = this.applicationContext.getContentResolver().openInputStream(fileUri);
+ String fileName = this.getFileNameFromContentScheme(fileUri, this.applicationContext).trim();
+ String mimeType = this.getMimeTypeFromFileName(fileName);
+
+ request.part(uploadName, fileName, mimeType, inputStream);
+ }
+ }
+ }
+
+ private String getFileNameFromContentScheme(Uri contentSchemeUri, Context applicationContext) {
+ Cursor returnCursor = applicationContext.getContentResolver().query(contentSchemeUri, null, null, null, null);
+
+ if (returnCursor == null || !returnCursor.moveToFirst()) {
+ return null;
+ }
+
+ int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ String fileName = returnCursor.getString(nameIndex);
+ returnCursor.close();
+
+ return fileName;
+ }
+
+ private String getMimeTypeFromFileName(String fileName) {
+ if (fileName == null || !fileName.contains(".")) {
+ return null;
+ }
+
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ int extIndex = fileName.lastIndexOf('.') + 1;
+ String extension = fileName.substring(extIndex).toLowerCase();
+
+ return mimeTypeMap.getMimeTypeFromExtension(extension);
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java
new file mode 100644
index 00000000..822079e3
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java
@@ -0,0 +1,124 @@
+package com.silkimen.cordovahttp;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import com.silkimen.http.TLSConfiguration;
+
+import org.apache.cordova.CallbackContext;
+
+import android.app.Activity;
+import android.util.Log;
+import android.content.res.AssetManager;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+class CordovaServerTrust implements Runnable {
+ private static final String TAG = "Cordova-Plugin-HTTP";
+
+ private final TrustManager[] noOpTrustManagers;
+ private final HostnameVerifier noOpVerifier;
+
+ private String mode;
+ private Activity activity;
+ private TLSConfiguration tlsConfiguration;
+ private CallbackContext callbackContext;
+
+ public CordovaServerTrust(final String mode, final Activity activity, final TLSConfiguration configContainer,
+ final CallbackContext callbackContext) {
+
+ this.mode = mode;
+ this.activity = activity;
+ this.tlsConfiguration = configContainer;
+ this.callbackContext = callbackContext;
+
+ this.noOpTrustManagers = new TrustManager[] { new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ // intentionally left blank
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ // intentionally left blank
+ }
+ } };
+
+ this.noOpVerifier = new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public void run() {
+ try {
+ if ("legacy".equals(this.mode)) {
+ this.tlsConfiguration.setHostnameVerifier(null);
+ this.tlsConfiguration.setTrustManagers(null);
+ } else if ("nocheck".equals(this.mode)) {
+ this.tlsConfiguration.setHostnameVerifier(this.noOpVerifier);
+ this.tlsConfiguration.setTrustManagers(this.noOpTrustManagers);
+ } else if ("pinned".equals(this.mode)) {
+ this.tlsConfiguration.setHostnameVerifier(null);
+ this.tlsConfiguration.setTrustManagers(this.getTrustManagers(this.getCertsFromBundle("www/certificates")));
+ } else {
+ this.tlsConfiguration.setHostnameVerifier(null);
+ this.tlsConfiguration.setTrustManagers(this.getTrustManagers(this.getCertsFromKeyStore("AndroidCAStore")));
+ }
+
+ callbackContext.success();
+ } catch (Exception e) {
+ Log.e(TAG, "An error occured while configuring SSL cert mode", e);
+ callbackContext.error("An error occured while configuring SSL cert mode");
+ }
+ }
+
+ private TrustManager[] getTrustManagers(KeyStore store) throws GeneralSecurityException {
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init(store);
+
+ return tmf.getTrustManagers();
+ }
+
+ private KeyStore getCertsFromBundle(String path) throws GeneralSecurityException, IOException {
+ AssetManager assetManager = this.activity.getAssets();
+ String[] files = assetManager.list(path);
+
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ String keyStoreType = KeyStore.getDefaultType();
+ KeyStore keyStore = KeyStore.getInstance(keyStoreType);
+
+ keyStore.load(null, null);
+
+ for (int i = 0; i < files.length; i++) {
+ int index = files[i].lastIndexOf('.');
+
+ if (index == -1 || !files[i].substring(index).equals(".cer")) {
+ continue;
+ }
+
+ keyStore.setCertificateEntry("CA" + i, cf.generateCertificate(assetManager.open(path + "/" + files[i])));
+ }
+
+ return keyStore;
+ }
+
+ private KeyStore getCertsFromKeyStore(String storeType) throws GeneralSecurityException, IOException {
+ KeyStore store = KeyStore.getInstance(storeType);
+ store.load(null);
+
+ return store;
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpBodyDecoder.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpBodyDecoder.java
new file mode 100644
index 00000000..92d69e2c
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpBodyDecoder.java
@@ -0,0 +1,55 @@
+package com.silkimen.http;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+
+public class HttpBodyDecoder {
+ private static final String[] ACCEPTED_CHARSETS = new String[] { "UTF-8", "ISO-8859-1" };
+
+ public static String decodeBody(byte[] body, String charsetName)
+ throws CharacterCodingException, MalformedInputException {
+
+ return decodeBody(ByteBuffer.wrap(body), charsetName);
+ }
+
+ public static String decodeBody(ByteBuffer body, String charsetName)
+ throws CharacterCodingException, MalformedInputException {
+
+ if (charsetName == null) {
+ return tryDecodeByteBuffer(body);
+ } else {
+ return decodeByteBuffer(body, charsetName);
+ }
+ }
+
+ private static String tryDecodeByteBuffer(ByteBuffer buffer)
+ throws CharacterCodingException, MalformedInputException {
+
+ for (int i = 0; i < ACCEPTED_CHARSETS.length - 1; i++) {
+ try {
+ return decodeByteBuffer(buffer, ACCEPTED_CHARSETS[i]);
+ } catch (MalformedInputException e) {
+ continue;
+ } catch (CharacterCodingException e) {
+ continue;
+ }
+ }
+
+ return decodeBody(buffer, ACCEPTED_CHARSETS[ACCEPTED_CHARSETS.length - 1]);
+ }
+
+ private static String decodeByteBuffer(ByteBuffer buffer, String charsetName)
+ throws CharacterCodingException, MalformedInputException {
+
+ return createCharsetDecoder(charsetName).decode(buffer).toString();
+ }
+
+ private static CharsetDecoder createCharsetDecoder(String charsetName) {
+ return Charset.forName(charsetName).newDecoder().onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpRequest.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpRequest.java
new file mode 100644
index 00000000..7e638bb1
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/HttpRequest.java
@@ -0,0 +1,3095 @@
+/*
+ * Copyright (c) 2014 Kevin Sawicki <kevinsawicki@gmail.com>
+ * modified by contributors of cordova-plugin-advanced-http
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+package com.silkimen.http;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.Proxy.Type.HTTP;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.Flushable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.security.AccessController;
+import java.security.GeneralSecurityException;
+import java.security.PrivilegedAction;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.GZIPInputStream;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A fluid interface for making HTTP requests using an underlying
+ * {@link HttpURLConnection} (or sub-class).
+ * <p>
+ * Each instance supports making a single request and cannot be reused for
+ * further requests.
+ */
+public class HttpRequest {
+
+ /**
+ * 'UTF-8' charset name
+ */
+ public static final String CHARSET_UTF8 = "UTF-8";
+
+ /**
+ * 'application/x-www-form-urlencoded' content type header value
+ */
+ public static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
+
+ /**
+ * 'application/json' content type header value
+ */
+ public static final String CONTENT_TYPE_JSON = "application/json";
+
+ /**
+ * 'gzip' encoding header value
+ */
+ public static final String ENCODING_GZIP = "gzip";
+
+ /**
+ * 'Accept' header name
+ */
+ public static final String HEADER_ACCEPT = "Accept";
+
+ /**
+ * 'Accept-Charset' header name
+ */
+ public static final String HEADER_ACCEPT_CHARSET = "Accept-Charset";
+
+ /**
+ * 'Accept-Encoding' header name
+ */
+ public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
+
+ /**
+ * 'Authorization' header name
+ */
+ public static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * 'Cache-Control' header name
+ */
+ public static final String HEADER_CACHE_CONTROL = "Cache-Control";
+
+ /**
+ * 'Content-Encoding' header name
+ */
+ public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
+
+ /**
+ * 'Content-Length' header name
+ */
+ public static final String HEADER_CONTENT_LENGTH = "Content-Length";
+
+ /**
+ * 'Content-Type' header name
+ */
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ /**
+ * 'Date' header name
+ */
+ public static final String HEADER_DATE = "Date";
+
+ /**
+ * 'ETag' header name
+ */
+ public static final String HEADER_ETAG = "ETag";
+
+ /**
+ * 'Expires' header name
+ */
+ public static final String HEADER_EXPIRES = "Expires";
+
+ /**
+ * 'If-None-Match' header name
+ */
+ public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
+
+ /**
+ * 'Last-Modified' header name
+ */
+ public static final String HEADER_LAST_MODIFIED = "Last-Modified";
+
+ /**
+ * 'Location' header name
+ */
+ public static final String HEADER_LOCATION = "Location";
+
+ /**
+ * 'Proxy-Authorization' header name
+ */
+ public static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
+
+ /**
+ * 'Referer' header name
+ */
+ public static final String HEADER_REFERER = "Referer";
+
+ /**
+ * 'Server' header name
+ */
+ public static final String HEADER_SERVER = "Server";
+
+ /**
+ * 'User-Agent' header name
+ */
+ public static final String HEADER_USER_AGENT = "User-Agent";
+
+ /**
+ * 'DELETE' request method
+ */
+ public static final String METHOD_DELETE = "DELETE";
+
+ /**
+ * 'GET' request method
+ */
+ public static final String METHOD_GET = "GET";
+
+ /**
+ * 'HEAD' request method
+ */
+ public static final String METHOD_HEAD = "HEAD";
+
+ /**
+ * 'OPTIONS' options method
+ */
+ public static final String METHOD_OPTIONS = "OPTIONS";
+
+ /**
+ * 'POST' request method
+ */
+ public static final String METHOD_POST = "POST";
+
+ /**
+ * 'PUT' request method
+ */
+ public static final String METHOD_PUT = "PUT";
+
+ /**
+ * 'TRACE' request method
+ */
+ public static final String METHOD_TRACE = "TRACE";
+
+ /**
+ * 'charset' header value parameter
+ */
+ public static final String PARAM_CHARSET = "charset";
+
+ private static final String BOUNDARY = "00content0boundary00";
+
+ private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary=" + BOUNDARY;
+
+ private static final String CRLF = "\r\n";
+
+ private static final String[] EMPTY_STRINGS = new String[0];
+
+ private static String getValidCharset(final String charset) {
+ if (charset != null && charset.length() > 0)
+ return charset;
+ else
+ return CHARSET_UTF8;
+ }
+
+ private static StringBuilder addPathSeparator(final String baseUrl, final StringBuilder result) {
+ // Add trailing slash if the base URL doesn't have any path segments.
+ //
+ // The following test is checking for the last slash not being part of
+ // the protocol to host separator: '://'.
+ if (baseUrl.indexOf(':') + 2 == baseUrl.lastIndexOf('/'))
+ result.append('/');
+ return result;
+ }
+
+ private static StringBuilder addParamPrefix(final String baseUrl, final StringBuilder result) {
+ // Add '?' if missing and add '&' if params already exist in base url
+ final int queryStart = baseUrl.indexOf('?');
+ final int lastChar = result.length() - 1;
+ if (queryStart == -1)
+ result.append('?');
+ else if (queryStart < lastChar && baseUrl.charAt(lastChar) != '&')
+ result.append('&');
+ return result;
+ }
+
+ private static StringBuilder addParam(final Object key, Object value, final StringBuilder result) {
+ if (value != null && value.getClass().isArray())
+ value = arrayToList(value);
+
+ if (value instanceof Iterable<?>) {
+ Iterator<?> iterator = ((Iterable<?>) value).iterator();
+ while (iterator.hasNext()) {
+ result.append(key);
+ result.append("[]=");
+ Object element = iterator.next();
+ if (element != null)
+ result.append(element);
+ if (iterator.hasNext())
+ result.append("&");
+ }
+ } else {
+ result.append(key);
+ result.append("=");
+ if (value != null)
+ result.append(value);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates {@link HttpURLConnection HTTP connections} for {@link URL urls}.
+ */
+ public interface ConnectionFactory {
+ /**
+ * Open an {@link HttpURLConnection} for the specified {@link URL}.
+ *
+ * @throws IOException
+ */
+ HttpURLConnection create(URL url) throws IOException;
+
+ /**
+ * Open an {@link HttpURLConnection} for the specified {@link URL} and
+ * {@link Proxy}.
+ *
+ * @throws IOException
+ */
+ HttpURLConnection create(URL url, Proxy proxy) throws IOException;
+
+ /**
+ * A {@link ConnectionFactory} which uses the built-in
+ * {@link URL#openConnection()}
+ */
+ ConnectionFactory DEFAULT = new ConnectionFactory() {
+ public HttpURLConnection create(URL url) throws IOException {
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ public HttpURLConnection create(URL url, Proxy proxy) throws IOException {
+ return (HttpURLConnection) url.openConnection(proxy);
+ }
+ };
+ }
+
+ private static ConnectionFactory CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
+
+ /**
+ * Specify the {@link ConnectionFactory} used to create new requests.
+ */
+ public static void setConnectionFactory(final ConnectionFactory connectionFactory) {
+ if (connectionFactory == null)
+ CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
+ else
+ CONNECTION_FACTORY = connectionFactory;
+ }
+
+ /**
+ * Callback interface for reporting upload progress for a request.
+ */
+ public interface UploadProgress {
+ /**
+ * Callback invoked as data is uploaded by the request.
+ *
+ * @param uploaded The number of bytes already uploaded
+ * @param total The total number of bytes that will be uploaded or -1 if the
+ * length is unknown.
+ */
+ void onUpload(long uploaded, long total);
+
+ UploadProgress DEFAULT = new UploadProgress() {
+ public void onUpload(long uploaded, long total) {
+ }
+ };
+ }
+
+ /**
+ * <p>
+ * Encodes and decodes to and from Base64 notation.
+ * </p>
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will. This
+ * software comes with no guarantees or warranties but with plenty of
+ * well-wishing instead! Please visit
+ * <a href="http://iharder.net/base64">http://iharder.net/base64</a>
+ * periodically to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.3.7
+ */
+ public static class Base64 {
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** Preferred encoding. */
+ private final static String PREFERRED_ENCODING = "US-ASCII";
+
+ /** The 64 valid Base64 values. */
+ private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E',
+ (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
+ (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W',
+ (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
+ (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
+ (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6',
+ (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' };
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /**
+ * <p>
+ * Encodes up to three bytes of the array <var>source</var> and writes the
+ * resulting four Base64 bytes to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method does
+ * not check to make sure your arrays are large enough to accomodate
+ * <var>srcOffset</var> + 3 for the <var>source</var> array or
+ * <var>destOffset</var> + 4 for the <var>destination</var> array. The actual
+ * number of significant bytes in your array is given by <var>numSigBytes</var>.
+ * </p>
+ * <p>
+ * This is the lowest level of the encoding methods with all possible
+ * parameters.
+ * </p>
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the <var>destination</var> array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination,
+ int destOffset) {
+
+ byte[] ALPHABET = _STANDARD_ALPHABET;
+
+ int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
+ return destination;
+
+ case 2:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ }
+ }
+
+ /**
+ * Encode string as a byte array in Base64 annotation.
+ *
+ * @param string
+ * @return The Base64-encoded data as a string
+ */
+ public static String encode(String string) {
+ byte[] bytes;
+ try {
+ bytes = string.getBytes(PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ bytes = string.getBytes();
+ }
+ return encodeBytes(bytes);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source) {
+ return encodeBytes(source, 0, source.length);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source, int off, int len) {
+ byte[] encoded = encodeBytesToBytes(source, off, len);
+ try {
+ return new String(encoded, PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException uue) {
+ return new String(encoded);
+ }
+ }
+
+ /**
+ * Similar to {@link #encodeBytes(byte[], int, int)} but returns a byte array
+ * instead of instantiating a String. This is more efficient if you're working
+ * with I/O streams and have large data sets to encode.
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes(byte[] source, int off, int len) {
+
+ if (source == null)
+ throw new NullPointerException("Cannot serialize a null array.");
+
+ if (off < 0)
+ throw new IllegalArgumentException("Cannot have negative offset: " + off);
+
+ if (len < 0)
+ throw new IllegalArgumentException("Cannot have length offset: " + len);
+
+ if (off + len > source.length)
+ throw new IllegalArgumentException(String
+ .format("Cannot have offset of %d and length of %d with array of length %d", off, len, source.length));
+
+ // Bytes needed for actual encoding
+ int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0);
+
+ byte[] outBuff = new byte[encLen];
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ for (; d < len2; d += 3, e += 4)
+ encode3to4(source, d + off, 3, outBuff, e);
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e);
+ e += 4;
+ }
+
+ if (e <= outBuff.length - 1) {
+ byte[] finalOut = new byte[e];
+ System.arraycopy(outBuff, 0, finalOut, 0, e);
+ return finalOut;
+ } else
+ return outBuff;
+ }
+ }
+
+ /**
+ * HTTP request exception whose cause is always an {@link IOException}
+ */
+ public static class HttpRequestException extends RuntimeException {
+
+ private static final long serialVersionUID = -1170466989781746231L;
+
+ /**
+ * Create a new HttpRequestException with the given cause
+ *
+ * @param cause
+ */
+ public HttpRequestException(final IOException cause) {
+ super(cause);
+ }
+
+ /**
+ * Get {@link IOException} that triggered this request exception
+ *
+ * @return {@link IOException} cause
+ */
+ @Override
+ public IOException getCause() {
+ return (IOException) super.getCause();
+ }
+ }
+
+ /**
+ * Operation that handles executing a callback once complete and handling nested
+ * exceptions
+ *
+ * @param <V>
+ */
+ protected static abstract class Operation<V> implements Callable<V> {
+
+ /**
+ * Run operation
+ *
+ * @return result
+ * @throws HttpRequestException
+ * @throws IOException
+ */
+ protected abstract V run() throws HttpRequestException, IOException;
+
+ /**
+ * Operation complete callback
+ *
+ * @throws IOException
+ */
+ protected abstract void done() throws IOException;
+
+ public V call() throws HttpRequestException {
+ boolean thrown = false;
+ try {
+ return run();
+ } catch (HttpRequestException e) {
+ thrown = true;
+ throw e;
+ } catch (IOException e) {
+ thrown = true;
+ throw new HttpRequestException(e);
+ } finally {
+ try {
+ done();
+ } catch (IOException e) {
+ if (!thrown)
+ throw new HttpRequestException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Class that ensures a {@link Closeable} gets closed with proper exception
+ * handling.
+ *
+ * @param <V>
+ */
+ protected static abstract class CloseOperation<V> extends Operation<V> {
+
+ private final Closeable closeable;
+
+ private final boolean ignoreCloseExceptions;
+
+ /**
+ * Create closer for operation
+ *
+ * @param closeable
+ * @param ignoreCloseExceptions
+ */
+ protected CloseOperation(final Closeable closeable, final boolean ignoreCloseExceptions) {
+ this.closeable = closeable;
+ this.ignoreCloseExceptions = ignoreCloseExceptions;
+ }
+
+ @Override
+ protected void done() throws IOException {
+ if (closeable instanceof Flushable)
+ ((Flushable) closeable).flush();
+ if (ignoreCloseExceptions)
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // Ignored
+ }
+ else
+ closeable.close();
+ }
+ }
+
+ /**
+ * Class that and ensures a {@link Flushable} gets flushed with proper exception
+ * handling.
+ *
+ * @param <V>
+ */
+ protected static abstract class FlushOperation<V> extends Operation<V> {
+
+ private final Flushable flushable;
+
+ /**
+ * Create flush operation
+ *
+ * @param flushable
+ */
+ protected FlushOperation(final Flushable flushable) {
+ this.flushable = flushable;
+ }
+
+ @Override
+ protected void done() throws IOException {
+ flushable.flush();
+ }
+ }
+
+ /**
+ * Request output stream
+ */
+ public static class RequestOutputStream extends BufferedOutputStream {
+
+ private final CharsetEncoder encoder;
+
+ /**
+ * Create request output stream
+ *
+ * @param stream
+ * @param charset
+ * @param bufferSize
+ */
+ public RequestOutputStream(final OutputStream stream, final String charset, final int bufferSize) {
+ super(stream, bufferSize);
+
+ encoder = Charset.forName(getValidCharset(charset)).newEncoder();
+ }
+
+ /**
+ * Write string to stream
+ *
+ * @param value
+ * @return this stream
+ * @throws IOException
+ */
+ public RequestOutputStream write(final String value) throws IOException {
+ final ByteBuffer bytes = encoder.encode(CharBuffer.wrap(value));
+
+ super.write(bytes.array(), 0, bytes.limit());
+
+ return this;
+ }
+ }
+
+ /**
+ * Represents array of any type as list of objects so we can easily iterate over
+ * it
+ *
+ * @param array of elements
+ * @return list with the same elements
+ */
+ private static List<Object> arrayToList(final Object array) {
+ if (array instanceof Object[])
+ return Arrays.asList((Object[]) array);
+
+ List<Object> result = new ArrayList<Object>();
+ // Arrays of the primitive types can't be cast to array of Object, so this:
+ if (array instanceof int[])
+ for (int value : (int[]) array)
+ result.add(value);
+ else if (array instanceof boolean[])
+ for (boolean value : (boolean[]) array)
+ result.add(value);
+ else if (array instanceof long[])
+ for (long value : (long[]) array)
+ result.add(value);
+ else if (array instanceof float[])
+ for (float value : (float[]) array)
+ result.add(value);
+ else if (array instanceof double[])
+ for (double value : (double[]) array)
+ result.add(value);
+ else if (array instanceof short[])
+ for (short value : (short[]) array)
+ result.add(value);
+ else if (array instanceof byte[])
+ for (byte value : (byte[]) array)
+ result.add(value);
+ else if (array instanceof char[])
+ for (char value : (char[]) array)
+ result.add(value);
+ return result;
+ }
+
+ /**
+ * Encode the given URL as an ASCII {@link String}
+ * <p>
+ * This method ensures the path and query segments of the URL are properly
+ * encoded such as ' ' characters being encoded to '%20' or any UTF-8 characters
+ * that are non-ASCII. No encoding of URLs is done by default by the
+ * {@link HttpRequest} constructors and so if URL encoding is needed this method
+ * should be called before calling the {@link HttpRequest} constructor.
+ *
+ * @param url
+ * @return encoded URL
+ * @throws HttpRequestException
+ */
+ public static String encode(final CharSequence url) throws HttpRequestException {
+ URL parsed;
+ try {
+ parsed = new URL(url.toString());
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+
+ String host = parsed.getHost();
+ int port = parsed.getPort();
+ if (port != -1)
+ host = host + ':' + Integer.toString(port);
+
+ try {
+ String encoded = new URI(parsed.getProtocol(), host, parsed.getPath(), parsed.getQuery(), null).toASCIIString();
+ int paramsStart = encoded.indexOf('?');
+ if (paramsStart > 0 && paramsStart + 1 < encoded.length())
+ encoded = encoded.substring(0, paramsStart + 1) + encoded.substring(paramsStart + 1).replace("+", "%2B");
+ return encoded;
+ } catch (URISyntaxException e) {
+ IOException io = new IOException("Parsing URI failed");
+ io.initCause(e);
+ throw new HttpRequestException(io);
+ }
+ }
+
+ /**
+ * Append given map as query parameters to the base URL
+ * <p>
+ * Each map entry's key will be a parameter name and the value's
+ * {@link Object#toString()} will be the parameter value.
+ *
+ * @param url
+ * @param params
+ * @return URL with appended query params
+ */
+ public static String append(final CharSequence url, final Map<?, ?> params) {
+ final String baseUrl = url.toString();
+ if (params == null || params.isEmpty())
+ return baseUrl;
+
+ final StringBuilder result = new StringBuilder(baseUrl);
+
+ addPathSeparator(baseUrl, result);
+ addParamPrefix(baseUrl, result);
+
+ Entry<?, ?> entry;
+ Iterator<?> iterator = params.entrySet().iterator();
+ entry = (Entry<?, ?>) iterator.next();
+ addParam(entry.getKey().toString(), entry.getValue(), result);
+
+ while (iterator.hasNext()) {
+ result.append('&');
+ entry = (Entry<?, ?>) iterator.next();
+ addParam(entry.getKey().toString(), entry.getValue(), result);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Append given name/value pairs as query parameters to the base URL
+ * <p>
+ * The params argument is interpreted as a sequence of name/value pairs so the
+ * given number of params must be divisible by 2.
+ *
+ * @param url
+ * @param params name/value pairs
+ * @return URL with appended query params
+ */
+ public static String append(final CharSequence url, final Object... params) {
+ final String baseUrl = url.toString();
+ if (params == null || params.length == 0)
+ return baseUrl;
+
+ if (params.length % 2 != 0)
+ throw new IllegalArgumentException("Must specify an even number of parameter names/values");
+
+ final StringBuilder result = new StringBuilder(baseUrl);
+
+ addPathSeparator(baseUrl, result);
+ addParamPrefix(baseUrl, result);
+
+ addParam(params[0], params[1], result);
+
+ for (int i = 2; i < params.length; i += 2) {
+ result.append('&');
+ addParam(params[i], params[i + 1], result);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Start a 'GET' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest get(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_GET);
+ }
+
+ /**
+ * Start a 'GET' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest get(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_GET);
+ }
+
+ /**
+ * Start a 'GET' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param params The query parameters to include as part of the baseUrl
+ * @param encode true to encode the full URL
+ *
+ * @see #append(CharSequence, Map)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest get(final CharSequence baseUrl, final Map<?, ?> params, final boolean encode) {
+ String url = append(baseUrl, params);
+ return get(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'GET' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param encode true to encode the full URL
+ * @param params the name/value query parameter pairs to include as part of the
+ * baseUrl
+ *
+ * @see #append(CharSequence, Object...)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest get(final CharSequence baseUrl, final boolean encode, final Object... params) {
+ String url = append(baseUrl, params);
+ return get(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'POST' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest post(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_POST);
+ }
+
+ /**
+ * Start a 'POST' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest post(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_POST);
+ }
+
+ /**
+ * Start a 'POST' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param params the query parameters to include as part of the baseUrl
+ * @param encode true to encode the full URL
+ *
+ * @see #append(CharSequence, Map)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest post(final CharSequence baseUrl, final Map<?, ?> params, final boolean encode) {
+ String url = append(baseUrl, params);
+ return post(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'POST' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param encode true to encode the full URL
+ * @param params the name/value query parameter pairs to include as part of the
+ * baseUrl
+ *
+ * @see #append(CharSequence, Object...)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest post(final CharSequence baseUrl, final boolean encode, final Object... params) {
+ String url = append(baseUrl, params);
+ return post(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'PUT' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest put(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_PUT);
+ }
+
+ /**
+ * Start a 'PUT' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest put(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_PUT);
+ }
+
+ /**
+ * Start a 'PUT' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param params the query parameters to include as part of the baseUrl
+ * @param encode true to encode the full URL
+ *
+ * @see #append(CharSequence, Map)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest put(final CharSequence baseUrl, final Map<?, ?> params, final boolean encode) {
+ String url = append(baseUrl, params);
+ return put(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'PUT' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param encode true to encode the full URL
+ * @param params the name/value query parameter pairs to include as part of the
+ * baseUrl
+ *
+ * @see #append(CharSequence, Object...)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest put(final CharSequence baseUrl, final boolean encode, final Object... params) {
+ String url = append(baseUrl, params);
+ return put(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'DELETE' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest delete(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_DELETE);
+ }
+
+ /**
+ * Start a 'DELETE' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest delete(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_DELETE);
+ }
+
+ /**
+ * Start a 'DELETE' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param params The query parameters to include as part of the baseUrl
+ * @param encode true to encode the full URL
+ *
+ * @see #append(CharSequence, Map)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest delete(final CharSequence baseUrl, final Map<?, ?> params, final boolean encode) {
+ String url = append(baseUrl, params);
+ return delete(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'DELETE' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param encode true to encode the full URL
+ * @param params the name/value query parameter pairs to include as part of the
+ * baseUrl
+ *
+ * @see #append(CharSequence, Object...)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest delete(final CharSequence baseUrl, final boolean encode, final Object... params) {
+ String url = append(baseUrl, params);
+ return delete(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'HEAD' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest head(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_HEAD);
+ }
+
+ /**
+ * Start a 'HEAD' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest head(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_HEAD);
+ }
+
+ /**
+ * Start a 'HEAD' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param params The query parameters to include as part of the baseUrl
+ * @param encode true to encode the full URL
+ *
+ * @see #append(CharSequence, Map)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest head(final CharSequence baseUrl, final Map<?, ?> params, final boolean encode) {
+ String url = append(baseUrl, params);
+ return head(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start a 'GET' request to the given URL along with the query params
+ *
+ * @param baseUrl
+ * @param encode true to encode the full URL
+ * @param params the name/value query parameter pairs to include as part of the
+ * baseUrl
+ *
+ * @see #append(CharSequence, Object...)
+ * @see #encode(CharSequence)
+ *
+ * @return request
+ */
+ public static HttpRequest head(final CharSequence baseUrl, final boolean encode, final Object... params) {
+ String url = append(baseUrl, params);
+ return head(encode ? encode(url) : url);
+ }
+
+ /**
+ * Start an 'OPTIONS' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest options(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_OPTIONS);
+ }
+
+ /**
+ * Start an 'OPTIONS' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest options(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_OPTIONS);
+ }
+
+ /**
+ * Start a 'TRACE' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest trace(final CharSequence url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_TRACE);
+ }
+
+ /**
+ * Start a 'TRACE' request to the given URL
+ *
+ * @param url
+ * @return request
+ * @throws HttpRequestException
+ */
+ public static HttpRequest trace(final URL url) throws HttpRequestException {
+ return new HttpRequest(url, METHOD_TRACE);
+ }
+
+ /**
+ * Set the 'http.keepAlive' property to the given value.
+ * <p>
+ * This setting will apply to all requests.
+ *
+ * @param keepAlive
+ */
+ public static void keepAlive(final boolean keepAlive) {
+ setProperty("http.keepAlive", Boolean.toString(keepAlive));
+ }
+
+ /**
+ * Set the 'http.maxConnections' property to the given value.
+ * <p>
+ * This setting will apply to all requests.
+ *
+ * @param maxConnections
+ */
+ public static void maxConnections(final int maxConnections) {
+ setProperty("http.maxConnections", Integer.toString(maxConnections));
+ }
+
+ /**
+ * Set the 'http.proxyHost' and 'https.proxyHost' properties to the given host
+ * value.
+ * <p>
+ * This setting will apply to all requests.
+ *
+ * @param host
+ */
+ public static void proxyHost(final String host) {
+ setProperty("http.proxyHost", host);
+ setProperty("https.proxyHost", host);
+ }
+
+ /**
+ * Set the 'http.proxyPort' and 'https.proxyPort' properties to the given port
+ * number.
+ * <p>
+ * This setting will apply to all requests.
+ *
+ * @param port
+ */
+ public static void proxyPort(final int port) {
+ final String portValue = Integer.toString(port);
+ setProperty("http.proxyPort", portValue);
+ setProperty("https.proxyPort", portValue);
+ }
+
+ /**
+ * Set the 'http.nonProxyHosts' property to the given host values.
+ * <p>
+ * Hosts will be separated by a '|' character.
+ * <p>
+ * This setting will apply to all requests.
+ *
+ * @param hosts
+ */
+ public static void nonProxyHosts(final String... hosts) {
+ if (hosts != null && hosts.length > 0) {
+ StringBuilder separated = new StringBuilder();
+ int last = hosts.length - 1;
+ for (int i = 0; i < last; i++)
+ separated.append(hosts[i]).append('|');
+ separated.append(hosts[last]);
+ setProperty("http.nonProxyHosts", separated.toString());
+ } else
+ setProperty("http.nonProxyHosts", null);
+ }
+
+ /**
+ * Set property to given value.
+ * <p>
+ * Specifying a null value will cause the property to be cleared
+ *
+ * @param name
+ * @param value
+ * @return previous value
+ */
+ private static String setProperty(final String name, final String value) {
+ final PrivilegedAction<String> action;
+ if (value != null)
+ action = new PrivilegedAction<String>() {
+
+ public String run() {
+ return System.setProperty(name, value);
+ }
+ };
+ else
+ action = new PrivilegedAction<String>() {
+
+ public String run() {
+ return System.clearProperty(name);
+ }
+ };
+ return AccessController.doPrivileged(action);
+ }
+
+ private HttpURLConnection connection = null;
+
+ private final URL url;
+
+ private final String requestMethod;
+
+ private RequestOutputStream output;
+
+ private boolean multipart;
+
+ private boolean form;
+
+ private boolean ignoreCloseExceptions = true;
+
+ private boolean uncompress = false;
+
+ private int bufferSize = 8192;
+
+ private long totalSize = -1;
+
+ private long totalWritten = 0;
+
+ private String httpProxyHost;
+
+ private int httpProxyPort;
+
+ private UploadProgress progress = UploadProgress.DEFAULT;
+
+ /**
+ * Create HTTP connection wrapper
+ *
+ * @param url Remote resource URL.
+ * @param method HTTP request method (e.g., "GET", "POST").
+ * @throws HttpRequestException
+ */
+ public HttpRequest(final CharSequence url, final String method) throws HttpRequestException {
+ try {
+ this.url = new URL(url.toString());
+ } catch (MalformedURLException e) {
+ throw new HttpRequestException(e);
+ }
+ this.requestMethod = method;
+ }
+
+ /**
+ * Create HTTP connection wrapper
+ *
+ * @param url Remote resource URL.
+ * @param method HTTP request method (e.g., "GET", "POST").
+ * @throws HttpRequestException
+ */
+ public HttpRequest(final URL url, final String method) throws HttpRequestException {
+ this.url = url;
+ this.requestMethod = method;
+ }
+
+ private Proxy createProxy() {
+ return new Proxy(HTTP, new InetSocketAddress(httpProxyHost, httpProxyPort));
+ }
+
+ private HttpURLConnection createConnection() {
+ try {
+ final HttpURLConnection connection;
+ if (httpProxyHost != null)
+ connection = CONNECTION_FACTORY.create(url, createProxy());
+ else
+ connection = CONNECTION_FACTORY.create(url);
+ connection.setRequestMethod(requestMethod);
+ return connection;
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return method() + ' ' + url();
+ }
+
+ /**
+ * Get underlying connection
+ *
+ * @return connection
+ */
+ public HttpURLConnection getConnection() {
+ if (connection == null)
+ connection = createConnection();
+ return connection;
+ }
+
+ /**
+ * Set whether or not to ignore exceptions that occur from calling
+ * {@link Closeable#close()}
+ * <p>
+ * The default value of this setting is <code>true</code>
+ *
+ * @param ignore
+ * @return this request
+ */
+ public HttpRequest ignoreCloseExceptions(final boolean ignore) {
+ ignoreCloseExceptions = ignore;
+ return this;
+ }
+
+ /**
+ * Get whether or not exceptions thrown by {@link Closeable#close()} are ignored
+ *
+ * @return true if ignoring, false if throwing
+ */
+ public boolean ignoreCloseExceptions() {
+ return ignoreCloseExceptions;
+ }
+
+ /**
+ * Get the status code of the response
+ *
+ * @return the response code
+ * @throws HttpRequestException
+ */
+ public int code() throws HttpRequestException {
+ try {
+ closeOutput();
+ return getConnection().getResponseCode();
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Set the value of the given {@link AtomicInteger} to the status code of the
+ * response
+ *
+ * @param output
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest code(final AtomicInteger output) throws HttpRequestException {
+ output.set(code());
+ return this;
+ }
+
+ /**
+ * Is the response code a 200 OK?
+ *
+ * @return true if 200, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean ok() throws HttpRequestException {
+ return HTTP_OK == code();
+ }
+
+ /**
+ * Is the response code a 201 Created?
+ *
+ * @return true if 201, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean created() throws HttpRequestException {
+ return HTTP_CREATED == code();
+ }
+
+ /**
+ * Is the response code a 204 No Content?
+ *
+ * @return true if 204, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean noContent() throws HttpRequestException {
+ return HTTP_NO_CONTENT == code();
+ }
+
+ /**
+ * Is the response code a 500 Internal Server Error?
+ *
+ * @return true if 500, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean serverError() throws HttpRequestException {
+ return HTTP_INTERNAL_ERROR == code();
+ }
+
+ /**
+ * Is the response code a 400 Bad Request?
+ *
+ * @return true if 400, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean badRequest() throws HttpRequestException {
+ return HTTP_BAD_REQUEST == code();
+ }
+
+ /**
+ * Is the response code a 404 Not Found?
+ *
+ * @return true if 404, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean notFound() throws HttpRequestException {
+ return HTTP_NOT_FOUND == code();
+ }
+
+ /**
+ * Is the response code a 304 Not Modified?
+ *
+ * @return true if 304, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean notModified() throws HttpRequestException {
+ return HTTP_NOT_MODIFIED == code();
+ }
+
+ /**
+ * Get status message of the response
+ *
+ * @return message
+ * @throws HttpRequestException
+ */
+ public String message() throws HttpRequestException {
+ try {
+ closeOutput();
+ return getConnection().getResponseMessage();
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Disconnect the connection
+ *
+ * @return this request
+ */
+ public HttpRequest disconnect() {
+ getConnection().disconnect();
+ return this;
+ }
+
+ /**
+ * Set chunked streaming mode to the given size
+ *
+ * @param size
+ * @return this request
+ */
+ public HttpRequest chunk(final int size) {
+ getConnection().setChunkedStreamingMode(size);
+ return this;
+ }
+
+ /**
+ * Set the size used when buffering and copying between streams
+ * <p>
+ * This size is also used for send and receive buffers created for both char and
+ * byte arrays
+ * <p>
+ * The default buffer size is 8,192 bytes
+ *
+ * @param size
+ * @return this request
+ */
+ public HttpRequest bufferSize(final int size) {
+ if (size < 1)
+ throw new IllegalArgumentException("Size must be greater than zero");
+ bufferSize = size;
+ return this;
+ }
+
+ /**
+ * Get the configured buffer size
+ * <p>
+ * The default buffer size is 8,192 bytes
+ *
+ * @return buffer size
+ */
+ public int bufferSize() {
+ return bufferSize;
+ }
+
+ /**
+ * Set whether or not the response body should be automatically uncompressed
+ * when read from.
+ * <p>
+ * This will only affect requests that have the 'Content-Encoding' response
+ * header set to 'gzip'.
+ * <p>
+ * This causes all receive methods to use a {@link GZIPInputStream} when
+ * applicable so that higher level streams and readers can read the data
+ * uncompressed.
+ * <p>
+ * Setting this option does not cause any request headers to be set
+ * automatically so {@link #acceptGzipEncoding()} should be used in conjunction
+ * with this setting to tell the server to gzip the response.
+ *
+ * @param uncompress
+ * @return this request
+ */
+ public HttpRequest uncompress(final boolean uncompress) {
+ this.uncompress = uncompress;
+ return this;
+ }
+
+ /**
+ * Create byte array output stream
+ *
+ * @return stream
+ */
+ protected ByteArrayOutputStream byteStream() {
+ final int size = contentLength();
+ if (size > 0)
+ return new ByteArrayOutputStream(size);
+ else
+ return new ByteArrayOutputStream();
+ }
+
+ /**
+ * Get response as {@link String} in given character set
+ * <p>
+ * This will fall back to using the UTF-8 character set if the given charset is
+ * null
+ *
+ * @param charset
+ * @return string
+ * @throws HttpRequestException
+ */
+ public String body(final String charset) throws HttpRequestException {
+ final ByteArrayOutputStream output = byteStream();
+ try {
+ copy(buffer(), output);
+ return output.toString(getValidCharset(charset));
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Get response as {@link String} using character set returned from
+ * {@link #charset()}
+ *
+ * @return string
+ * @throws HttpRequestException
+ */
+ public String body() throws HttpRequestException {
+ return body(charset());
+ }
+
+ /**
+ * Get the response body as a {@link String} and set it as the value of the
+ * given reference.
+ *
+ * @param output
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest body(final AtomicReference<String> output) throws HttpRequestException {
+ output.set(body());
+ return this;
+ }
+
+ /**
+ * Get the response body as a {@link String} and set it as the value of the
+ * given reference.
+ *
+ * @param output
+ * @param charset
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest body(final AtomicReference<String> output, final String charset) throws HttpRequestException {
+ output.set(body(charset));
+ return this;
+ }
+
+ /**
+ * Is the response body empty?
+ *
+ * @return true if the Content-Length response header is 0, false otherwise
+ * @throws HttpRequestException
+ */
+ public boolean isBodyEmpty() throws HttpRequestException {
+ return contentLength() == 0;
+ }
+
+ /**
+ * Get response as byte array
+ *
+ * @return byte array
+ * @throws HttpRequestException
+ */
+ public byte[] bytes() throws HttpRequestException {
+ final ByteArrayOutputStream output = byteStream();
+ try {
+ copy(buffer(), output);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return output.toByteArray();
+ }
+
+ /**
+ * Get response in a buffered stream
+ *
+ * @see #bufferSize(int)
+ * @return stream
+ * @throws HttpRequestException
+ */
+ public BufferedInputStream buffer() throws HttpRequestException {
+ return new BufferedInputStream(stream(), bufferSize);
+ }
+
+ /**
+ * Get stream to response body
+ *
+ * @return stream
+ * @throws HttpRequestException
+ */
+ public InputStream stream() throws HttpRequestException {
+ InputStream stream;
+ if (code() < HTTP_BAD_REQUEST)
+ try {
+ stream = getConnection().getInputStream();
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ else {
+ stream = getConnection().getErrorStream();
+ if (stream == null)
+ try {
+ stream = getConnection().getInputStream();
+ } catch (IOException e) {
+ if (contentLength() > 0)
+ throw new HttpRequestException(e);
+ else
+ stream = new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ if (!uncompress || !ENCODING_GZIP.equals(contentEncoding()))
+ return stream;
+ else
+ try {
+ return new GZIPInputStream(stream);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Get reader to response body using given character set.
+ * <p>
+ * This will fall back to using the UTF-8 character set if the given charset is
+ * null
+ *
+ * @param charset
+ * @return reader
+ * @throws HttpRequestException
+ */
+ public InputStreamReader reader(final String charset) throws HttpRequestException {
+ try {
+ return new InputStreamReader(stream(), getValidCharset(charset));
+ } catch (UnsupportedEncodingException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Get reader to response body using the character set returned from
+ * {@link #charset()}
+ *
+ * @return reader
+ * @throws HttpRequestException
+ */
+ public InputStreamReader reader() throws HttpRequestException {
+ return reader(charset());
+ }
+
+ /**
+ * Get buffered reader to response body using the given character set r and the
+ * configured buffer size
+ *
+ *
+ * @see #bufferSize(int)
+ * @param charset
+ * @return reader
+ * @throws HttpRequestException
+ */
+ public BufferedReader bufferedReader(final String charset) throws HttpRequestException {
+ return new BufferedReader(reader(charset), bufferSize);
+ }
+
+ /**
+ * Get buffered reader to response body using the character set returned from
+ * {@link #charset()} and the configured buffer size
+ *
+ * @see #bufferSize(int)
+ * @return reader
+ * @throws HttpRequestException
+ */
+ public BufferedReader bufferedReader() throws HttpRequestException {
+ return bufferedReader(charset());
+ }
+
+ /**
+ * Stream response body to file
+ *
+ * @param file
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest receive(final File file) throws HttpRequestException {
+ final OutputStream output;
+ try {
+ output = new BufferedOutputStream(new FileOutputStream(file), bufferSize);
+ } catch (FileNotFoundException e) {
+ throw new HttpRequestException(e);
+ }
+ return new CloseOperation<HttpRequest>(output, ignoreCloseExceptions) {
+
+ @Override
+ protected HttpRequest run() throws HttpRequestException, IOException {
+ return receive(output);
+ }
+ }.call();
+ }
+
+ /**
+ * Stream response to given output stream
+ *
+ * @param output
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest receive(final OutputStream output) throws HttpRequestException {
+ try {
+ return copy(buffer(), output);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Stream response to given print stream
+ *
+ * @param output
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest receive(final PrintStream output) throws HttpRequestException {
+ return receive((OutputStream) output);
+ }
+
+ /**
+ * Receive response into the given appendable
+ *
+ * @param appendable
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest receive(final Appendable appendable) throws HttpRequestException {
+ final BufferedReader reader = bufferedReader();
+ return new CloseOperation<HttpRequest>(reader, ignoreCloseExceptions) {
+
+ @Override
+ public HttpRequest run() throws IOException {
+ final CharBuffer buffer = CharBuffer.allocate(bufferSize);
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ buffer.rewind();
+ appendable.append(buffer, 0, read);
+ buffer.rewind();
+ }
+ return HttpRequest.this;
+ }
+ }.call();
+ }
+
+ /**
+ * Receive response into the given writer
+ *
+ * @param writer
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest receive(final Writer writer) throws HttpRequestException {
+ final BufferedReader reader = bufferedReader();
+ return new CloseOperation<HttpRequest>(reader, ignoreCloseExceptions) {
+
+ @Override
+ public HttpRequest run() throws IOException {
+ return copy(reader, writer);
+ }
+ }.call();
+ }
+
+ /**
+ * Set read timeout on connection to given value
+ *
+ * @param timeout
+ * @return this request
+ */
+ public HttpRequest readTimeout(final int timeout) {
+ getConnection().setReadTimeout(timeout);
+ return this;
+ }
+
+ /**
+ * Set connect timeout on connection to given value
+ *
+ * @param timeout
+ * @return this request
+ */
+ public HttpRequest connectTimeout(final int timeout) {
+ getConnection().setConnectTimeout(timeout);
+ return this;
+ }
+
+ /**
+ * Set header name to given value
+ *
+ * @param name
+ * @param value
+ * @return this request
+ */
+ public HttpRequest header(final String name, final String value) {
+ getConnection().setRequestProperty(name, value);
+ return this;
+ }
+
+ /**
+ * Set header name to given value
+ *
+ * @param name
+ * @param value
+ * @return this request
+ */
+ public HttpRequest header(final String name, final Number value) {
+ return header(name, value != null ? value.toString() : null);
+ }
+
+ /**
+ * Set all headers found in given map where the keys are the header names and
+ * the values are the header values
+ *
+ * @param headers
+ * @return this request
+ */
+ public HttpRequest headers(final Map<String, String> headers) {
+ if (!headers.isEmpty())
+ for (Entry<String, String> header : headers.entrySet())
+ header(header);
+ return this;
+ }
+
+ /**
+ * Set header to have given entry's key as the name and value as the value
+ *
+ * @param header
+ * @return this request
+ */
+ public HttpRequest header(final Entry<String, String> header) {
+ return header(header.getKey(), header.getValue());
+ }
+
+ /**
+ * Get a response header
+ *
+ * @param name
+ * @return response header
+ * @throws HttpRequestException
+ */
+ public String header(final String name) throws HttpRequestException {
+ closeOutputQuietly();
+ return getConnection().getHeaderField(name);
+ }
+
+ /**
+ * Get all the response headers
+ *
+ * @return map of response header names to their value(s)
+ * @throws HttpRequestException
+ */
+ public Map<String, List<String>> headers() throws HttpRequestException {
+ closeOutputQuietly();
+ return getConnection().getHeaderFields();
+ }
+
+ /**
+ * Get a date header from the response falling back to returning -1 if the
+ * header is missing or parsing fails
+ *
+ * @param name
+ * @return date, -1 on failures
+ * @throws HttpRequestException
+ */
+ public long dateHeader(final String name) throws HttpRequestException {
+ return dateHeader(name, -1L);
+ }
+
+ /**
+ * Get a date header from the response falling back to returning the given
+ * default value if the header is missing or parsing fails
+ *
+ * @param name
+ * @param defaultValue
+ * @return date, default value on failures
+ * @throws HttpRequestException
+ */
+ public long dateHeader(final String name, final long defaultValue) throws HttpRequestException {
+ closeOutputQuietly();
+ return getConnection().getHeaderFieldDate(name, defaultValue);
+ }
+
+ /**
+ * Get an integer header from the response falling back to returning -1 if the
+ * header is missing or parsing fails
+ *
+ * @param name
+ * @return header value as an integer, -1 when missing or parsing fails
+ * @throws HttpRequestException
+ */
+ public int intHeader(final String name) throws HttpRequestException {
+ return intHeader(name, -1);
+ }
+
+ /**
+ * Get an integer header value from the response falling back to the given
+ * default value if the header is missing or if parsing fails
+ *
+ * @param name
+ * @param defaultValue
+ * @return header value as an integer, default value when missing or parsing
+ * fails
+ * @throws HttpRequestException
+ */
+ public int intHeader(final String name, final int defaultValue) throws HttpRequestException {
+ closeOutputQuietly();
+ return getConnection().getHeaderFieldInt(name, defaultValue);
+ }
+
+ /**
+ * Get all values of the given header from the response
+ *
+ * @param name
+ * @return non-null but possibly empty array of {@link String} header values
+ */
+ public String[] headers(final String name) {
+ final Map<String, List<String>> headers = headers();
+ if (headers == null || headers.isEmpty())
+ return EMPTY_STRINGS;
+
+ final List<String> values = headers.get(name);
+ if (values != null && !values.isEmpty())
+ return values.toArray(new String[values.size()]);
+ else
+ return EMPTY_STRINGS;
+ }
+
+ /**
+ * Get parameter with given name from header value in response
+ *
+ * @param headerName
+ * @param paramName
+ * @return parameter value or null if missing
+ */
+ public String parameter(final String headerName, final String paramName) {
+ return getParam(header(headerName), paramName);
+ }
+
+ /**
+ * Get all parameters from header value in response
+ * <p>
+ * This will be all key=value pairs after the first ';' that are separated by a
+ * ';'
+ *
+ * @param headerName
+ * @return non-null but possibly empty map of parameter headers
+ */
+ public Map<String, String> parameters(final String headerName) {
+ return getParams(header(headerName));
+ }
+
+ /**
+ * Get parameter values from header value
+ *
+ * @param header
+ * @return parameter value or null if none
+ */
+ protected Map<String, String> getParams(final String header) {
+ if (header == null || header.length() == 0)
+ return Collections.emptyMap();
+
+ final int headerLength = header.length();
+ int start = header.indexOf(';') + 1;
+ if (start == 0 || start == headerLength)
+ return Collections.emptyMap();
+
+ int end = header.indexOf(';', start);
+ if (end == -1)
+ end = headerLength;
+
+ Map<String, String> params = new LinkedHashMap<String, String>();
+ while (start < end) {
+ int nameEnd = header.indexOf('=', start);
+ if (nameEnd != -1 && nameEnd < end) {
+ String name = header.substring(start, nameEnd).trim();
+ if (name.length() > 0) {
+ String value = header.substring(nameEnd + 1, end).trim();
+ int length = value.length();
+ if (length != 0)
+ if (length > 2 && '"' == value.charAt(0) && '"' == value.charAt(length - 1))
+ params.put(name, value.substring(1, length - 1));
+ else
+ params.put(name, value);
+ }
+ }
+
+ start = end + 1;
+ end = header.indexOf(';', start);
+ if (end == -1)
+ end = headerLength;
+ }
+
+ return params;
+ }
+
+ /**
+ * Get parameter value from header value
+ *
+ * @param value
+ * @param paramName
+ * @return parameter value or null if none
+ */
+ protected String getParam(final String value, final String paramName) {
+ if (value == null || value.length() == 0)
+ return null;
+
+ final int length = value.length();
+ int start = value.indexOf(';') + 1;
+ if (start == 0 || start == length)
+ return null;
+
+ int end = value.indexOf(';', start);
+ if (end == -1)
+ end = length;
+
+ while (start < end) {
+ int nameEnd = value.indexOf('=', start);
+ if (nameEnd != -1 && nameEnd < end && paramName.equals(value.substring(start, nameEnd).trim())) {
+ String paramValue = value.substring(nameEnd + 1, end).trim();
+ int valueLength = paramValue.length();
+ if (valueLength != 0)
+ if (valueLength > 2 && '"' == paramValue.charAt(0) && '"' == paramValue.charAt(valueLength - 1))
+ return paramValue.substring(1, valueLength - 1);
+ else
+ return paramValue;
+ }
+
+ start = end + 1;
+ end = value.indexOf(';', start);
+ if (end == -1)
+ end = length;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get 'charset' parameter from 'Content-Type' response header
+ *
+ * @return charset or null if none
+ */
+ public String charset() {
+ return parameter(HEADER_CONTENT_TYPE, PARAM_CHARSET);
+ }
+
+ /**
+ * Set the 'User-Agent' header to given value
+ *
+ * @param userAgent
+ * @return this request
+ */
+ public HttpRequest userAgent(final String userAgent) {
+ return header(HEADER_USER_AGENT, userAgent);
+ }
+
+ /**
+ * Set the 'Referer' header to given value
+ *
+ * @param referer
+ * @return this request
+ */
+ public HttpRequest referer(final String referer) {
+ return header(HEADER_REFERER, referer);
+ }
+
+ /**
+ * Set value of {@link HttpURLConnection#setUseCaches(boolean)}
+ *
+ * @param useCaches
+ * @return this request
+ */
+ public HttpRequest useCaches(final boolean useCaches) {
+ getConnection().setUseCaches(useCaches);
+ return this;
+ }
+
+ /**
+ * Set the 'Accept-Encoding' header to given value
+ *
+ * @param acceptEncoding
+ * @return this request
+ */
+ public HttpRequest acceptEncoding(final String acceptEncoding) {
+ return header(HEADER_ACCEPT_ENCODING, acceptEncoding);
+ }
+
+ /**
+ * Set the 'Accept-Encoding' header to 'gzip'
+ *
+ * @see #uncompress(boolean)
+ * @return this request
+ */
+ public HttpRequest acceptGzipEncoding() {
+ return acceptEncoding(ENCODING_GZIP);
+ }
+
+ /**
+ * Set the 'Accept-Charset' header to given value
+ *
+ * @param acceptCharset
+ * @return this request
+ */
+ public HttpRequest acceptCharset(final String acceptCharset) {
+ return header(HEADER_ACCEPT_CHARSET, acceptCharset);
+ }
+
+ /**
+ * Get the 'Content-Encoding' header from the response
+ *
+ * @return this request
+ */
+ public String contentEncoding() {
+ return header(HEADER_CONTENT_ENCODING);
+ }
+
+ /**
+ * Get the 'Server' header from the response
+ *
+ * @return server
+ */
+ public String server() {
+ return header(HEADER_SERVER);
+ }
+
+ /**
+ * Get the 'Date' header from the response
+ *
+ * @return date value, -1 on failures
+ */
+ public long date() {
+ return dateHeader(HEADER_DATE);
+ }
+
+ /**
+ * Get the 'Cache-Control' header from the response
+ *
+ * @return cache control
+ */
+ public String cacheControl() {
+ return header(HEADER_CACHE_CONTROL);
+ }
+
+ /**
+ * Get the 'ETag' header from the response
+ *
+ * @return entity tag
+ */
+ public String eTag() {
+ return header(HEADER_ETAG);
+ }
+
+ /**
+ * Get the 'Expires' header from the response
+ *
+ * @return expires value, -1 on failures
+ */
+ public long expires() {
+ return dateHeader(HEADER_EXPIRES);
+ }
+
+ /**
+ * Get the 'Last-Modified' header from the response
+ *
+ * @return last modified value, -1 on failures
+ */
+ public long lastModified() {
+ return dateHeader(HEADER_LAST_MODIFIED);
+ }
+
+ /**
+ * Get the 'Location' header from the response
+ *
+ * @return location
+ */
+ public String location() {
+ return header(HEADER_LOCATION);
+ }
+
+ /**
+ * Set the 'Authorization' header to given value
+ *
+ * @param authorization
+ * @return this request
+ */
+ public HttpRequest authorization(final String authorization) {
+ return header(HEADER_AUTHORIZATION, authorization);
+ }
+
+ /**
+ * Set the 'Proxy-Authorization' header to given value
+ *
+ * @param proxyAuthorization
+ * @return this request
+ */
+ public HttpRequest proxyAuthorization(final String proxyAuthorization) {
+ return header(HEADER_PROXY_AUTHORIZATION, proxyAuthorization);
+ }
+
+ /**
+ * Set the 'Authorization' header to given values in Basic authentication format
+ *
+ * @param name
+ * @param password
+ * @return this request
+ */
+ public HttpRequest basic(final String name, final String password) {
+ return authorization("Basic " + Base64.encode(name + ':' + password));
+ }
+
+ /**
+ * Set the 'Proxy-Authorization' header to given values in Basic authentication
+ * format
+ *
+ * @param name
+ * @param password
+ * @return this request
+ */
+ public HttpRequest proxyBasic(final String name, final String password) {
+ return proxyAuthorization("Basic " + Base64.encode(name + ':' + password));
+ }
+
+ /**
+ * Set the 'If-Modified-Since' request header to the given value
+ *
+ * @param ifModifiedSince
+ * @return this request
+ */
+ public HttpRequest ifModifiedSince(final long ifModifiedSince) {
+ getConnection().setIfModifiedSince(ifModifiedSince);
+ return this;
+ }
+
+ /**
+ * Set the 'If-None-Match' request header to the given value
+ *
+ * @param ifNoneMatch
+ * @return this request
+ */
+ public HttpRequest ifNoneMatch(final String ifNoneMatch) {
+ return header(HEADER_IF_NONE_MATCH, ifNoneMatch);
+ }
+
+ /**
+ * Set the 'Content-Type' request header to the given value
+ *
+ * @param contentType
+ * @return this request
+ */
+ public HttpRequest contentType(final String contentType) {
+ return contentType(contentType, null);
+ }
+
+ /**
+ * Set the 'Content-Type' request header to the given value and charset
+ *
+ * @param contentType
+ * @param charset
+ * @return this request
+ */
+ public HttpRequest contentType(final String contentType, final String charset) {
+ if (charset != null && charset.length() > 0) {
+ final String separator = "; " + PARAM_CHARSET + '=';
+ return header(HEADER_CONTENT_TYPE, contentType + separator + charset);
+ } else
+ return header(HEADER_CONTENT_TYPE, contentType);
+ }
+
+ /**
+ * Get the 'Content-Type' header from the response
+ *
+ * @return response header value
+ */
+ public String contentType() {
+ return header(HEADER_CONTENT_TYPE);
+ }
+
+ /**
+ * Get the 'Content-Length' header from the response
+ *
+ * @return response header value
+ */
+ public int contentLength() {
+ return intHeader(HEADER_CONTENT_LENGTH);
+ }
+
+ /**
+ * Set the 'Content-Length' request header to the given value
+ *
+ * @param contentLength
+ * @return this request
+ */
+ public HttpRequest contentLength(final String contentLength) {
+ return contentLength(Integer.parseInt(contentLength));
+ }
+
+ /**
+ * Set the 'Content-Length' request header to the given value
+ *
+ * @param contentLength
+ * @return this request
+ */
+ public HttpRequest contentLength(final int contentLength) {
+ getConnection().setFixedLengthStreamingMode(contentLength);
+ return this;
+ }
+
+ /**
+ * Set the 'Accept' header to given value
+ *
+ * @param accept
+ * @return this request
+ */
+ public HttpRequest accept(final String accept) {
+ return header(HEADER_ACCEPT, accept);
+ }
+
+ /**
+ * Set the 'Accept' header to 'application/json'
+ *
+ * @return this request
+ */
+ public HttpRequest acceptJson() {
+ return accept(CONTENT_TYPE_JSON);
+ }
+
+ /**
+ * Copy from input stream to output stream
+ *
+ * @param input
+ * @param output
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest copy(final InputStream input, final OutputStream output) throws IOException {
+ return new CloseOperation<HttpRequest>(input, ignoreCloseExceptions) {
+
+ @Override
+ public HttpRequest run() throws IOException {
+ final byte[] buffer = new byte[bufferSize];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ totalWritten += read;
+ progress.onUpload(totalWritten, totalSize);
+ }
+ return HttpRequest.this;
+ }
+ }.call();
+ }
+
+ /**
+ * Copy from reader to writer
+ *
+ * @param input
+ * @param output
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest copy(final Reader input, final Writer output) throws IOException {
+ return new CloseOperation<HttpRequest>(input, ignoreCloseExceptions) {
+
+ @Override
+ public HttpRequest run() throws IOException {
+ final char[] buffer = new char[bufferSize];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ totalWritten += read;
+ progress.onUpload(totalWritten, -1);
+ }
+ return HttpRequest.this;
+ }
+ }.call();
+ }
+
+ /**
+ * Set the UploadProgress callback for this request
+ *
+ * @param callback
+ * @return this request
+ */
+ public HttpRequest progress(final UploadProgress callback) {
+ if (callback == null)
+ progress = UploadProgress.DEFAULT;
+ else
+ progress = callback;
+ return this;
+ }
+
+ private HttpRequest incrementTotalSize(final long size) {
+ if (totalSize == -1)
+ totalSize = 0;
+ totalSize += size;
+ return this;
+ }
+
+ /**
+ * Close output stream
+ *
+ * @return this request
+ * @throws HttpRequestException
+ * @throws IOException
+ */
+ protected HttpRequest closeOutput() throws IOException {
+ progress(null);
+ if (output == null)
+ return this;
+ if (multipart)
+ output.write(CRLF + "--" + BOUNDARY + "--" + CRLF);
+ if (ignoreCloseExceptions)
+ try {
+ output.close();
+ } catch (IOException ignored) {
+ // Ignored
+ }
+ else
+ output.close();
+ output = null;
+ return this;
+ }
+
+ /**
+ * Call {@link #closeOutput()} and re-throw a caught {@link IOException}s as an
+ * {@link HttpRequestException}
+ *
+ * @return this request
+ * @throws HttpRequestException
+ */
+ protected HttpRequest closeOutputQuietly() throws HttpRequestException {
+ try {
+ return closeOutput();
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Open output stream
+ *
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest openOutput() throws IOException {
+ if (output != null)
+ return this;
+ getConnection().setDoOutput(true);
+ final String charset = getParam(getConnection().getRequestProperty(HEADER_CONTENT_TYPE), PARAM_CHARSET);
+ output = new RequestOutputStream(getConnection().getOutputStream(), charset, bufferSize);
+ return this;
+ }
+
+ /**
+ * Start part of a multipart
+ *
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest startPart() throws IOException {
+ if (!multipart) {
+ multipart = true;
+ contentType(CONTENT_TYPE_MULTIPART).openOutput();
+ output.write("--" + BOUNDARY + CRLF);
+ } else
+ output.write(CRLF + "--" + BOUNDARY + CRLF);
+ return this;
+ }
+
+ /**
+ * Write part header
+ *
+ * @param name
+ * @param filename
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest writePartHeader(final String name, final String filename) throws IOException {
+ return writePartHeader(name, filename, null);
+ }
+
+ /**
+ * Write part header
+ *
+ * @param name
+ * @param filename
+ * @param contentType
+ * @return this request
+ * @throws IOException
+ */
+ protected HttpRequest writePartHeader(final String name, final String filename, final String contentType)
+ throws IOException {
+ final StringBuilder partBuffer = new StringBuilder();
+ partBuffer.append("form-data; name=\"").append(name);
+ if (filename != null)
+ partBuffer.append("\"; filename=\"").append(filename);
+ partBuffer.append('"');
+ partHeader("Content-Disposition", partBuffer.toString());
+ if (contentType != null)
+ partHeader(HEADER_CONTENT_TYPE, contentType);
+ return send(CRLF);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param part
+ * @return this request
+ */
+ public HttpRequest part(final String name, final String part) {
+ return part(name, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final String part) throws HttpRequestException {
+ return part(name, filename, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param contentType value of the Content-Type part header
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final String contentType, final String part)
+ throws HttpRequestException {
+ try {
+ startPart();
+ writePartHeader(name, filename, contentType);
+ output.write(part);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return this;
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final Number part) throws HttpRequestException {
+ return part(name, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final Number part) throws HttpRequestException {
+ return part(name, filename, part != null ? part.toString() : null);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final File part) throws HttpRequestException {
+ return part(name, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final File part) throws HttpRequestException {
+ return part(name, filename, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param contentType value of the Content-Type part header
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final String contentType, final File part)
+ throws HttpRequestException {
+ final InputStream stream;
+ try {
+ stream = new BufferedInputStream(new FileInputStream(part));
+ incrementTotalSize(part.length());
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return part(name, filename, contentType, stream);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final InputStream part) throws HttpRequestException {
+ return part(name, null, null, part);
+ }
+
+ /**
+ * Write part of a multipart request to the request body
+ *
+ * @param name
+ * @param filename
+ * @param contentType value of the Content-Type part header
+ * @param part
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest part(final String name, final String filename, final String contentType, final InputStream part)
+ throws HttpRequestException {
+ try {
+ startPart();
+ writePartHeader(name, filename, contentType);
+ copy(part, output);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return this;
+ }
+
+ /**
+ * Write a multipart header to the response body
+ *
+ * @param name
+ * @param value
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest partHeader(final String name, final String value) throws HttpRequestException {
+ return send(name).send(": ").send(value).send(CRLF);
+ }
+
+ /**
+ * Write contents of file to request body
+ *
+ * @param input
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest send(final File input) throws HttpRequestException {
+ final InputStream stream;
+ try {
+ stream = new BufferedInputStream(new FileInputStream(input));
+ incrementTotalSize(input.length());
+ } catch (FileNotFoundException e) {
+ throw new HttpRequestException(e);
+ }
+ return send(stream);
+ }
+
+ /**
+ * Write byte array to request body
+ *
+ * @param input
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest send(final byte[] input) throws HttpRequestException {
+ if (input != null)
+ incrementTotalSize(input.length);
+ return send(new ByteArrayInputStream(input));
+ }
+
+ /**
+ * Write stream to request body
+ * <p>
+ * The given stream will be closed once sending completes
+ *
+ * @param input
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest send(final InputStream input) throws HttpRequestException {
+ try {
+ openOutput();
+ copy(input, output);
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return this;
+ }
+
+ /**
+ * Write reader to request body
+ * <p>
+ * The given reader will be closed once sending completes
+ *
+ * @param input
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest send(final Reader input) throws HttpRequestException {
+ try {
+ openOutput();
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ final Writer writer = new OutputStreamWriter(output, output.encoder.charset());
+ return new FlushOperation<HttpRequest>(writer) {
+
+ @Override
+ protected HttpRequest run() throws IOException {
+ return copy(input, writer);
+ }
+ }.call();
+ }
+
+ /**
+ * Write char sequence to request body
+ * <p>
+ * The charset configured via {@link #contentType(String)} will be used and
+ * UTF-8 will be used if it is unset.
+ *
+ * @param value
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest send(final CharSequence value) throws HttpRequestException {
+ try {
+ openOutput();
+ output.write(value.toString());
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return this;
+ }
+
+ /**
+ * Create writer to request output stream
+ *
+ * @return writer
+ * @throws HttpRequestException
+ */
+ public OutputStreamWriter writer() throws HttpRequestException {
+ try {
+ openOutput();
+ return new OutputStreamWriter(output, output.encoder.charset());
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ }
+
+ /**
+ * Write the values in the map as form data to the request body
+ * <p>
+ * The pairs specified will be URL-encoded in UTF-8 and sent with the
+ * 'application/x-www-form-urlencoded' content-type
+ *
+ * @param values
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Map<?, ?> values) throws HttpRequestException {
+ return form(values, CHARSET_UTF8);
+ }
+
+ /**
+ * Write the key and value in the entry as form data to the request body
+ * <p>
+ * The pair specified will be URL-encoded in UTF-8 and sent with the
+ * 'application/x-www-form-urlencoded' content-type
+ *
+ * @param entry
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Entry<?, ?> entry) throws HttpRequestException {
+ return form(entry, CHARSET_UTF8);
+ }
+
+ /**
+ * Write the key and value in the entry as form data to the request body
+ * <p>
+ * The pair specified will be URL-encoded and sent with the
+ * 'application/x-www-form-urlencoded' content-type
+ *
+ * @param entry
+ * @param charset
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Entry<?, ?> entry, final String charset) throws HttpRequestException {
+ return form(entry.getKey(), entry.getValue(), charset);
+ }
+
+ /**
+ * Write the name/value pair as form data to the request body
+ * <p>
+ * The pair specified will be URL-encoded in UTF-8 and sent with the
+ * 'application/x-www-form-urlencoded' content-type
+ *
+ * @param name
+ * @param value
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Object name, final Object value) throws HttpRequestException {
+ return form(name, value, CHARSET_UTF8);
+ }
+
+ /**
+ * Write the name/value pair as form data to the request body
+ * <p>
+ * The values specified will be URL-encoded and sent with the
+ * 'application/x-www-form-urlencoded' content-type
+ *
+ * @param name
+ * @param value
+ * @param charset
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Object name, final Object value, String charset) throws HttpRequestException {
+ final boolean first = !form;
+ if (first) {
+ contentType(CONTENT_TYPE_FORM, charset);
+ form = true;
+ }
+ charset = getValidCharset(charset);
+ try {
+ openOutput();
+ if (!first)
+ output.write('&');
+ output.write(URLEncoder.encode(name.toString(), charset));
+ output.write('=');
+ if (value != null)
+ output.write(URLEncoder.encode(value.toString(), charset));
+ } catch (IOException e) {
+ throw new HttpRequestException(e);
+ }
+ return this;
+ }
+
+ /**
+ * Write the values in the map as encoded form data to the request body
+ *
+ * @param values
+ * @param charset
+ * @return this request
+ * @throws HttpRequestException
+ */
+ public HttpRequest form(final Map<?, ?> values, final String charset) throws HttpRequestException {
+ if (!values.isEmpty())
+ for (Entry<?, ?> entry : values.entrySet())
+ form(entry, charset);
+ return this;
+ }
+
+ public HttpRequest setSSLSocketFactory(SSLSocketFactory socketFactory) throws HttpRequestException {
+ final HttpURLConnection connection = getConnection();
+ if (connection instanceof HttpsURLConnection)
+ ((HttpsURLConnection) connection).setSSLSocketFactory(socketFactory);
+ return this;
+ }
+
+ public HttpRequest setHostnameVerifier(HostnameVerifier verifier) {
+ final HttpURLConnection connection = getConnection();
+ if (connection instanceof HttpsURLConnection)
+ ((HttpsURLConnection) connection).setHostnameVerifier(verifier);
+ return this;
+ }
+
+ /**
+ * Get the {@link URL} of this request's connection
+ *
+ * @return request URL
+ */
+ public URL url() {
+ return getConnection().getURL();
+ }
+
+ /**
+ * Get the HTTP method of this request
+ *
+ * @return method
+ */
+ public String method() {
+ return getConnection().getRequestMethod();
+ }
+
+ /**
+ * Configure an HTTP proxy on this connection. Use
+ * {{@link #proxyBasic(String, String)} if this proxy requires basic
+ * authentication.
+ *
+ * @param proxyHost
+ * @param proxyPort
+ * @return this request
+ */
+ public HttpRequest useProxy(final String proxyHost, final int proxyPort) {
+ if (connection != null)
+ throw new IllegalStateException(
+ "The connection has already been created. This method must be called before reading or writing to the request.");
+
+ this.httpProxyHost = proxyHost;
+ this.httpProxyPort = proxyPort;
+ return this;
+ }
+
+ /**
+ * Set whether or not the underlying connection should follow redirects in the
+ * response.
+ *
+ * @param followRedirects - true fo follow redirects, false to not.
+ * @return this request
+ */
+ public HttpRequest followRedirects(final boolean followRedirects) {
+ getConnection().setInstanceFollowRedirects(followRedirects);
+ return this;
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/JsonUtils.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/JsonUtils.java
new file mode 100644
index 00000000..72f1b480
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/JsonUtils.java
@@ -0,0 +1,58 @@
+package com.silkimen.http;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class JsonUtils {
+ public static HashMap<String, String> getStringMap(JSONObject object) throws JSONException {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ if (object == null) {
+ return map;
+ }
+
+ Iterator<?> i = object.keys();
+
+ while (i.hasNext()) {
+ String key = (String) i.next();
+ map.put(key, object.getString(key));
+ }
+ return map;
+ }
+
+ public static HashMap<String, Object> getObjectMap(JSONObject object) throws JSONException {
+ HashMap<String, Object> map = new HashMap<String, Object>();
+
+ if (object == null) {
+ return map;
+ }
+
+ Iterator<?> i = object.keys();
+
+ while (i.hasNext()) {
+ String key = (String) i.next();
+ Object value = object.get(key);
+
+ if (value instanceof JSONArray) {
+ map.put(key, getObjectList((JSONArray) value));
+ } else {
+ map.put(key, object.get(key));
+ }
+ }
+ return map;
+ }
+
+ public static ArrayList<Object> getObjectList(JSONArray array) throws JSONException {
+ ArrayList<Object> list = new ArrayList<Object>();
+
+ for (int i = 0; i < array.length(); i++) {
+ list.add(array.get(i));
+ }
+ return list;
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/KeyChainKeyManager.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/KeyChainKeyManager.java
new file mode 100644
index 00000000..ecdaa38c
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/KeyChainKeyManager.java
@@ -0,0 +1,57 @@
+package com.silkimen.http;
+
+import android.content.Context;
+import android.security.KeyChain;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509ExtendedKeyManager;
+
+public class KeyChainKeyManager extends X509ExtendedKeyManager {
+ private final String alias;
+ private final X509Certificate[] chain;
+ private final PrivateKey key;
+
+ public KeyChainKeyManager(String alias, PrivateKey key, X509Certificate[] chain) {
+ this.alias = alias;
+ this.key = key;
+ this.chain = chain;
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+ return this.alias;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ return chain;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ return key;
+ }
+
+ @Override
+ public final String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getClientAliases(String keyType, Principal[] issuers) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getServerAliases(String keyType, Principal[] issuers) {
+ // not a client SSLSocket callback
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSConfiguration.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSConfiguration.java
new file mode 100644
index 00000000..c33df6c1
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSConfiguration.java
@@ -0,0 +1,63 @@
+package com.silkimen.http;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+import com.silkimen.http.TLSSocketFactory;
+
+public class TLSConfiguration {
+ private TrustManager[] trustManagers;
+ private KeyManager[] keyManagers;
+ private HostnameVerifier hostnameVerifier;
+
+ private SSLSocketFactory socketFactory;
+
+ public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public void setKeyManagers(KeyManager[] keyManagers) {
+ this.keyManagers = keyManagers;
+ this.socketFactory = null;
+ }
+
+ public void setTrustManagers(TrustManager[] trustManagers) {
+ this.trustManagers = trustManagers;
+ this.socketFactory = null;
+ }
+
+ public HostnameVerifier getHostnameVerifier() {
+ return this.hostnameVerifier;
+ }
+
+ public SSLSocketFactory getTLSSocketFactory() throws IOException {
+ if (this.socketFactory != null) {
+ return this.socketFactory;
+ }
+
+ try {
+ 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();
+ }
+
+ return this.socketFactory;
+ } catch (GeneralSecurityException e) {
+ IOException ioException = new IOException("Security exception occured while configuring TLS context");
+ ioException.initCause(e);
+ throw ioException;
+ }
+ }
+}
diff --git a/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSSocketFactory.java b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSSocketFactory.java
new file mode 100644
index 00000000..9bc75b12
--- /dev/null
+++ b/StoneIsland/plugins/cordova-plugin-advanced-http/src/android/com/silkimen/http/TLSSocketFactory.java
@@ -0,0 +1,63 @@
+package com.silkimen.http;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+public class TLSSocketFactory extends SSLSocketFactory {
+
+ private SSLSocketFactory delegate;
+
+ public TLSSocketFactory(SSLContext context) {
+ delegate = context.getSocketFactory();
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return delegate.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return delegate.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
+ return enableTLSOnSocket(delegate.createSocket(socket, host, port, autoClose));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+ return enableTLSOnSocket(delegate.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException, UnknownHostException {
+ return enableTLSOnSocket(delegate.createSocket(host, port, localHost, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return enableTLSOnSocket(delegate.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
+ throws IOException {
+ return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort));
+ }
+
+ private Socket enableTLSOnSocket(Socket socket) {
+ if (socket != null && (socket instanceof SSLSocket)) {
+ ((SSLSocket) socket).setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" });
+ }
+ return socket;
+ }
+}