// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.http;

import jodd.io.StreamUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Simple HTTP tunnel base ready to be extended.
 */
public class HttpTunnel {

	/**
	 * The number of threads that can be executed in parallel.
	 */
	protected int threadPoolSize = 10;

	/**
	 * Number of incoming sockets connection that can be hold
	 * before processing each.
	 */
	protected int socketBacklog = 100;

	/**
	 * Tunnel listening port.
	 */
	protected int listenPort = 8888;

	/**
	 * Target host.
	 */
	protected String targetHost = "localhost";

	/**
	 * Target port.
	 */
	protected int targetPort = 8080;

	protected ExecutorService executorService;
	protected volatile boolean running;
	protected ServerSocket serverSocket;

	/**
	 * Starts HTTP tunnel. Method ends when the tunnel is stopped.
	 */
	public void start() throws IOException {
		serverSocket = new ServerSocket(listenPort, socketBacklog);
		serverSocket.setReuseAddress(true);
		executorService = Executors.newFixedThreadPool(threadPoolSize);

		running = true;
		while (running) {
			Socket socket = serverSocket.accept();
			socket.setKeepAlive(false);
			executorService.execute(onSocketConnection(socket));
		}
		executorService.shutdown();
	}

	/**
	 * Invoked on incoming connection. By default returns {@link HttpTunnelConnection}
	 * to handle the connection. May be used to return custom
	 * handlers.
	 */
	protected Runnable onSocketConnection(Socket socket) {
		return new HttpTunnelConnection(socket);
	}

	/**
	 * Stops the tunnel, shutdowns the thread pool and closes server socket.
	 */
	public void stop() {
		running = false;
		executorService.shutdown();
		try {
			serverSocket.close();
		} catch (IOException ignore) {
		}
	}

	/**
	 * Single connection handler that performs the tunneling.
	 */
	public class HttpTunnelConnection implements Runnable {

		protected final Socket socket;

		public HttpTunnelConnection(Socket socket) {
			this.socket = socket;
		}

		public void run() {
			try {
				tunnel();
			} catch (IOException ioex) {
				ioex.printStackTrace();
			}
		}

		/**
		 * Invoked after income connection is parsed. Nothing is
		 * changed in the request, except the target host and port.
		 */
		protected void onRequest(HttpRequest request) {
		}

		/**
		 * Invoked after target response is processed. Response is now
		 * ready to be sent back to the client. The following header
		 * parameters are changed:
		 * <ul>
		 * <li>Transfer-Encoding is removed, as body is returned at once,</li>
		 * <li>Content-Length is added/update to body size.</li>
		 * </ul>
		 */
		protected void onResponse(HttpResponse response) {
		}

		/**
		 * Performs the tunneling. The following steps occurs:
		 * <ul>
		 * <li>read and parse clients request</li>
		 * <li>open socket to target</li>
		 * <li>resend request to target</li>
		 * <li>read targets response</li>
		 * <li>fix response and resend it to client</li>
		 * </ul>
		 */
		protected void tunnel() throws IOException {

			// read request
			InputStream socketInput = socket.getInputStream();
			HttpRequest request = HttpRequest.readFrom(socketInput);

			// open client socket to target
			Socket clientSocket = new Socket();
			clientSocket.connect(new InetSocketAddress(targetHost, targetPort));

			// do request
			request.host(targetHost);
			request.port(targetPort);
			request.setHostHeader();
			onRequest(request);

			// resend request to target
			OutputStream out = clientSocket.getOutputStream();
			request.sendTo(out);

			// read target response
			InputStream in = clientSocket.getInputStream();
			HttpResponse response = HttpResponse.readFrom(in);

			// close client socket
			StreamUtil.close(in);
			StreamUtil.close(out);
			try {
				clientSocket.close();
			} catch (IOException ignore) {
			}

			// fix response
			if (response.body() != null) {
				response.removeHeader("Transfer-Encoding");
				response.contentLength(response.body().length());
			}

			// do response
			onResponse(response);

			// send response back
			OutputStream socketOutput = socket.getOutputStream();
			response.sendTo(socketOutput);

			// close socket
			StreamUtil.close(socketInput);
			StreamUtil.close(socketOutput);
			try {
				socket.close();
			} catch (IOException ignore) {
			}
		}
	}

}