Initial commit to show off.

This commit is contained in:
retoor 2024-12-17 22:49:51 +01:00
parent f994c5dfb4
commit ac0fc9104d
7 changed files with 228 additions and 2 deletions

29
Makefile Normal file
View File

@ -0,0 +1,29 @@
BIN = ./.venv/bin/
PYTHON = ./.venv/bin/python
PIP = ./.venv/bin/pip
APP_NAME=zamenyat
all: install build
ensure_repo:
-@git init
ensure_env: ensure_repo
-@python3 -m venv .venv
install: ensure_env
$(PIP) install -e .
format:
$(PIP) install shed
. $(BIN)/activate && shed
build:
$(MAKE) format
$(PIP) install build
$(PYTHON) -m build
run:
$(BIN)$(APP_NAME) --host="0.0.0.0" --port=3046 --upstream-host="127.0.0.1" --upstream-port=9999

View File

@ -1,3 +1,4 @@
# zamenyat
# Zamenyat
HTTP bridge configurable to replace the content you want to see replaced. This can be used to have a real version and anonymous version for a website for example like I did. This site exists in the retoor version and under my real name for example.
HTTP bridge configurable to replace the content you want to see replaced. This can be used to have a real version and anonymous version for a website for example like I did. This site exists in the retoor version and under my real name for example.

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

24
setup.cfg Normal file
View File

@ -0,0 +1,24 @@
[metadata]
name = zamenyat
version = 1.0.0
description = Replaces sensitive information served by HTTP.
author = retoor
author_email = retoor@molodetz.nl
license = MIT
long_description = file: README.md
long_description_content_type = text/markdown
[options]
packages = find:
package_dir =
= src
python_requires = >=3.7
install_requires =
app @ git+https://retoor.molodetz.nl/retoor/app
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
zamenyat = zamenyat.__main__:main

0
src/zamenyat/__init__.py Normal file
View File

35
src/zamenyat/__main__.py Normal file
View File

@ -0,0 +1,35 @@
import argparse
from zamenyat.app import Application
parser = argparse.ArgumentParser(description="Zamenyat sensitive content replacer.")
parser.add_argument(
"--host",
required=True,
type=str
)
parser.add_argument(
"--port",
required=True,
type=int
)
parser.add_argument(
"--upstream-host",
required=True,
type=str
)
parser.add_argument(
"--upstream-port",
required=True,
type=int
)
def main():
args = parser.parse_args()
app = Application(upstream_host=args.upstream_host, upstream_port=args.upstream_port)
app.serve(host=args.host,port=args.port)
if __name__ == '__main__':
main()

134
src/zamenyat/app.py Normal file
View File

@ -0,0 +1,134 @@
from app.app import Application as BaseApplication, get_timestamp
import asyncio
from concurrent.futures import ThreadPoolExecutor as Executor
import time
ZAMENYAT_THREAD_COUNT = 500
ZAMENYAT_BUFFER_SIZE = 4096*2
ZAMENYAT_HEADER_MAX_LENGTH = 4096*2
class Application:
def __init__(self, upstream_host, upstream_port, *args, **kwargs):
self.upstream_host = upstream_host
self.upstream_port = upstream_port
self.server = None
self.host = None
self.port = None
self.executor = None
self.buffer_size = ZAMENYAT_BUFFER_SIZE
self.header_max_length = ZAMENYAT_HEADER_MAX_LENGTH
self.connection_count = 0
self.total_connection_count = 0
super().__init__(*args, **kwargs)
async def get_headers(self, reader):
data = b''
headers = None
while True:
chunk = await reader.read(self.buffer_size)
if not chunk:
break
data += chunk
if len(data) > self.header_max_length:
break
headers_end = data.find(b'\r\n\r\n')
if headers_end:
headers = data[:headers_end]
data = data[:headers_end + 4]
break
if not headers:
return None, None, None
header_dict = {}
req_resp, *headers = headers.split(b"\r\n")
for header_line in headers:
key, *value = header_line.split(b": ")
key = key.decode()
value = ": ".join([value.decode() for value in value])
header_dict[key] = int(value) if value.isdigit() else value
return req_resp.decode(), header_dict, data
def header_dict_to_bytes(self, req_resp, headers):
header_list = [req_resp]
for key, value in headers.items():
header_list.append("{}: {}".format(key, value))
header_list.append("\r\n")
return ("\r\n".join(header_list)).encode()
async def stream(self, reader,writer):
try:
while True:
req_resp, headers, data = await self.get_headers(reader)
if not headers:
break
if 'Content-Length' in headers:
while not len(data) == headers['Content-Length']:
chunk_size = headers['Content-Length'] - len(data) if self.buffer_size > headers['Content-Length'] - len(data) else self.buffer_size
chunk = await reader.read(chunk_size)
if not chunk:
data = None
break
data += chunk
print(self.header_dict_to_bytes(req_resp,headers).decode())
writer.write(self.header_dict_to_bytes(req_resp, headers))
#await writer.drain()
if data:
print(data)
while data:
chunk_size = self.buffer_size if len(data) > self.buffer_size else len(data)
chunk = data[:chunk_size]
writer.write(chunk)
data = data[chunk_size:]
await writer.drain()
if not headers.get('Connection') == 'keep-alive': # and not headers.get('Upgrade-Insecure-Requests'):
break
except asyncio.CancelledError:
pass
finally:
pass
#writer.close()
#await writer.wait_closed()
async def handle_client(self,reader,writer):
self.connection_count += 1
self.total_connection_count += 1
connection_nr = self.connection_count
upstream_reader, upstream_writer = await asyncio.open_connection(self.upstream_host, self.upstream_port)
time_start = time.time()
print(f"Connected to upstream #{self.total_connection_count} server {self.upstream_host}:{self.upstream_port} #{connection_nr} Time: {get_timestamp()}")
tasks = [
self.stream(upstream_reader, writer),
self.stream(reader, upstream_writer)
]
await asyncio.gather(*tasks)
time_end = time.time()
time_duration = time_end - time_start
print(f"Disconnected upstream #{self.total_connection_count} server {self.upstream_host}:{self.upstream_port} #{connection_nr} Duration: {time_duration:.5f}s")
self.connection_count -= 1
def upgrade_executor(self, thread_count):
self.executor = Executor(max_workers=thread_count)
loop = asyncio.get_running_loop()
loop.set_default_executor(self.executor)
return self.executor
async def serve_async(self, host,port):
self.upgrade_executor(ZAMENYAT_THREAD_COUNT)
self.host = host
self.port = port
self.server = await asyncio.start_server(self.handle_client, self.host, self.port)
async with self.server:
await self.server.serve_forever()
def serve(self, host, port):
try:
asyncio.run(self.serve_async(host,port))
except KeyboardInterrupt:
print("Shutted down server")