/* * Created by Angel Leon (@gubatron), Alden Torres (aldenml) * Copyright (c) 2011-2014,, FrostWire(R). All rights reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.frostwire.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A pure java based HTTP client with resume capabilities. * @author gubatron * @author aldenml * */ final class FWHttpClient implements HttpClient { private static final Logger LOG = LoggerFactory.getLogger(FWHttpClient.class); private static final int DEFAULT_TIMEOUT = 10000; private static final String DEFAULT_USER_AGENT = UserAgentGenerator.getUserAgent(); private HttpClientListener listener; private boolean canceled; public String get(String url) throws IOException { return get(url, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT); } public String get(String url, int timeout) throws IOException { return get(url, timeout, DEFAULT_USER_AGENT); } public String get(String url, int timeout, String userAgent) throws IOException { return get(url, timeout, userAgent, null, null); } public String get(String url, int timeout, String userAgent, String referrer, String cookie) throws IOException { return get(url, timeout, userAgent, referrer, cookie, null); } @Override public String get(String url, int timeout, String userAgent, String referrer, String cookie, Map<String, String> customHeaders) throws IOException { String result = null; ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); get(url, baos, timeout, userAgent, referrer, cookie, -1, -1, customHeaders); result = new String(baos.toByteArray(), "UTF-8"); } catch (java.net.SocketTimeoutException timeoutException) { throw timeoutException; } catch (IOException e) { LOG.error("Error getting string from http body response: " + e.getMessage(), e); throw e; } finally { closeQuietly(baos); } return result; } public byte[] getBytes(String url, int timeout, String userAgent, String referrer) { byte[] result = null; ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); get(url, baos, timeout, userAgent, referrer, null, -1); result = baos.toByteArray(); } catch (Throwable e) { LOG.error("Error getting string from http body response: " + e.getMessage(), e); } finally { closeQuietly(baos); } return result; } public byte[] getBytes(String url) { return getBytes(url, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, null); } public void save(String url, File file, boolean resume) throws IOException { save(url, file, resume, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT); } public void save(String url, File file, boolean resume, int timeout, String userAgent) throws IOException { save(url, file, resume, timeout, userAgent, null); } public void save(String url, File file, boolean resume, int timeout, String userAgent, String referrer) throws IOException { FileOutputStream fos = null; int rangeStart = 0; try { if (resume && file.exists()) { fos = new FileOutputStream(file, true); rangeStart = (int) file.length(); } else { fos = new FileOutputStream(file, false); rangeStart = -1; } get(url, fos, timeout, userAgent, null, referrer, rangeStart); } finally { closeQuietly(fos); } } @Override public void post(String url, int timeout, String userAgent, String content, boolean gzip) throws IOException { canceled = false; final URL u = new URL(url); final HttpURLConnection conn = (HttpURLConnection) u.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(timeout); conn.setReadTimeout(timeout); conn.setRequestProperty("User-Agent", userAgent); conn.setInstanceFollowRedirects(false); if (conn instanceof HttpsURLConnection) { setHostnameVerifier((HttpsURLConnection) conn); } byte[] data = content.getBytes("UTF-8"); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "text/plain"); conn.setRequestProperty("charset", "utf-8"); conn.setUseCaches(false); ByteArrayInputStream in = new ByteArrayInputStream(data); try { OutputStream out = null; if (gzip) { out = new GZIPOutputStream(conn.getOutputStream()); } else { out = conn.getOutputStream(); } byte[] b = new byte[4096]; int n = 0; while (!canceled && (n = in.read(b, 0, b.length)) != -1) { if (!canceled) { out.write(b, 0, n); out.flush(); onData(b, 0, n); } } closeQuietly(out); conn.connect(); int httpResponseCode = getResponseCode(conn); if (httpResponseCode != HttpURLConnection.HTTP_OK && httpResponseCode != HttpURLConnection.HTTP_PARTIAL) { throw new ResponseCodeNotSupportedException(httpResponseCode); } if (canceled) { onCancel(); } else { onComplete(); } } catch (Exception e) { onError(e); } finally { closeQuietly(in); closeQuietly(conn); } } @Override public String post(String url, int timeout, String userAgent, Map<String, String> formData) { String result = null; ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); post(url, baos, timeout, userAgent, formData); result = new String(baos.toByteArray(), "UTF-8"); } catch (Throwable e) { LOG.error("Error getting string from http body response: " + e.getMessage(), e); } finally { closeQuietly(baos); } return result; } private String buildRange(int rangeStart, int rangeLength) { String prefix = "bytes=" + rangeStart + "-"; return prefix + ((rangeLength > -1) ? (rangeStart + rangeLength) : ""); } private void get(String url, OutputStream out, int timeout, String userAgent, String referrer, String cookie, int rangeStart) throws IOException { get(url, out, timeout, userAgent, referrer, cookie, rangeStart, -1, null); } private void get(String url, OutputStream out, int timeout, String userAgent, String referrer, String cookie, int rangeStart, int rangeLength, final Map<String, String> customHeaders) throws IOException { canceled = false; final URL u = new URL(url); final URLConnection conn = u.openConnection(); conn.setConnectTimeout(timeout); conn.setReadTimeout(timeout); conn.setRequestProperty("User-Agent", userAgent); if (referrer != null) { conn.setRequestProperty("Referer", referrer); } if (cookie != null) { conn.setRequestProperty("Cookie", cookie); } if (conn instanceof HttpURLConnection) { ((HttpURLConnection) conn).setInstanceFollowRedirects(true); } if (conn instanceof HttpsURLConnection) { setHostnameVerifier((HttpsURLConnection) conn); } if (rangeStart > 0) { conn.setRequestProperty("Range", buildRange(rangeStart, rangeLength)); } if (customHeaders != null && customHeaders.size() > 0) { //put down here so it can overwrite any of the previous headers. setCustomHeaders(conn, customHeaders); } InputStream in = conn.getInputStream(); if ("gzip".equals(conn.getContentEncoding())) { in = new GZIPInputStream(in); } int httpResponseCode = getResponseCode(conn); if (httpResponseCode != HttpURLConnection.HTTP_OK && httpResponseCode != HttpURLConnection.HTTP_PARTIAL) { throw new ResponseCodeNotSupportedException(httpResponseCode); } onHeaders(conn.getHeaderFields()); checkRangeSupport(rangeStart, conn); try { byte[] b = new byte[4096]; int n = 0; while (!canceled && (n = in.read(b, 0, b.length)) != -1) { if (!canceled) { out.write(b, 0, n); onData(b, 0, n); } } closeQuietly(out); if (canceled) { onCancel(); } else { onComplete(); } } catch (Exception e) { onError(e); } finally { closeQuietly(in); closeQuietly(conn); } } private void post(String url, OutputStream out, int timeout, String userAgent, Map<String, String> formData) throws IOException { canceled = false; final URL u = new URL(url); final HttpURLConnection conn = (HttpURLConnection) u.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(timeout); conn.setReadTimeout(timeout); conn.setRequestProperty("User-Agent", userAgent); conn.setInstanceFollowRedirects(false); if (conn instanceof HttpsURLConnection) { setHostnameVerifier((HttpsURLConnection) conn); } StringBuilder sb = new StringBuilder(); if (formData != null && formData.size() > 0) { for (Entry<String, String> kv : formData.entrySet()) { sb.append("&"); sb.append(kv.getKey()); sb.append("="); sb.append(kv.getValue()); } sb.deleteCharAt(0); } byte[] data = sb.toString().getBytes("UTF-8"); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("charset", "utf-8"); conn.setUseCaches(false); InputStream in = new ByteArrayInputStream(data); try { OutputStream postOut = conn.getOutputStream(); byte[] b = new byte[4096]; int n = 0; while (!canceled && (n = in.read(b, 0, b.length)) != -1) { if (!canceled) { postOut.write(b, 0, n); postOut.flush(); onData(b, 0, n); } } closeQuietly(postOut); closeQuietly(in); conn.connect(); in = conn.getInputStream(); int httpResponseCode = getResponseCode(conn); if (httpResponseCode != HttpURLConnection.HTTP_OK && httpResponseCode != HttpURLConnection.HTTP_PARTIAL) { throw new ResponseCodeNotSupportedException(httpResponseCode); } b = new byte[4096]; n = 0; while (!canceled && (n = in.read(b, 0, b.length)) != -1) { if (!canceled) { out.write(b, 0, n); onData(b, 0, n); } } closeQuietly(out); if (canceled) { onCancel(); } else { onComplete(); } } catch (Exception e) { onError(e); } finally { closeQuietly(in); closeQuietly(conn); } } private void setCustomHeaders(URLConnection conn, Map<String, String> headers) { for (Entry<String, String> e : headers.entrySet()) { conn.setRequestProperty(e.getKey(), e.getValue()); } } private void setHostnameVerifier(HttpsURLConnection conn) { conn.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); } private int getResponseCode(URLConnection conn) { try { return ((HttpURLConnection) conn).getResponseCode(); } catch (IOException e) { e.printStackTrace(); LOG.error("can't get response code ", e); return -1; } } private void checkRangeSupport(int rangeStart, URLConnection conn) throws HttpRangeOutOfBoundsException, RangeNotSupportedException { boolean hasContentRange = conn.getHeaderField("Content-Range") != null; boolean hasAcceptRanges = conn.getHeaderField("Accept-Ranges") != null && conn.getHeaderField("Accept-Ranges").equals("bytes"); if (rangeStart > 0 && !hasContentRange && !hasAcceptRanges) { RangeNotSupportedException rangeNotSupportedException = new RangeNotSupportedException("Server does not support bytes range request"); onError(rangeNotSupportedException); throw rangeNotSupportedException; } } private void onHeaders(Map<String, List<String>> headerFields) { if (getListener() != null) { try { getListener().onHeaders(this, headerFields); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } } private void onCancel() { if (getListener() != null) { try { getListener().onCancel(this); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } } private void onData(byte[] b, int i, int n) { if (getListener() != null) { try { getListener().onData(this, b, 0, n); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } } protected void onError(Exception e) { if (getListener() != null) { try { getListener().onError(this, e); } catch (Exception e2) { LOG.warn(e2.getMessage(), e2); } } } protected void onComplete() { if (getListener() != null) { try { getListener().onComplete(this); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } } private static void closeQuietly(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException ioe) { // ignore } } private void closeQuietly(URLConnection conn) { if (conn instanceof HttpURLConnection) { try { ((HttpURLConnection) conn).disconnect(); } catch (Throwable e) { LOG.debug("Error closing http connection", e); } } } @Override public void setListener(HttpClientListener listener) { this.listener = listener; } @Override public HttpClientListener getListener() { return listener; } @Override public void cancel() { canceled = true; } @Override public boolean isCanceled() { return canceled; } }