diff options
| author | PliablePixels <pliablepixels@gmail.com> | 2015-06-24 18:47:42 -0400 |
|---|---|---|
| committer | PliablePixels <pliablepixels@gmail.com> | 2015-06-24 18:47:42 -0400 |
| commit | 855a0e8ddc273b58066530a1b55a946021dfc56e (patch) | |
| tree | 26550033e855a31a265fc2da4da3df0cc2733dc1 /plugins/com.synconset.cordovaHTTP/src/android | |
| parent | d442629aa825aab6bc55ab6be19e3aba060867fe (diff) | |
Cleaned up code, commented, preparing for HTTPS via CordovaHTTP
Diffstat (limited to 'plugins/com.synconset.cordovaHTTP/src/android')
7 files changed, 3882 insertions, 0 deletions
diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttp.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttp.java new file mode 100644 index 00000000..afb85a06 --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttp.java @@ -0,0 +1,106 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import org.apache.cordova.CallbackContext; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.HostnameVerifier; + +import java.util.Iterator; + +import android.util.Log; + +import com.github.kevinsawicki.http.HttpRequest; + +public abstract class CordovaHttp { + protected static final String TAG = "CordovaHTTP"; + protected static final String CHARSET = "UTF-8"; + + private static AtomicBoolean sslPinning = new AtomicBoolean(false); + private static AtomicBoolean acceptAllCerts = new AtomicBoolean(false); + + private String urlString; + private Map<?, ?> params; + private Map<String, String> headers; + private CallbackContext callbackContext; + + public CordovaHttp(String urlString, Map<?, ?> params, Map<String, String> headers, CallbackContext callbackContext) { + this.urlString = urlString; + this.params = params; + this.headers = headers; + this.callbackContext = callbackContext; + } + + public static void enableSSLPinning(boolean enable) { + sslPinning.set(enable); + if (enable) { + acceptAllCerts.set(false); + } + } + + public static void acceptAllCerts(boolean accept) { + acceptAllCerts.set(accept); + if (accept) { + sslPinning.set(false); + } + } + + protected String getUrlString() { + return this.urlString; + } + + protected Map<?, ?> getParams() { + return this.params; + } + + protected Map<String, String> getHeaders() { + return this.headers; + } + + protected CallbackContext getCallbackContext() { + return this.callbackContext; + } + + protected HttpRequest setupSecurity(HttpRequest request) { + if (acceptAllCerts.get()) { + request.trustAllCerts(); + request.trustAllHosts(); + } + if (sslPinning.get()) { + request.pinToCerts(); + } + return request; + } + + protected void respondWithError(int status, String msg) { + try { + JSONObject response = new JSONObject(); + response.put("status", status); + response.put("error", msg); + this.callbackContext.error(response); + } catch (JSONException e) { + this.callbackContext.error(msg); + } + } + + protected void respondWithError(String msg) { + this.respondWithError(500, msg); + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpDownload.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpDownload.java new file mode 100644 index 00000000..a1b78963 --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpDownload.java @@ -0,0 +1,69 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import android.util.Log; + +import com.github.kevinsawicki.http.HttpRequest; +import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; + +import java.io.File; +import java.net.UnknownHostException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import javax.net.ssl.SSLHandshakeException; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.file.FileUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +public class CordovaHttpDownload extends CordovaHttp implements Runnable { + private String filePath; + + public CordovaHttpDownload(String urlString, Map<?, ?> params, Map<String, String> headers, CallbackContext callbackContext, String filePath) { + super(urlString, params, headers, callbackContext); + this.filePath = filePath; + } + + @Override + public void run() { + try { + HttpRequest request = HttpRequest.get(this.getUrlString(), this.getParams(), true); + this.setupSecurity(request); + request.acceptCharset(CHARSET); + request.headers(this.getHeaders()); + int code = request.code(); + + JSONObject response = new JSONObject(); + response.put("status", code); + if (code >= 200 && code < 300) { + URI uri = new URI(filePath); + File file = new File(uri); + request.receive(file); + JSONObject fileEntry = FileUtils.getEntry(file); + response.put("file", fileEntry); + this.getCallbackContext().success(response); + } else { + response.put("error", "There was an error downloading the file"); + this.getCallbackContext().error(response); + } + } catch(URISyntaxException e) { + this.respondWithError("There was an error with the given filePath"); + } catch (JSONException e) { + this.respondWithError("There was an error generating the response"); + } catch (HttpRequestException e) { + if (e.getCause() instanceof UnknownHostException) { + this.respondWithError(0, "The host could not be resolved"); + } else if (e.getCause() instanceof SSLHandshakeException) { + this.respondWithError("SSL handshake failed"); + } else { + this.respondWithError("There was an error with the request"); + } + } + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpGet.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpGet.java new file mode 100644 index 00000000..1b37bf8c --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpGet.java @@ -0,0 +1,63 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.HostnameVerifier; + +import javax.net.ssl.SSLHandshakeException; + +import org.apache.cordova.CallbackContext; +import org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import com.github.kevinsawicki.http.HttpRequest; +import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; + +public class CordovaHttpGet extends CordovaHttp implements Runnable { + public CordovaHttpGet(String urlString, Map<?, ?> params, Map<String, String> headers, CallbackContext callbackContext) { + super(urlString, params, headers, callbackContext); + } + + @Override + public void run() { + try { + HttpRequest request = HttpRequest.get(this.getUrlString(), this.getParams(), true); + this.setupSecurity(request); + request.acceptCharset(CHARSET); + request.headers(this.getHeaders()); + int code = request.code(); + String body = request.body(CHARSET); + JSONObject response = new JSONObject(); + response.put("status", code); + if (code >= 200 && code < 300) { + response.put("data", body); + this.getCallbackContext().success(response); + } else { + response.put("error", body); + this.getCallbackContext().error(response); + } + } catch (JSONException e) { + this.respondWithError("There was an error generating the response"); + } catch (HttpRequestException e) { + if (e.getCause() instanceof UnknownHostException) { + this.respondWithError(0, "The host could not be resolved"); + } else if (e.getCause() instanceof SSLHandshakeException) { + this.respondWithError("SSL handshake failed"); + } else { + this.respondWithError("There was an error with the request"); + } + } + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPlugin.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPlugin.java new file mode 100644 index 00000000..b2cf398f --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPlugin.java @@ -0,0 +1,184 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.HostnameVerifier; + +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.content.res.AssetManager; +import android.util.Base64; +import android.util.Log; + +import com.github.kevinsawicki.http.HttpRequest; + +public class CordovaHttpPlugin extends CordovaPlugin { + private static final String TAG = "CordovaHTTP"; + + private HashMap<String, String> globalHeaders; + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + this.globalHeaders = new HashMap<String, String>(); + } + + @Override + public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException { + if (action.equals("get")) { + String urlString = args.getString(0); + JSONObject params = args.getJSONObject(1); + JSONObject headers = args.getJSONObject(2); + HashMap<?, ?> paramsMap = this.getMapFromJSONObject(params); + HashMap<String, String> headersMap = this.addToMap(this.globalHeaders, headers); + CordovaHttpGet get = new CordovaHttpGet(urlString, paramsMap, headersMap, callbackContext); + cordova.getThreadPool().execute(get); + } else if (action.equals("post")) { + String urlString = args.getString(0); + JSONObject params = args.getJSONObject(1); + JSONObject headers = args.getJSONObject(2); + HashMap<?, ?> paramsMap = this.getMapFromJSONObject(params); + HashMap<String, String> headersMap = this.addToMap(this.globalHeaders, headers); + CordovaHttpPost post = new CordovaHttpPost(urlString, paramsMap, headersMap, callbackContext); + cordova.getThreadPool().execute(post); + } else if (action.equals("useBasicAuth")) { + String username = args.getString(0); + String password = args.getString(1); + this.useBasicAuth(username, password); + callbackContext.success(); + } else if (action.equals("enableSSLPinning")) { + try { + boolean enable = args.getBoolean(0); + this.enableSSLPinning(enable); + callbackContext.success(); + } catch(Exception e) { + e.printStackTrace(); + callbackContext.error("There was an error setting up ssl pinning"); + } + } else if (action.equals("acceptAllCerts")) { + boolean accept = args.getBoolean(0); + CordovaHttp.acceptAllCerts(accept); + } else if (action.equals("setHeader")) { + String header = args.getString(0); + String value = args.getString(1); + this.setHeader(header, value); + callbackContext.success(); + } else if (action.equals("uploadFile")) { + String urlString = args.getString(0); + JSONObject params = args.getJSONObject(1); + JSONObject headers = args.getJSONObject(2); + HashMap<?, ?> paramsMap = this.getMapFromJSONObject(params); + HashMap<String, String> headersMap = this.addToMap(this.globalHeaders, headers); + String filePath = args.getString(3); + String name = args.getString(4); + CordovaHttpUpload upload = new CordovaHttpUpload(urlString, paramsMap, headersMap, callbackContext, filePath, name); + cordova.getThreadPool().execute(upload); + } else if (action.equals("downloadFile")) { + String urlString = args.getString(0); + JSONObject params = args.getJSONObject(1); + JSONObject headers = args.getJSONObject(2); + HashMap<?, ?> paramsMap = this.getMapFromJSONObject(params); + HashMap<String, String> headersMap = this.addToMap(this.globalHeaders, headers); + String filePath = args.getString(3); + CordovaHttpDownload download = new CordovaHttpDownload(urlString, paramsMap, headersMap, callbackContext, filePath); + cordova.getThreadPool().execute(download); + } else { + return false; + } + return true; + } + + private void useBasicAuth(String username, String password) { + String loginInfo = username + ":" + password; + loginInfo = "Basic " + Base64.encodeToString(loginInfo.getBytes(), Base64.NO_WRAP); + this.globalHeaders.put("Authorization", loginInfo); + } + + private void setHeader(String header, String value) { + this.globalHeaders.put(header, value); + } + + private void enableSSLPinning(boolean enable) throws GeneralSecurityException, IOException { + if (enable) { + AssetManager assetManager = cordova.getActivity().getAssets(); + String[] files = assetManager.list(""); + int index; + ArrayList<String> cerFiles = new ArrayList<String>(); + for (int i = 0; i < files.length; i++) { + index = files[i].lastIndexOf('.'); + if (index != -1) { + if (files[i].substring(index).equals(".cer")) { + cerFiles.add(files[i]); + } + } + } + + // scan the www/certificates folder for .cer files as well + files = assetManager.list("www/certificates"); + for (int i = 0; i < files.length; i++) { + index = files[i].lastIndexOf('.'); + if (index != -1) { + if (files[i].substring(index).equals(".cer")) { + cerFiles.add("www/certificates/" + files[i]); + } + } + } + + for (int i = 0; i < cerFiles.size(); i++) { + InputStream in = cordova.getActivity().getAssets().open(cerFiles.get(i)); + InputStream caInput = new BufferedInputStream(in); + HttpRequest.addCert(caInput); + } + CordovaHttp.enableSSLPinning(true); + } else { + CordovaHttp.enableSSLPinning(false); + } + } + + private HashMap<String, String> addToMap(HashMap<String, String> map, JSONObject object) throws JSONException { + HashMap<String, String> newMap = (HashMap<String, String>)map.clone(); + Iterator<?> i = object.keys(); + + while (i.hasNext()) { + String key = (String)i.next(); + newMap.put(key, object.getString(key)); + } + return newMap; + } + + private HashMap<String, Object> getMapFromJSONObject(JSONObject object) throws JSONException { + HashMap<String, Object> map = new HashMap<String, Object>(); + Iterator<?> i = object.keys(); + + while(i.hasNext()) { + String key = (String)i.next(); + map.put(key, object.get(key)); + } + return map; + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPost.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPost.java new file mode 100644 index 00000000..c0ae17f3 --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpPost.java @@ -0,0 +1,56 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import java.net.UnknownHostException; +import java.util.Map; + +import org.apache.cordova.CallbackContext; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.net.ssl.SSLHandshakeException; + +import android.util.Log; + +import com.github.kevinsawicki.http.HttpRequest; +import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; + +public class CordovaHttpPost extends CordovaHttp implements Runnable { + public CordovaHttpPost(String urlString, Map<?, ?> params, Map<String, String> headers, CallbackContext callbackContext) { + super(urlString, params, headers, callbackContext); + } + + @Override + public void run() { + try { + HttpRequest request = HttpRequest.post(this.getUrlString()); + this.setupSecurity(request); + request.acceptCharset(CHARSET); + request.headers(this.getHeaders()); + request.form(this.getParams()); + int code = request.code(); + String body = request.body(CHARSET); + JSONObject response = new JSONObject(); + response.put("status", code); + if (code >= 200 && code < 300) { + response.put("data", body); + this.getCallbackContext().success(response); + } else { + response.put("error", body); + this.getCallbackContext().error(response); + } + } catch (JSONException e) { + this.respondWithError("There was an error generating the response"); + } catch (HttpRequestException e) { + if (e.getCause() instanceof UnknownHostException) { + this.respondWithError(0, "The host could not be resolved"); + } else if (e.getCause() instanceof SSLHandshakeException) { + this.respondWithError("SSL handshake failed"); + } else { + this.respondWithError("There was an error with the request"); + } + } + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpUpload.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpUpload.java new file mode 100644 index 00000000..622a52a5 --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/CordovaHttpUpload.java @@ -0,0 +1,95 @@ +/** + * A HTTP plugin for Cordova / Phonegap + */ +package com.synconset; + +import java.io.File; +import java.net.UnknownHostException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.cordova.CallbackContext; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.net.ssl.SSLHandshakeException; + +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.github.kevinsawicki.http.HttpRequest; +import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; + +public class CordovaHttpUpload extends CordovaHttp implements Runnable { + private String filePath; + private String name; + + public CordovaHttpUpload(String urlString, Map<?, ?> params, Map<String, String> headers, CallbackContext callbackContext, String filePath, String name) { + super(urlString, params, headers, callbackContext); + this.filePath = filePath; + this.name = name; + } + + @Override + public void run() { + try { + HttpRequest request = HttpRequest.post(this.getUrlString()); + this.setupSecurity(request); + request.acceptCharset(CHARSET); + request.headers(this.getHeaders()); + URI uri = new URI(filePath); + int index = filePath.lastIndexOf('/'); + String filename = filePath.substring(index + 1); + index = filePath.lastIndexOf('.'); + String ext = filePath.substring(index + 1); + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + String mimeType = mimeTypeMap.getMimeTypeFromExtension(ext); + request.part(this.name, filename, mimeType, new File(uri)); + + Set<?> set = (Set<?>)this.getParams().entrySet(); + Iterator<?> i = set.iterator(); + while (i.hasNext()) { + Entry<?, ?> e = (Entry<?, ?>)i.next(); + String key = (String)e.getKey(); + Object value = e.getValue(); + if (value instanceof Number) { + request.part(key, (Number)value); + } else if (value instanceof String) { + request.part(key, (String)value); + } else { + this.respondWithError("All parameters must be Numbers or Strings"); + return; + } + } + + int code = request.code(); + String body = request.body(CHARSET); + + JSONObject response = new JSONObject(); + response.put("status", code); + if (code >= 200 && code < 300) { + response.put("data", body); + this.getCallbackContext().success(response); + } else { + response.put("error", body); + this.getCallbackContext().error(response); + } + } catch (URISyntaxException e) { + this.respondWithError("There was an error loading the file"); + } catch (JSONException e) { + this.respondWithError("There was an error generating the response"); + } catch (HttpRequestException e) { + if (e.getCause() instanceof UnknownHostException) { + this.respondWithError(0, "The host could not be resolved"); + } else if (e.getCause() instanceof SSLHandshakeException) { + this.respondWithError("SSL handshake failed"); + } else { + this.respondWithError("There was an error with the request"); + } + } + } +} diff --git a/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/HttpRequest.java b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/HttpRequest.java new file mode 100644 index 00000000..f8dcf68c --- /dev/null +++ b/plugins/com.synconset.cordovaHTTP/src/android/com/synconset/CordovaHTTP/HttpRequest.java @@ -0,0 +1,3309 @@ +/* + * Copyright (c) 2014 Kevin Sawicki <kevinsawicki@gmail.com> + * + * 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.github.kevinsawicki.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.KeyStore; +import java.security.PrivilegedAction; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +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.TrustManagerFactory; +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 SSLSocketFactory PINNED_FACTORY; + + private static SSLSocketFactory TRUSTED_FACTORY; + + private static ArrayList<Certificate> PINNED_CERTS; + + private static HostnameVerifier TRUSTED_VERIFIER; + + private static String getValidCharset(final String charset) { + if (charset != null && charset.length() > 0) + return charset; + else + return CHARSET_UTF8; + } + + private static SSLSocketFactory getPinnedFactory() + throws HttpRequestException { + if (PINNED_FACTORY != null) { + return PINNED_FACTORY; + } else { + IOException e = new IOException("You must add at least 1 certificate in order to pin to certificates"); + throw new HttpRequestException(e); + } + } + + private static SSLSocketFactory getTrustedFactory() + throws HttpRequestException { + if (TRUSTED_FACTORY == null) { + final TrustManager[] trustAllCerts = 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 + } + } }; + try { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustAllCerts, new SecureRandom()); + TRUSTED_FACTORY = context.getSocketFactory(); + } catch (GeneralSecurityException e) { + IOException ioException = new IOException( + "Security exception configuring SSL context"); + ioException.initCause(e); + throw new HttpRequestException(ioException); + } + } + + return TRUSTED_FACTORY; + } + + private static HostnameVerifier getTrustedVerifier() { + if (TRUSTED_VERIFIER == null) + TRUSTED_VERIFIER = new HostnameVerifier() { + + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + + return TRUSTED_VERIFIER; + } + + 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; + } + + /** + * 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; + } + + + /** + * Add a certificate to test against when using ssl pinning. + * + * @param ca + * The Certificate to add + * @throws GeneralSecurityException + * @throws IOException + */ + public static void addCert(Certificate ca) throws GeneralSecurityException, IOException { + if (PINNED_CERTS == null) { + PINNED_CERTS = new ArrayList<Certificate>(); + } + PINNED_CERTS.add(ca); + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + + for (int i = 0; i < PINNED_CERTS.size(); i++) { + keyStore.setCertificateEntry("CA" + i, PINNED_CERTS.get(i)); + } + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + // Create an SSLContext that uses our TrustManager + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + PINNED_FACTORY = sslContext.getSocketFactory(); + } + + /** + * Add a certificate to test against when using ssl pinning. + * + * @param in + * An InputStream to read a certificate from + * @throws GeneralSecurityException + * @throws IOException + */ + public static void addCert(InputStream in) throws GeneralSecurityException, IOException { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate ca; + try { + ca = cf.generateCertificate(in); + addCert(ca); + } finally { + in.close(); + } + } + + /** + * 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; + } + } + + /** + * 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; + Object value; + Iterator<?> iterator = params.entrySet().iterator(); + entry = (Entry<?, ?>) iterator.next(); + result.append(entry.getKey().toString()); + result.append('='); + value = entry.getValue(); + if (value != null) + result.append(value); + + while (iterator.hasNext()) { + result.append('&'); + entry = (Entry<?, ?>) iterator.next(); + result.append(entry.getKey().toString()); + result.append('='); + value = entry.getValue(); + if (value != null) + result.append(value); + } + + 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); + + Object value; + result.append(params[0]); + result.append('='); + value = params[1]; + if (value != null) + result.append(value); + + for (int i = 2; i < params.length; i += 2) { + result.append('&'); + result.append(params[i]); + result.append('='); + value = params[i + 1]; + if (value != null) + result.append(value); + } + + 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, String...) + * @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, String...) + * @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, String...) + * @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, String...) + * @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, String...) + * @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' & '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' & '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; + } + + /** + * Configure HTTPS connection to trust only certain certificates + * <p> + * This method throws an exception if the current request is not a HTTPS request + * + * @return this request + * @throws HttpRequestException + */ + public HttpRequest pinToCerts() throws HttpRequestException { + final HttpURLConnection connection = getConnection(); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(getPinnedFactory()); + } else { + IOException e = new IOException("You must use a https url to use ssl pinning"); + throw new HttpRequestException(e); + } + return this; + } + + /** + * Configure HTTPS connection to trust all certificates + * <p> + * This method does nothing if the current request is not a HTTPS request + * + * @return this request + * @throws HttpRequestException + */ + public HttpRequest trustAllCerts() throws HttpRequestException { + final HttpURLConnection connection = getConnection(); + if (connection instanceof HttpsURLConnection) + ((HttpsURLConnection) connection) + .setSSLSocketFactory(getTrustedFactory()); + return this; + } + + /** + * Configure HTTPS connection to trust all hosts using a custom + * {@link HostnameVerifier} that always returns <code>true</code> for each + * host verified + * <p> + * This method does nothing if the current request is not a HTTPS request + * + * @return this request + */ + public HttpRequest trustAllHosts() { + final HttpURLConnection connection = getConnection(); + if (connection instanceof HttpsURLConnection) + ((HttpsURLConnection) connection) + .setHostnameVerifier(getTrustedVerifier()); + 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; + } +} |
