initial implementation
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.venv
|
||||
__pycache__
|
||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[build-system]
|
||||
requires = ["hatchling >= 1.26"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "rproxy"
|
||||
dynamic = ["version"]
|
||||
|
||||
dependencies = [
|
||||
"pyyaml",
|
||||
"pydantic",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
rproxy = "rproxy.__main__:start_reverse_proxy"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "rproxy/__about__.py"
|
||||
|
||||
# [tool.hatch.build.targets.wheel]
|
||||
# only-include = ["rproxy"]
|
||||
1
rproxy/__about__.py
Normal file
1
rproxy/__about__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
0
rproxy/__init__.py
Normal file
0
rproxy/__init__.py
Normal file
42
rproxy/__main__.py
Executable file
42
rproxy/__main__.py
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
from rproxy.sockets import start_reverse_proxy
|
||||
from rproxy.config import Config
|
||||
|
||||
|
||||
def usage() -> str:
|
||||
example = (
|
||||
Path(importlib.util.find_spec("rproxy").origin).parent
|
||||
/ "resources"
|
||||
/ "example.config.yaml"
|
||||
)
|
||||
return (
|
||||
f"Reverse proxy server.\n\nusage: {prog} config\n"
|
||||
f"example: {prog} ./my-config.yaml\n\n"
|
||||
f"See example config file at {example}"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
with open(sys.argv[1]) as f:
|
||||
config = Config(**yaml.safe_load(f))
|
||||
print(config)
|
||||
except (FileNotFoundError, IndexError) as e:
|
||||
prog = os.path.basename(sys.argv[0])
|
||||
print(
|
||||
f"Error: missing config file argument. {e}.\n{usage()}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
start_reverse_proxy(config)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
19
rproxy/config.py
Normal file
19
rproxy/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pydantic
|
||||
|
||||
|
||||
class SocketAddr(pydantic.BaseModel):
|
||||
host: str
|
||||
port: int = 8000
|
||||
|
||||
|
||||
class LocationMatch(pydantic.BaseModel):
|
||||
request_line: str = ""
|
||||
|
||||
|
||||
class Location(SocketAddr):
|
||||
match: LocationMatch = LocationMatch()
|
||||
|
||||
|
||||
class Config(pydantic.BaseModel):
|
||||
listen: SocketAddr
|
||||
locations: list[Location]
|
||||
14
rproxy/resources/example.config.yaml
Normal file
14
rproxy/resources/example.config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
listen:
|
||||
host: &local localhost
|
||||
port: 8001
|
||||
|
||||
locations:
|
||||
- match:
|
||||
request_line: ^GET /api.*\r\n
|
||||
# headers:
|
||||
# foo: bar
|
||||
host: *local
|
||||
port: 8081
|
||||
|
||||
- host: *local
|
||||
port: 8089
|
||||
96
rproxy/sockets.py
Normal file
96
rproxy/sockets.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from rproxy.config import Config
|
||||
|
||||
|
||||
# Function to forward data from one socket to another
|
||||
def forward_data(source: socket.socket, destination: socket.socket):
|
||||
while True:
|
||||
data = source.recv(4 << 10)
|
||||
if not data:
|
||||
break
|
||||
destination.sendall(data)
|
||||
|
||||
|
||||
def handle_client(
|
||||
request_line: bytes,
|
||||
client_socket: socket.socket,
|
||||
target_host: str,
|
||||
target_port: int,
|
||||
):
|
||||
try:
|
||||
# Connect to the target server
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.connect((target_host, target_port))
|
||||
|
||||
# We do this just once, and then assume any other requests to this
|
||||
# connection will be for the selected backend.
|
||||
server_socket.sendall(request_line)
|
||||
|
||||
# Create threads to forward data in both directions
|
||||
client_to_server = threading.Thread(
|
||||
target=forward_data, args=(client_socket, server_socket)
|
||||
)
|
||||
server_to_client = threading.Thread(
|
||||
target=forward_data, args=(server_socket, client_socket)
|
||||
)
|
||||
|
||||
# Start the threads
|
||||
client_to_server.start()
|
||||
server_to_client.start()
|
||||
|
||||
# Wait for the threads to complete
|
||||
client_to_server.join()
|
||||
server_to_client.join()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
client_socket.close()
|
||||
server_socket.close()
|
||||
|
||||
|
||||
def read_request_line(socket: socket.socket):
|
||||
data = b""
|
||||
while not data.endswith(b"\r\n"):
|
||||
data += socket.recv(1)
|
||||
return data
|
||||
|
||||
|
||||
def start_reverse_proxy(config: Config):
|
||||
# Create a socket
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
# Bind the socket to the specified address and port
|
||||
server_socket.bind((config.listen.host, config.listen.port))
|
||||
|
||||
# Listen for incoming connections
|
||||
server_socket.listen(5)
|
||||
print(f"Reverse proxy listening on {config.listen.host}:{config.listen.port}")
|
||||
|
||||
while True:
|
||||
# Accept a connection
|
||||
client_socket, client_address = server_socket.accept()
|
||||
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")
|
||||
|
||||
# Handle the client connection in a new thread
|
||||
request_line = read_request_line(client_socket)
|
||||
target_host, target_port = None, None
|
||||
for location in config.locations:
|
||||
if re.match(location.match.request_line, request_line.decode()):
|
||||
target_host = location.host
|
||||
target_port = location.port
|
||||
break
|
||||
|
||||
if not (target_host and target_port):
|
||||
print(f'Missing location match for request line "{request_line}"')
|
||||
client_socket.close()
|
||||
continue
|
||||
|
||||
client_thread = threading.Thread(
|
||||
target=handle_client,
|
||||
args=(request_line, client_socket, target_host, target_port),
|
||||
)
|
||||
client_thread.start()
|
||||
Reference in New Issue
Block a user