/****************************************************************************
 * Copyright (C) 2013-2017 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.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.openecard.bouncycastle.tls.TlsClientProtocol;
import org.openecard.bouncycastle.tls.crypto.TlsCrypto;
import org.openecard.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto;
import org.openecard.bouncycastle.util.encoders.Base64;
import org.openecard.crypto.common.ReusableSecureRandom;
import org.openecard.crypto.tls.CertificateVerifier;
import org.openecard.crypto.tls.ClientCertDefaultTlsClient;
import org.openecard.crypto.tls.SocketWrapper;
import org.openecard.crypto.tls.verify.CertificateVerifierBuilder;
import org.openecard.crypto.tls.auth.DynamicAuthentication;
import org.openecard.crypto.tls.verify.HostnameVerifier;
import org.openecard.crypto.tls.verify.KeyLengthVerifier;
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
 */
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 proxyValidate Flag indicating whether to perform a certificate validation of the proxy server connection.
     * @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)) {
	    TlsCrypto crypto = new BcTlsCrypto(ReusableSecureRandom.getInstance());
	    ClientCertDefaultTlsClient tlsClient = new ClientCertDefaultTlsClient(crypto, proxyHost, true);
	    DynamicAuthentication tlsAuth = new DynamicAuthentication(proxyHost);
	    if (proxyValidate) {
		CertificateVerifier cv = new CertificateVerifierBuilder()
			.and(new HostnameVerifier())
			.and(new KeyLengthVerifier())
			.and(new JavaSecVerifier())
			.build();
		tlsAuth.setCertificateVerifier(cv);
	    }
	    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
    }

}
