/**************************************************************************** * Copyright (C) 2013 ecsec GmbH. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.crypto.tls.proxy; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.security.GeneralSecurityException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.openecard.bouncycastle.crypto.tls.TlsClientProtocol; import org.openecard.bouncycastle.util.encoders.Base64; import org.openecard.crypto.tls.ClientCertDefaultTlsClient; import org.openecard.crypto.tls.SocketWrapper; import org.openecard.crypto.tls.auth.DynamicAuthentication; import org.openecard.crypto.tls.verify.JavaSecVerifier; /** * HTTP proxy supporting only CONNECT tunnel. * This class is initialised with the proxy parameters and can then establish a tunnel with the getSocket method. * The authentication parameters are optional. Scheme must be one of http and https. * * @author Tobias Wich <tobias.wich@ecsec.de> */ public final class HttpConnectProxy extends Proxy { private final String proxyScheme; private final boolean proxyValidate; private final String proxyHost; private final int proxyPort; private final String proxyUser; private final String proxyPass; /** * Create a HTTP proxy instance configured with the given values. * This method does not perform any reachability checks, it only saves the values for later use. * * @param proxyScheme HTTP or HTTPS * @param proxyHost Hostname of the proxy * @param proxyPort Port of the proxy. * @param proxyUser Optional username for authentication against the proxy. * @param proxyPass Optional pass for authentication against the proxy. */ public HttpConnectProxy(@Nonnull String proxyScheme, boolean proxyValidate, @Nonnull String proxyHost, @Nonnegative int proxyPort, @Nullable String proxyUser, @Nullable String proxyPass) { super(Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); this.proxyScheme = proxyScheme; this.proxyValidate = proxyValidate; this.proxyHost = proxyHost; this.proxyPort = proxyPort; this.proxyUser = proxyUser; this.proxyPass = proxyPass; } /** * Gets a freshly allocated socket representing a tunnel to the given host through the configured proxy. * * @param host Hostname of the target. IP addresses are allowed. * @param port Port number of the target. * @return Connected socket to the target. * @throws IOException Thrown in case the proxy or the target are not functioning correctly. */ @Nonnull public Socket getSocket(@Nonnull String host, @Nonnegative int port) throws IOException { Socket sock = connectSocket(); String requestStr = makeRequestStr(host, port); // deliver request sock.getOutputStream().write(requestStr.getBytes()); // get HTTP response and validate it InputStream in = sock.getInputStream(); String proxyResponse = getResponse(in); validateResponse(proxyResponse); // validation passed, slurp in the rest of the data and return if (in.available() > 0) { in.skip(in.available()); } return sock; } private Socket connectSocket() throws IOException { // Socket object connecting to proxy Socket sock = new Socket(); SocketAddress addr = new InetSocketAddress(proxyHost, proxyPort); sock.setKeepAlive(true); // this is pretty much, but not a problem, as this only shifts the responsibility to the server sock.setSoTimeout(5 * 60 * 1000); sock.connect(addr, 60 * 1000); // evaluate scheme if ("HTTPS".equals(proxyScheme)) { ClientCertDefaultTlsClient tlsClient = new ClientCertDefaultTlsClient(proxyHost); DynamicAuthentication tlsAuth = new DynamicAuthentication(); tlsAuth.setHostname(proxyHost); if (proxyValidate) { try { tlsAuth.setCertificateVerifier(new JavaSecVerifier()); } catch (GeneralSecurityException ex) { throw new IOException("Failed to load certificate verifier.", ex); } } tlsClient.setAuthentication(tlsAuth); TlsClientProtocol proto = new TlsClientProtocol(sock.getInputStream(), sock.getOutputStream()); proto.connect(tlsClient); // wrap socket Socket tlsSock = new SocketWrapper(sock, proto.getInputStream(), proto.getOutputStream()); return tlsSock; } else { return sock; } } private String makeRequestStr(String host, int port) { // HTTP CONNECT protocol RFC 2616 StringBuilder requestStr = new StringBuilder(1024); requestStr.append("CONNECT ").append(host).append(":").append(port). append(" HTTP/1.0\r\n"); // Add Proxy Authorization if proxyUser and proxyPass is set if (proxyUser != null && ! proxyUser.isEmpty() && proxyPass != null && ! proxyPass.isEmpty()) { String proxyUserPass = String.format("%s:%s", proxyUser, proxyPass); proxyUserPass = Base64.toBase64String(proxyUserPass.getBytes()); requestStr.append("Proxy-Authorization: Basic ").append(proxyUserPass).append("\r\n"); } // finalize request requestStr.append("\r\n"); return requestStr.toString(); } private String getResponse(InputStream in) throws IOException { byte[] tmpBuffer = new byte[512]; int len = in.read(tmpBuffer, 0, tmpBuffer.length); if (len == 0) { throw new SocketException("Invalid response from proxy."); } String proxyResponse = new String(tmpBuffer, 0, len, "UTF-8"); return proxyResponse; } private void validateResponse(String response) throws IOException { // desctruct response status line // upgrade to HTTP/1.1 is ok, as nobody evaluates that stuff further // the last . catches everything including newlines, which is important because the match fails then Pattern p = Pattern.compile("HTTP/1\\.(1|0) (\\d{3}) (.*)\\r\\n(?s).*"); Matcher m = p.matcher(response); if (m.matches()) { String code = m.group(2); String desc = m.group(3); int codeNum = Integer.parseInt(code); // expecting 200 in order to continue if (codeNum != 200) { throw new HttpConnectProxyException("Failed to create proxy socket.", codeNum, desc); } } else { throw new HttpConnectProxyException("Invalid HTTP response from proxy.", 500, "Response malformed."); } // if we passed the exceptions the response is fine } }