Build a network tunnel using Python and the LAMP(PHP) stack.

## Getting Started
Hosting services based on the LAMP stack (Linux + Apache + MySQL + PHP), commonly known as shared hosting, can be obtained at an affordable price in any country. If you are lucky, you can get the infrastructure for one-hundredth the cost of acquiring a virtual server.

I was interested in creating a proxy (also known as a VPN) using shared hosting, and I recently succeeded. I considered an implementation similar to [Shadowsocks](https://shadowsocks.org/) as the most ideal and proceeded with this approach.

This article explains how it is possible.

## Stateless vs. Stateful

### Stateless method
When implementing a proxy, especially one based on the HTTP protocol, the methods are divided into Stateless and Stateful.

The Stateless method is one that has often been attempted in LAMP. (for example, to bypass [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policy on the web browser), It is typically implemented in such a way that a PHP file accesses the specified remote URL on behalf of the user by entering the remote URL as a parameter. This can be seen as a kind of incomplete forward proxy.

For example,

```
{base_url}/proxy.php?url={remote_url}
```

However, this method damages the original headers, making it difficult to consider it a complete proxy as we know it. In my implementation of the Stateless method, I worked to resolve this issue.

To solve this problem, a client written in Python will be introduced shortly. The client encapsulates the entire HTTP request to prevent any loss.

### Stateful method

The primary issue with the previously mentioned Stateless method is its dependency on the environment variables of server applications like Apache or Nginx. When using the LAMP stack as a proxy, the most significant problem would likely be the length restriction on the data to be transmitted (in the case of Apache, this corresponds to the `max_upload_size` variable).

The Stateful method can address the issues arising from dependency on environment variables.

To solve this problem, a client written in Python will be introduced shortly. The clients establish the tunnel with the LAMP based shared server.

## Build a relay with JSON-RPC 2.0

An example of the relay approach I proposed to address the issues is as follows. I adopted the JSON-RPC 2.0 protocol for this implementation.

### Stateless relay

The Stateless method completes the proxy process with just one request.

```
{
    "jsonrpc": "2.0",
    "method": "relay_request",
    "params": {
        "data": <base64 encoded data>,
        "compressed": <e.g. deflate, none>,    // proposal
        "client": <address of the client>,
        "server": <address of the remote server>,
        "port": <port number of the remote server>,
        "scheme": <scheme (e.g. http, https, ssl, tls),
        "url": <URL>,
        "length": <length of data>,
        "chunksize": <size of buffer (e.g. 8192)>,
        "datetime": <datetime (e.g. %Y-%m-%d %H:%M:%S.%f)>
    },
    "id": 3
}
```

### Stateful relay
The Stateful method allows data transmission after receiving acceptance from the server.

```
{
    "jsonrpc": "2.0",
    "method": "relay_connect",
    "params": {
        "client": <address of the client>
        "port": <port number of the client>,
        "chunksize": <size of buffer (e.g. 8192)>,
        "datetime": <datetime (e.g. %Y-%m-%d %H:%M:%S.%f)>
    },
    "id": 3
}

# To be receive the `relay_accept` method request
```

## SSL decryption

We need an SSL decryption solution for conducting proxies for HTTPS (Secure HTTP) as well. For this purpose, the client includes the following code. It generates fake certificates tailored to the domains it connects to automatically.

```python
    try:
        if not os.path.isfile(certpath):
            epoch = "%d" % (time.time() * 1000)
            p1 = Popen([openssl_binpath, "req", "-new", "-key", certkey, "-subj", "/CN=%s" % hostname], stdout=PIPE)
            p2 = Popen([openssl_binpath, "x509", "-req", "-days", "3650", "-CA", cacert, "-CAkey", cakey, "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE)
            p2.communicate()
    except Exception as e:
        print("[*] Skipped generating the certificate. Cause: %s" % (str(e)))
```

## SSL negotiation

Since HTTPS negotiates its process in a stateful style, a connection acceptance message such as `200 Connection Established` response is required.

```python
def proxy_connect(webserver, conn):
    hostname = webserver.decode(client_encoding)
    certpath = "%s/%s.crt" % (certdir.rstrip('/'), hostname)

    # https://stackoverflow.com/questions/24055036/handle-https-request-in-proxy-server-by-c-sharp-connect-tunnel
    conn.send(b'HTTP/1.1 200 Connection Established\r\n\r\n')

    # (omitted: Create a certificate)

    # https://stackoverflow.com/questions/11255530/python-simple-ssl-socket-server
    # https://docs.python.org/3/library/ssl.html
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain(certpath, certkey)

    # https://stackoverflow.com/questions/11255530/python-simple-ssl-socket-server
    conn = context.wrap_socket(conn, server_side=True)
    data = conn.recv(buffer_size)

    return (conn, data)

def proxy_server(webserver, port, scheme, method, url, conn, addr, data):
    try:
        print("[*] Started the request. %s" % (str(addr[0])))

        # SSL negotiation
        is_ssl = scheme in [b'https', b'tls', b'ssl']
        if is_ssl and method == b'CONNECT':
            while True:
                try:
                    conn, data = proxy_connect(webserver, conn)
                    break   # success
                #except OSError as e:
                #    print ("[*] Retrying SSL negotiation... (%s:%s) %s" % (webserver.decode(client_encoding), str(port), str(e)))
                except Exception as e:
                    raise Exception("SSL negotiation failed. (%s:%s) %s" % (webserver.decode(client_encoding), str(port), str(e)))

# (ommited)
```

## implementation!

### Example of the server-side implementation (PHP)

```php
define("PHP_HTTPPROXY_VERSION", "0.1.5");
define("DEFAULT_SOCKET_TIMEOUT", 1);
define("STATEFUL_SOCKET_TIMEOUT", 30);
define("MAX_EXECUTION_TIME", 0);

if (strpos($_SERVER['HTTP_USER_AGENT'], "php-httpproxy/") !== 0) {
    exit('<!DOCTYPE html><html><head><title>It works!</title><meta charset="utf-8"></head><body><h1>It works!</h1><p><a href="https://github.com/gnh1201/caterpillar">Download the client</a></p><p>' . $_SERVER['HTTP_USER_AGENT'] . '</p><hr><p>php-httpproxy/' . PHP_HTTPPROXY_VERSION . ' (Server; PHP ' . phpversion() . '; Caterpillar; abuse@catswords.net)</p></body></html>');
}

ini_set("default_socket_timeout", DEFAULT_SOCKET_TIMEOUT);  // must be. because of `feof()` works
ini_set("max_execution_time", MAX_EXECUTION_TIME);

function jsonrpc2_encode($method, $params, $id = '') {
    $data = array(
        "jsonrpc" => "2.0",
        "method" => $method,
        "params" => $params,
        "id" => $id
    );
    return json_encode($data);
}

function jsonrpc2_result_encode($result, $id = '') {
    $data = array(
        "jsonrpc" => "2.0",
        "result" => $result,
        "id" => $id
    );
    return json_encode($data);
}

function jsonrpc2_error_encode($error, $id = '') {
    $data = array(
        "jsonrpc" => "2.0",
        "error" => $error,
        "id" => $id
    );
    return json_encode($data);
}

function parse_headers($str) { // Parses HTTP headers into an array
    // https://stackoverflow.com/questions/16934409/curl-as-proxy-deal-with-https-connect-method
    // https://stackoverflow.com/questions/12433958/how-to-parse-response-headers-in-php
    $headers = array();

    $lines = preg_split("'\r?\n'", $str);

    $first_line = array_shift($lines);
    $headers['@method'] = explode(' ', $first_line);

    foreach ($lines as $line) {
        if (!preg_match('/^([^:]+):(.*)$/', $line, $out)) continue;
        $headers[$out[1]] = trim($out[2]);
    }

    return $headers;
}

function read_from_remote_server($remote_address, $remote_port, $scheme, $data = null, $conn = null, $buffer_size = 8192, $id = '') {
    if (in_array($scheme, array("https", "ssl", "tls"))) {
        $remote_address = "tls://" . $remote_address;
    }

    $sock = fsockopen($remote_address, $remote_port, $error_code, $error_message, DEFAULT_SOCKET_TIMEOUT);
    if (!$sock) {
        $error = array(
            "status" => 502,
            "code" => $error_code,
            "message" => $error_message
        );

        if ($conn == null) {
            echo jsonrpc2_error_encode($error, $id);
        } else {
            $buf = sprintf("HTTP/1.1 502 Bad Gateway\r\n\r\n");
            $buf .= jsonrpc2_error_encode($error, $id);
            fwrite($conn, $buf);
        }
    } else {
        if ($conn == null) {
            // send data
            fwrite($sock, $data);

            // receive data
            $buf = null;
            while (!feof($sock) && $buf !== false) {
                $buf = fgets($sock, $buffer_size);
                echo $buf;
            }
        } else {
            // send data
            $buf = null;
            while (!feof($conn) && $buf !== false) {
                $buf = fgets($conn, $buffer_size);
                fwrite($sock, $buf);
            }

            // receive data
            $buf = null;
            while (!feof($sock) && $buf !== false) {
                $buf = fgets($sock, $buffer_size);
                fwrite($conn, $buf);
            }
        }

        fclose($sock);
    }
}

// stateless mode
function relay_request($params, $id = '') {
    $buffer_size = $params['buffer_size'];
    $request_data = base64_decode($params['request_data']);
    $request_header = parse_headers($request_data);
    $request_length = intval($params['request_length']);
    $client_address = $params['client_address'];
    $client_port = intval($params['client_port']);
    $client_encoding = $params['client_encoding'];
    $remote_address = $params['remote_address'];
    $remote_port = intval($params['remote_port']);
    $scheme = $params['scheme'];
    $datetime = $params['datetime'];   // format: %Y-%m-%d %H:%M:%S.%f

    if (in_array($scheme, array("https", "ssl", "tls"))) {
        $remote_address = "tls://" . $remote_address;
    }

    switch ($request_header['@method'][0]) {
        case "CONNECT":
            $error = array(
                "status" => 405,
                "code" => -1,
                "message" => "Method Not Allowed"
            );
            echo jsonrpc2_error_encode($error, $id);
            break;

        default:
            read_from_remote_server($remote_address, $remote_port, $scheme, $request_data, null, $buffer_size, $id);
    }
}

// stateful mode
function relay_connect($params, $id = '') {
    $buffer_size = $params['buffer_size'];
    $client_address = $params['client_address'];
    $client_port = intval($params['client_port']);
    $client_encoding = $params['client_encoding'];
    $remote_address = $params['remote_address'];
    $remote_port = intval($params['remote_port']);
    $scheme = $params['scheme'];
    $datetime = $params['datetime'];   // format: %Y-%m-%d %H:%M:%S.%f

    $starttime = microtime(true);
    $conn = fsockopen($client_address, $client_port, $error_code, $error_message, STATEFUL_SOCKET_TIMEOUT);
    if (!$conn) {
        $error = array(
            "status" => 502,
            "code" => $error_code,
            "message" => $error_message,
            "_params" => $params
        );
        echo jsonrpc2_error_encode($error, $id);
    } else {
        $stoptime = microtime(true);
        $connection_speed = floor(($stoptime - $starttime) * 1000);
        $data = jsonrpc2_encode("relay_accept", array(
            "success" => true,
            "connection_speed" => $connection_speed
        ), $id);
        fwrite($conn, $data . "\r\n\r\n");

        read_from_remote_server($remote_address, $remote_port, $scheme, null, $conn, $buffer_size, $id);
        fclose($conn);
    }
}

function get_client_address() {
    $client_address = '';
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        $client_address = $_SERVER['HTTP_CLIENT_IP'];
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $client_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } else {
        $client_address = $_SERVER['REMOTE_ADDR'];
    }
    return array("client_address" => $client_address);
}

// parse a context
$context = json_decode(file_get_contents('php://input'), true);

// check is it JSON-RPC 2 (stateless)
if ($context['jsonrpc'] == "2.0") {
    $method = $context['method'];
    switch ($method) {
        case "relay_request":
            relay_request($context['params'], $context['id']);    // stateless mode
            break;

        case "relay_connect":
            relay_connect($context['params'], $context['id']);    // stateful mode
            break;

        case "get_client_address":
            echo jsonrpc2_result_encode(get_client_address(), $context['id']);
            break;

        default:
            echo jsonrpc2_error_encode(array(
                "status" => 403,
                "message" => "Unsupported method"
            ), $context['id']);
    }
} else {
    echo jsonrpc2_error_encode(array(
       "status" => 403,
       "message" => "Unsupported format"
    ), "");
}
```

### Example of the client-side implementation (Python)

```python
import argparse
import socket
import sys
import os
from _thread import *
from subprocess import PIPE, Popen
import base64
import json
import ssl
import time
import hashlib
import traceback
import textwrap
import importlib
from datetime import datetime
from platform import python_version

import re
import requests
from requests.auth import HTTPBasicAuth
from urllib.parse import urlparse
from decouple import config

from base import Extension, extract_credentials, jsonrpc2_create_id, jsonrpc2_encode, jsonrpc2_result_encode

# initalization
try:
    listening_port = config('PORT', default=5555, cast=int)
    _username, _password, server_url = extract_credentials(config('SERVER_URL', default=''))
    server_connection_type = config('SERVER_CONNECTION_TYPE', default='')
    cakey = config('CA_KEY', default='ca.key')
    cacert = config('CA_CERT', default='ca.crt')
    certkey = config('CERT_KEY', default='cert.key')
    certdir = config('CERT_DIR', default='certs/')
    openssl_binpath = config('OPENSSL_BINPATH', default='openssl')
    client_encoding = config('CLIENT_ENCODING', default='utf-8')
    local_domain = config('LOCAL_DOMAIN', default='')
    proxy_pass = config('PROXY_PASS', default='')
except KeyboardInterrupt:
    print("\n[*] User has requested an interrupt")
    print("[*] Application Exiting.....")
    sys.exit()
except Exception as e:
    print("[*] Failed to initialize:", str(e))

parser = argparse.ArgumentParser()
parser.add_argument('--max_conn', help="Maximum allowed connections", default=255, type=int)
parser.add_argument('--buffer_size', help="Number of samples to be used", default=8192, type=int)

args = parser.parse_args()
max_connection = args.max_conn
buffer_size = args.buffer_size
accepted_relay = {}
resolved_address_list = []

# set environment of Extension
Extension.set_buffer_size(buffer_size)
Extension.set_protocol('tcp')

# set basic authentication
auth = None
if _username:
    auth = HTTPBasicAuth(_username, _password)

def parse_first_data(data):
    parsed_data = (b'', b'', b'', b'', b'')

    try:
        first_line = data.split(b'\n')[0]

        method, url = first_line.split()[0:2]

        http_pos = url.find(b'://') #Finding the position of ://
        scheme = b'http'  # check http/https or other protocol
        if http_pos == -1:
            temp = url
        else:
            temp = url[(http_pos+3):]
            scheme = url[0:http_pos]

        port_pos = temp.find(b':')

        webserver_pos = temp.find(b'/')
        if webserver_pos == -1:
            webserver_pos = len(temp)
        webserver = b''
        port = -1
        if port_pos == -1 or webserver_pos < port_pos:
            port = 80
            webserver = temp[:webserver_pos]
        else:
            port = int((temp[(port_pos+1):])[:webserver_pos-port_pos-1])
            webserver = temp[:port_pos]
            if port == 443:
                scheme = b'https'

        parsed_data = (webserver, port, scheme, method, url)
    except Exception as e:
        print("[*] Exception on parsing the header. Cause: %s" % (str(e)))

    return parsed_data

def conn_string(conn, data, addr):
    # JSON-RPC 2.0 request
    def process_jsonrpc2(data):
        jsondata = json.loads(data.decode(client_encoding, errors='ignore'))
        if jsondata['jsonrpc'] == "2.0":
            jsonrpc2_server(conn, jsondata['id'], jsondata['method'], jsondata['params'])
            return True
        return False

    # JSON-RPC 2.0 request over Socket (stateful)
    if data.find(b'{') == 0 and process_jsonrpc2(data):
        # will be close by the client
        return

    # parse first data (header)
    webserver, port, scheme, method, url = parse_first_data(data)

    # JSON-RPC 2.0 request over HTTP (stateless)
    path = urlparse(url.decode(client_encoding)).path
    if path == "/proxy-cgi/jsonrpc2":
        conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n')
        pos = data.find(b'\r\n\r\n')
        if pos > -1 and process_jsonrpc2(data[pos+4:]):
            conn.close()   # will be close by the server
            return

    # if it is reverse proxy
    if local_domain != '':
        localserver = local_domain.encode(client_encoding)
        if webserver == localserver or data.find(b'\nHost: ' + localserver) > -1:
            print ("[*] Detected the reverse proxy request: %s" % (local_domain))
            scheme, _webserver, _port = proxy_pass.encode(client_encoding).split(b':')
            webserver = _webserver[2:]
            port = int(_port.decode(client_encoding))

    proxy_server(webserver, port, scheme, method, url, conn, addr, data)

def jsonrpc2_server(conn, id, method, params):
    if method == "relay_accept":
        accepted_relay[id] = conn
        connection_speed = params['connection_speed']
        print ("[*] connection speed: %s miliseconds" % (str(connection_speed)))
        while conn.fileno() > -1:
            time.sleep(1)
        del accepted_relay[id]
        print ("[*] relay destroyed: %s" % (id))
    else:
        Extension.dispatch_rpcmethod(method, "call", id, params, conn)

    #return in conn_string()

def proxy_connect(webserver, conn):
    hostname = webserver.decode(client_encoding)
    certpath = "%s/%s.crt" % (certdir.rstrip('/'), hostname)

    # https://stackoverflow.com/questions/24055036/handle-https-request-in-proxy-server-by-c-sharp-connect-tunnel
    conn.send(b'HTTP/1.1 200 Connection Established\r\n\r\n')

    # https://github.com/inaz2/proxy2/blob/master/proxy2.py
    try:
        if not os.path.isfile(certpath):
            epoch = "%d" % (time.time() * 1000)
            p1 = Popen([openssl_binpath, "req", "-new", "-key", certkey, "-subj", "/CN=%s" % hostname], stdout=PIPE)
            p2 = Popen([openssl_binpath, "x509", "-req", "-days", "3650", "-CA", cacert, "-CAkey", cakey, "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE)
            p2.communicate()
    except Exception as e:
        print("[*] Skipped generating the certificate. Cause: %s" % (str(e)))

    # https://stackoverflow.com/questions/11255530/python-simple-ssl-socket-server
    # https://docs.python.org/3/library/ssl.html
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain(certpath, certkey)

    # https://stackoverflow.com/questions/11255530/python-simple-ssl-socket-server
    conn = context.wrap_socket(conn, server_side=True)
    data = conn.recv(buffer_size)

    return (conn, data)

def proxy_check_filtered(data, webserver, port, scheme, method, url):
    filtered = False

    filters = Extension.get_filters()
    print ("[*] Checking data with %s filters..." % (str(len(filters))))
    for f in filters:
        filtered = f.test(filtered, data, webserver, port, scheme, method, url)

    return filtered

def proxy_server(webserver, port, scheme, method, url, conn, addr, data):
    try:
        print("[*] Started the request. %s" % (str(addr[0])))

        # SSL negotiation
        is_ssl = scheme in [b'https', b'tls', b'ssl']
        if is_ssl and method == b'CONNECT':
            while True:
                try:
                    conn, data = proxy_connect(webserver, conn)
                    break   # success
                #except OSError as e:
                #    print ("[*] Retrying SSL negotiation... (%s:%s) %s" % (webserver.decode(client_encoding), str(port), str(e)))
                except Exception as e:
                    raise Exception("SSL negotiation failed. (%s:%s) %s" % (webserver.decode(client_encoding), str(port), str(e)))

        # override data
        if is_ssl:
            _, _, _, method, url = parse_first_data(data)

        # https://stackoverflow.com/questions/44343739/python-sockets-ssl-eof-occurred-in-violation-of-protocol
        def sock_close(sock, is_ssl = False):
            #if is_ssl:
            #    sock = sock.unwrap()
            #sock.shutdown(socket.SHUT_RDWR)
            sock.close()

        # Wait to see if there is more data to transmit
        def sendall(sock, conn, data):
            # send first chuck
            if proxy_check_filtered(data, webserver, port, scheme, method, url):
                sock.close()
                raise Exception("Filtered request")
            sock.send(data)
            if len(data) < buffer_size:
                return

            # send following chunks
            buffered = b''
            conn.settimeout(1)
            while True:
                try:
                    chunk = conn.recv(buffer_size)
                    if not chunk:
                        break
                    buffered += chunk
                    if proxy_check_filtered(buffered, webserver, port, scheme, method, url):
                        sock_close(sock, is_ssl)
                        raise Exception("Filtered request")
                    sock.send(chunk)
                    if len(buffered) > buffer_size*2:
                        buffered = buffered[-buffer_size*2:]
                except:
                    break

        # localhost mode
        if server_url == "localhost":
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

            if is_ssl:
                context = ssl.create_default_context()
                context.check_hostname = False
                context.verify_mode = ssl.CERT_NONE

                sock = context.wrap_socket(sock, server_hostname=webserver.decode(client_encoding))
                sock.connect((webserver, port))
                #sock.sendall(data)
                sendall(sock, conn, data)
            else:
                sock.connect((webserver, port))
                #sock.sendall(data)
                sendall(sock, conn, data)

            i = 0
            is_http_403 = False
            buffered = b''
            while True:
                chunk = sock.recv(buffer_size)
                if not chunk:
                    break
                if i == 0 and chunk.find(b'HTTP/1.1 403') == 0:
                    is_http_403 = True
                    break
                buffered += chunk
                if proxy_check_filtered(buffered, webserver, port, scheme, method, url):
                    sock_close(sock, is_ssl)
                    add_filtered_host(webserver.decode(client_encoding), '127.0.0.1')
                    raise Exception("Filtered response")
                conn.send(chunk)
                if len(buffered) > buffer_size*2:
                    buffered = buffered[-buffer_size*2:]
                i += 1

            # when blocked
            if is_http_403:
                print ("[*] Blocked the request by remote server: %s" % (webserver.decode(client_encoding)))

                def bypass_callback(response, *args, **kwargs):
                    if response.status_code != 200:
                        conn.sendall(b"HTTP/1.1 403 Forbidden\r\n\r\n{\"status\":403}")
                        return

                    # https://stackoverflow.com/questions/20658572/python-requests-print-entire-http-request-raw
                    format_headers = lambda d: '\r\n'.join(f'{k}: {v}' for k, v in d.items())

                    first_data = textwrap.dedent('HTTP/1.1 {res.status_code} {res.reason}\r\n{reshdrs}\r\n\r\n').format(
                        res=response,
                        reshdrs=format_headers(response.headers),
                    ).encode(client_encoding)
                    conn.send(first_data)

                    for chunk in response.iter_content(chunk_size=buffer_size):
                        conn.send(chunk)

                if is_ssl and method == b'GET':
                    print ("[*] Trying to bypass blocked request...")
                    remote_url = "%s://%s%s" % (scheme.decode(client_encoding), webserver.decode(client_encoding), url.decode(client_encoding))
                    requests.get(remote_url, stream=True, verify=False, hooks={'response': bypass_callback})
                else:
                    conn.sendall(b"HTTP/1.1 403 Forbidden\r\n\r\n{\"status\":403}")

            sock_close(sock, is_ssl)

            print("[*] Received %s chunks. (%s bytes per chunk)" % (str(i), str(buffer_size)))

        # stateful mode
        elif server_connection_type == "stateful":
            proxy_data = {
                'headers': {
                    "User-Agent": "php-httpproxy/0.1.5 (Client; Python " + python_version() + "; abuse@catswords.net)",
                },
                'data': {
                    "buffer_size": str(buffer_size),
                    "client_address": str(addr[0]),
                    "client_port": str(listening_port),
                    "client_encoding": client_encoding,
                    "remote_address": webserver.decode(client_encoding),
                    "remote_port": str(port),
                    "scheme": scheme.decode(client_encoding),
                    "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
                }
            }

            # get client address
            print ("[*] resolving the client address...")
            while len(resolved_address_list) == 0:
                try:
                    _, query_data = jsonrpc2_encode('get_client_address')
                    query = requests.post(server_url, headers=proxy_data['headers'], data=query_data, timeout=1, auth=auth)
                    if query.status_code == 200:
                        result = query.json()['result']
                        resolved_address_list.append(result['client_address'])
                    print ("[*] resolved IP: %s" % (result['client_address']))
                except requests.exceptions.ReadTimeout as e:
                    pass
            proxy_data['data']['client_address'] = resolved_address_list[0]

            # build a tunnel
            def relay_connect(id, raw_data, proxy_data):
                try:
                    # The tunnel connect forever until the client destroy it
                    relay = requests.post(server_url, headers=proxy_data['headers'], data=raw_data, stream=True, timeout=None, auth=auth)
                    for chunk in relay.iter_content(chunk_size=buffer_size):
                        jsondata = json.loads(chunk.decode(client_encoding, errors='ignore'))
                        if jsondata['jsonrpc'] == "2.0" and ("error" in jsondata):
                            e = jsondata['error']
                            print ("[*] Error received from the relay server: (%s) %s" % (str(e['code']), str(e['message'])))
                except requests.exceptions.ReadTimeout as e:
                    pass
            id, raw_data = jsonrpc2_encode('relay_connect', proxy_data['data'])
            start_new_thread(relay_connect, (id, raw_data, proxy_data))

            # wait for the relay
            print ("[*] waiting for the relay... %s" % (id))
            max_reties = 30
            t = 0
            while t < max_reties and not id in accepted_relay:
                time.sleep(1)
                t += 1
            if t < max_reties:
                sock = accepted_relay[id]
                print ("[*] connected the relay. %s" % (id))
                sendall(sock, conn, data)
            else:
                resolved_address_list.remove(resolved_address_list[0])
                print ("[*] the relay is gone. %s" % (id))
                sock_close(sock, is_ssl)
                return

            # get response
            i = 0
            buffered = b''
            while True:
                chunk = sock.recv(buffer_size)
                if not chunk:
                    break
                buffered += chunk
                if proxy_check_filtered(buffered, webserver, port, scheme, method, url):
                    sock_close(sock, is_ssl)
                    add_filtered_host(webserver.decode(client_encoding), '127.0.0.1')
                    raise Exception("Filtered response")
                conn.send(chunk)
                if len(buffered) > buffer_size*2:
                    buffered = buffered[-buffer_size*2:]
                i += 1

            sock_close(sock, is_ssl)

            print("[*] Received %s chunks. (%s bytes per chunk)" % (str(i), str(buffer_size)))

        # stateless mode
        elif server_connection_type == "stateless":
            proxy_data = {
                'headers': {
                    "User-Agent": "php-httpproxy/0.1.5 (Client; Python " + python_version() + "; abuse@catswords.net)",
                },
                'data': {
                    "buffer_size": str(buffer_size),
                    "request_data": base64.b64encode(data).decode(client_encoding),
                    "request_length": str(len(data)),
                    "client_address": str(addr[0]),
                    "client_port": str(listening_port),
                    "client_encoding": client_encoding,
                    "remote_address": webserver.decode(client_encoding),
                    "remote_port": str(port),
                    "scheme": scheme.decode(client_encoding),
                    "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
                }
            }
            _, raw_data = jsonrpc2_encode('relay_request', proxy_data['data'])

            print("[*] Sending %s bytes..." % (str(len(raw_data))))

            i = 0
            relay = requests.post(server_url, headers=proxy_data['headers'], data=raw_data, stream=True, auth=auth)
            buffered = b''
            for chunk in relay.iter_content(chunk_size=buffer_size):
                buffered += chunk
                if proxy_check_filtered(buffered, webserver, port, scheme, method, url):
                    add_filtered_host(webserver.decode(client_encoding), '127.0.0.1')
                    raise Exception("Filtered response")
                conn.send(chunk)
                if len(buffered) > buffer_size*2:
                    buffered = buffered[-buffer_size*2:]
                i += 1

            print("[*] Received %s chunks. (%s bytes per chunk)" % (str(i), str(buffer_size)))

        # nothing at all
        else:
            connector = Extension.get_connector(server_connection_type)
            if connector:
                connector.connect(conn, data, webserver, port, scheme, method, url)
            else:
                raise Exception("Unsupported connection type")

        print("[*] Request and received. Done. %s" % (str(addr[0])))
        conn.close()
    except Exception as e:
        print(traceback.format_exc())
        print("[*] Exception on requesting the data. Cause: %s" % (str(e)))
        conn.sendall(b"HTTP/1.1 403 Forbidden\r\n\r\n{\"status\":403}")
        conn.close()

# journaling a filtered hosts
def add_filtered_host(domain, ip_address):
    hosts_path = './filtered.hosts'
    with open(hosts_path, 'r') as file:
        lines = file.readlines()

    domain_exists = any(domain in line for line in lines)
    if not domain_exists:
        lines.append(f"{ip_address}\t{domain}\n")
        with open(hosts_path, 'w') as file:
            file.writelines(lines)

def start():    #Main Program
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('', listening_port))
        sock.listen(max_connection)
        print("[*] Server started successfully [ %d ]" %(listening_port))
    except Exception as e:
        print("[*] Unable to Initialize Socket:", str(e))
        sys.exit(2)

    while True:
        try:
            conn, addr = sock.accept() #Accept connection from client browser
            data = conn.recv(buffer_size) #Recieve client data
            start_new_thread(conn_string, (conn, data, addr)) #Starting a thread
        except KeyboardInterrupt:
            sock.close()
            print("\n[*] Graceful Shutdown")
            sys.exit(1)

if __name__== "__main__":
    # load extensions
    #Extension.register(importlib.import_module("plugins.yourownplugin").YourOwnPlugin())
    
    # start Caterpillar
    start()
```

## Write a configuration file

Please save the following content into a `.env` or `settings.ini` file.

```
[settings]
PORT=5555
SERVER_URL=http://example.org
SERVER_CONNECTION_TYPE=stateless
CA_KEY=ca.key
CA_CERT=ca.crt
CERT_KEY=cert.key
CERT_DIR=certs/
OPENSSL_BINPATH=openssl
CLIENT_ENCODING=utf-8
```

## Just do it
Once the implementation is complete, you can execute it with a command like `python3 server.py` to check the listening port (e.g., `localhost:5555`). You can then add this to the HTTP proxy settings of your operating system or web browser to start using it.

## Conclusion
You can build a proxy (or VPN) using shared hosting based on the LAMP stack.

If you are looking for an example of implementing an HTTP proxy capable of performing these functions, you can refer to the [Caterpillar Proxy](https://github.com/gnh1201/caterpillar) project.

Thank you.