aiohttp Port Forwarding (Reverse Proxy)

Port forwarding with aiohttp allows you to create a proxy server that redirects incoming HTTP requests to another server port.

In this tutorial, you’ll learn how to implement port forwarding using aiohttp in Python.

 

 

Implement Basic Aiohttp Server

To start, you’ll need to create a basic aiohttp server. Here’s how you can do it:

import aiohttp
from aiohttp import web
import asyncio
async def hello(request):
    return web.Response(text="Hello, World!")
app = web.Application()
app.router.add_get('/', hello)
web.run_app(app)

Output:

======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)

When you visit http://localhost:8080, you’ll see “Hello, World!” displayed.

 

Configure Port Forwarding

For example, let’s forward requests from port 8080 to port 9000.

We can do this by creating a reverse proxy. The aiohttp server will listen on port 8080 and forward requests to a service running on port 9000.

import aiohttp
from aiohttp import web
async def proxy_handler(request):
    async with aiohttp.ClientSession() as session:
        url = f"http://localhost:9000{request.path_qs}"
        method = request.method
        headers = request.headers.copy()
        data = await request.read()
        async with session.request(method, url, headers=headers, data=data) as resp:
            response = web.StreamResponse(status=resp.status, headers=resp.headers)
            await response.prepare(request)
            async for chunk in resp.content.iter_any():
                await response.write(chunk)
            return response
app = web.Application()
app.router.add_route('*', '/{path:.*}', proxy_handler)
if __name__ == '__main__':
    web.run_app(app, port=8080)

Now, let’s create a simple server that runs on port 9000 using aiohttp.

This server will respond to requests with a message indicating the path that was accessed.

from aiohttp import web
async def handle(request):
    name = request.match_info.get('name', "Anonymous")
    text = f"Hello, {name}! You've reached the server on port 9000. Path: {request.path}"
    return web.Response(text=text)
app = web.Application()
app.router.add_get('/', handle)
app.router.add_get('/{name}', handle)
if __name__ == '__main__':
    web.run_app(app, port=9000)

This server will now be listening on port 9000. You can test it directly by visiting http://localhost:9000 in your web browser.

This setup will forward all requests received on port 8080 to the corresponding paths on port 9000.

When you visit HTTP://127.0.0.1:8080, you’ll get:

Hello, Anonymous! You've reached the server on port 9000. Path: /

 

Process and Modify Forwarded Requests

Add or modify headers

You can add or modify headers before forwarding the request:

async def proxy_handler(request):
    async with aiohttp.ClientSession() as session:
        url = f"http://localhost:9000{request.path_qs}"
        method = request.method
        headers = request.headers.copy()
        headers['X-Forwarded-For'] = request.remote
        headers['X-Forwarded-Proto'] = request.scheme
        headers['X-Forwarded-Host'] = request.host

        # Add custom header
        headers['X-Custom-Header'] = 'SomeValue'

        # Modify existing header
        if 'User-Agent' in headers:
            headers['User-Agent'] += ' via-proxy'
        data = await request.read()
        async with session.request(method, url, headers=headers, data=data) as resp:
            response = web.StreamResponse(status=resp.status, headers=resp.headers)
            await response.prepare(request)
            async for chunk in resp.content.iter_any():
                await response.write(chunk)
            return response
app = web.Application()
app.router.add_route('*', '/{path:.*}', proxy_handler)
if __name__ == '__main__':
    web.run_app(app, port=8080)

This example adds ‘X-Forwarded-For’ and ‘X-Forwarded-Host’ headers to the forwarded request.

Transform request body

To transform the request body, you can modify it before forwarding:

async def transform_body(body):
    try:
        data = json.loads(body)
    except json.JSONDecodeError:
        return body
    from datetime import datetime
    data['timestamp'] = datetime.now().isoformat()
    data['processed_by_proxy'] = True
    return json.dumps(data)
async def proxy_handler(request):
    async with aiohttp.ClientSession() as session:
        url = f"http://localhost:9000{request.path_qs}"
        method = request.method
        headers = request.headers.copy()

        # Read and transform the body
        original_body = await request.read()
        transformed_body = await transform_body(original_body)
        async with session.request(method, url, headers=headers, data=transformed_body) as resp:
            response = web.StreamResponse(status=resp.status, headers=resp.headers)
            await response.prepare(request)
            async for chunk in resp.content.iter_any():
                await response.write(chunk)            
            return response
app = web.Application()
app.router.add_route('*', '/{path:.*}', proxy_handler)
if __name__ == '__main__':
    web.run_app(app, port=8080)

The transform_body function tries to parse the request body as JSON.

If successful, it adds a timestamp and a ‘processed’ flag to the data. If the body isn’t JSON, it returns the original body unchanged.

 

HTTPS Port Forwarding

To forward requests to HTTPS endpoints, you’ll need to handle SSL/TLS connections and deal with certificate verification:

import aiohttp
import ssl
from aiohttp import web
import json
import certifi
async def transform_body(body):
    try:
        data = json.loads(body)
        from datetime import datetime
        data['timestamp'] = datetime.now().isoformat()
        data['processed_by_proxy'] = True
        return json.dumps(data)
    except json.JSONDecodeError:
        return body
async def proxy_handler(request):
    ssl_context = ssl.create_default_context(cafile=certifi.where())
    async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context)) as session:

        url = f"https://httpbin.org{request.path}"
        method = request.method
        headers = request.headers.copy()
        original_body = await request.read()
        transformed_body = await transform_body(original_body)
        try:
            async with session.request(method, url, headers=headers, data=transformed_body) as resp:
                response = web.StreamResponse(status=resp.status, headers=resp.headers)
                await response.prepare(request)
                async for chunk in resp.content.iter_any():
                    await response.write(chunk)
                return response
        except aiohttp.ClientError as e:
            return web.Response(text=f"Error forwarding request: {str(e)}", status=500)
app = web.Application()
app.router.add_route('*', '/{path:.*}', proxy_handler)
if __name__ == '__main__':
    web.run_app(app, port=8080)

This will forward requests to the HTTPBin API.

Leave a Reply

Your email address will not be published. Required fields are marked *