Java tutorial
/* * Kontalk Android client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.client; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.apache.http.conn.ssl.AllowAllHostnameVerifier; import android.content.Context; import android.util.Log; import info.guardianproject.netcipher.client.TlsOnlySocketFactory; import org.kontalk.message.CompositeMessage; import org.kontalk.service.DownloadListener; import org.kontalk.util.InternalTrustStore; import org.kontalk.util.MediaStorage; import org.kontalk.util.Preferences; import org.kontalk.util.ProgressOutputStreamEntity; /** * FIXME this is actually specific to Kontalk Dropbox server. * @author Daniele Ricci */ public class ClientHTTPConnection { private static final String TAG = ClientHTTPConnection.class.getSimpleName(); /** Regex used to parse content-disposition headers */ private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); private final Context mContext; private final PrivateKey mPrivateKey; private final X509Certificate mCertificate; private HttpsURLConnection currentRequest; private final static int CONNECT_TIMEOUT = 15000; private final static int READ_TIMEOUT = 40000; public ClientHTTPConnection(Context context) { this(context, null, null); } public ClientHTTPConnection(Context context, PrivateKey privateKey, X509Certificate bridgeCert) { mContext = context; mPrivateKey = privateKey; mCertificate = bridgeCert; } public void abort() { try { currentRequest.disconnect(); } catch (Exception ignored) { } } /** * A generic download request. * @param url URL to download * @return the request object */ private HttpsURLConnection prepareURLDownload(String url, boolean acceptAnyCertificate) throws IOException { HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection(); try { setupClient(conn, acceptAnyCertificate); } catch (Exception e) { throw innerException("error setting up SSL connection", e); } return conn; } private IOException innerException(String detail, Throwable cause) { IOException ie = new IOException(detail); ie.initCause(cause); return ie; } private void setupClient(HttpsURLConnection conn, boolean acceptAnyCertificate) throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException, IOException { // bug caused by Lighttpd //conn.setRequestProperty("Expect", "100-continue"); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setDoInput(true); conn.setSSLSocketFactory(setupSSLSocketFactory(mContext, mPrivateKey, mCertificate, acceptAnyCertificate)); if (acceptAnyCertificate) conn.setHostnameVerifier(new AllowAllHostnameVerifier()); } public static SSLSocketFactory setupSSLSocketFactory(Context context, PrivateKey privateKey, X509Certificate certificate, boolean acceptAnyCertificate) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, UnrecoverableKeyException, NoSuchProviderException { // in-memory keystore KeyManager[] km = null; if (privateKey != null && certificate != null) { KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(null, null); keystore.setKeyEntry("private", privateKey, null, new Certificate[] { certificate }); // key managers KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmFactory.init(keystore, null); km = kmFactory.getKeyManagers(); } // trust managers TrustManager[] tm; if (acceptAnyCertificate) { tm = new TrustManager[] { new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } } }; } else { // load merged truststore (system + internal) KeyStore trustStore = InternalTrustStore.getTrustStore(context); // builtin keystore TrustManagerFactory tmFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmFactory.init(trustStore); tm = tmFactory.getTrustManagers(); } SSLContext ctx = SSLContext.getInstance("TLSv1"); ctx.init(km, tm, null); return new TlsOnlySocketFactory(ctx.getSocketFactory(), true); } /** * Downloads to a directory represented by a {@link File} object, * determining the file name from the Content-Disposition header. */ public void downloadAutofilename(String url, File defaultBase, Date timestamp, DownloadListener listener) throws IOException { _download(url, defaultBase, timestamp, listener); } private void _download(String url, File defaultBase, Date timestamp, DownloadListener listener) throws IOException { boolean acceptAnyCertificate = Preferences.getAcceptAnyCertificate(mContext); currentRequest = prepareURLDownload(url, acceptAnyCertificate); int code = currentRequest.getResponseCode(); // HTTP/1.1 200 OK -- other codes should throw Exceptions if (code == 200) { // use a more suitable filename, taking only the extension String contentType = currentRequest.getContentType(); File destination = null; if (contentType != null) { destination = CompositeMessage.getIncomingFile(contentType, timestamp != null ? timestamp : new Date()); } // still having problems? if (destination == null) { String name = null; String disp = currentRequest.getHeaderField("Content-Disposition"); if (disp != null) name = parseContentDisposition(disp); if (name == null) { // very bad hack to overcome server bad behaviour name = MediaStorage.UNKNOWN_FILENAME; } destination = new File(defaultBase, name); } // we need to wrap the entity to monitor the download progress ProgressOutputStreamEntity entity = new ProgressOutputStreamEntity(currentRequest, url, destination, listener); FileOutputStream out = new FileOutputStream(destination); entity.writeTo(out); out.close(); return; } Log.d(TAG, "invalid response: " + code); listener.error(url, null, new IOException("invalid response: " + code)); } /** * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be * downloaded to the file system. We only support the attachment type. */ private static String parseContentDisposition(String contentDisposition) { try { Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); if (m.find()) { return m.group(1); } } catch (IllegalStateException ex) { // This function is defined as returning null when it can't parse the header } return null; } }