package org.gingercal; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URI; import java.security.GeneralSecurityException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.conn.scheme.LayeredSocketFactory; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; /** * Android 2.3 transport. HttpURLConnection refuses PROPFIND/REPORT there, so this uses the * bundled Apache HttpClient, plus a TLS layer that supports certificate pinning (needed for a * self-signed legacy-TLS bridge) and SNI where the platform offers it. */ public class ApacheHttp implements Http { final DefaultHttpClient client; final Tls.PinTrust trust; public ApacheHttp(String pin, int timeoutMs) throws GeneralSecurityException { trust = new Tls.PinTrust(pin); HttpParams p = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(p, timeoutMs); HttpConnectionParams.setSoTimeout(p, timeoutMs); HttpConnectionParams.setStaleCheckingEnabled(p, true); HttpProtocolParams.setUseExpectContinue(p, false); HttpProtocolParams.setUserAgent(p, "GingerCal/1.0 (Android 2.3)"); p.setBooleanParameter("http.protocol.handle-redirects", false); SchemeRegistry reg = new SchemeRegistry(); reg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); reg.register(new Scheme("https", new TlsFactory(trust), 443)); client = new DefaultHttpClient(new ThreadSafeClientConnManager(p, reg), p); } static class Req extends HttpEntityEnclosingRequestBase { final String m; Req(String m, URI u) { this.m = m; setURI(u); } @Override public String getMethod() { return m; } } public Resp exec(String method, String url, String[] headers, String body, String contentType) throws IOException { Req r = new Req(method, URI.create(url)); for (int i = 0; i + 1 < headers.length; i += 2) r.addHeader(headers[i], headers[i + 1]); if (body != null) { StringEntity e = new StringEntity(body, "UTF-8"); e.setContentType(contentType); r.setEntity(e); } trust.reset(); HttpResponse res; try { res = client.execute(r); } catch (IOException e) { throw trust.translate(e); } Resp out = new Resp(); out.code = res.getStatusLine().getStatusCode(); Header h = res.getFirstHeader("ETag"); if (h != null) out.etag = h.getValue(); h = res.getFirstHeader("Location"); if (h != null) out.location = h.getValue(); HttpEntity en = res.getEntity(); if (en != null) { InputStream in = en.getContent(); try { ByteArrayOutputStream bo = new ByteArrayOutputStream(); byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) > 0) bo.write(buf, 0, n); out.body = new String(bo.toByteArray(), "UTF-8"); } finally { en.consumeContent(); } } return out; } public void close() { client.getConnectionManager().shutdown(); } // ------------------------------------------------------------------ TLS static class TlsFactory implements LayeredSocketFactory { final SSLSocketFactory f; final Tls.PinTrust trust; TlsFactory(Tls.PinTrust trust) throws GeneralSecurityException { this.trust = trust; SSLContext c = SSLContext.getInstance("TLS"); c.init(null, new TrustManager[] {trust}, null); f = c.getSocketFactory(); } public Socket createSocket() throws IOException { return f.createSocket(); } public Socket connectSocket(Socket sock, String host, int port, InetAddress local, int localPort, HttpParams params) throws IOException { SSLSocket s = (SSLSocket) (sock != null ? sock : createSocket()); if (local != null || localPort > 0) { s.bind(new InetSocketAddress(local, localPort < 0 ? 0 : localPort)); } sni(s, host); s.connect(new InetSocketAddress(host, port), HttpConnectionParams.getConnectionTimeout(params)); s.setSoTimeout(HttpConnectionParams.getSoTimeout(params)); s.startHandshake(); verifyHost(s, host); return s; } public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { SSLSocket s = (SSLSocket) f.createSocket(socket, host, port, autoClose); sni(s, host); s.startHandshake(); verifyHost(s, host); return s; } public boolean isSecure(Socket sock) { return true; } void verifyHost(SSLSocket s, String host) throws IOException { if (trust.pinned()) return; // the pin identifies the exact certificate if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(host, s.getSession())) { s.close(); throw new SSLException("Certificate is not valid for " + host); } } /** Gingerbread's OpenSSLSocketImpl has a hidden setHostname() for SNI. */ static void sni(SSLSocket s, String host) { if (host.matches("[0-9.]+") || host.indexOf(':') >= 0) return; try { Method m = s.getClass().getMethod("setHostname", String.class); m.invoke(s, host); } catch (Throwable ignored) { } } } }