From 605fbc51a376ebf7d8452f4f3e224259a565a868 Mon Sep 17 00:00:00 2001 From: nsde Date: Tue, 25 Jul 2023 02:42:53 +0200 Subject: [PATCH] WIP --- api/main.py | 4 +-- api/netclient.py | 24 +++++++++++++++ api/proxies.py | 47 ++++++++++++++++++++++++++-- api/security.py | 45 --------------------------- api/tests.py | 62 ------------------------------------- api/transfer.py | 72 ++++++++++++------------------------------- proxy_exception.txt | 64 ++++++++++++++++++++++++++++++++++++++ session_exception.txt | 44 ++++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 164 deletions(-) create mode 100644 api/netclient.py delete mode 100644 api/security.py delete mode 100644 api/tests.py create mode 100644 proxy_exception.txt create mode 100644 session_exception.txt diff --git a/api/main.py b/api/main.py index 86cace4..5397b6b 100644 --- a/api/main.py +++ b/api/main.py @@ -7,7 +7,6 @@ from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv -import security import transfer load_dotenv() @@ -25,7 +24,6 @@ app.add_middleware( @app.on_event('startup') async def startup_event(): """Read up the API server.""" - # await security.ip_protection_check() @app.get('/') async def root(): @@ -44,4 +42,4 @@ async def api_root(): 'status': 'ok', } -app.add_route('/{path:path}', transfer.transfer_streaming_response, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) +app.add_route('/{path:path}', transfer.handle_api_request, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) diff --git a/api/netclient.py b/api/netclient.py new file mode 100644 index 0000000..e8c8027 --- /dev/null +++ b/api/netclient.py @@ -0,0 +1,24 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +async def receive_target_stream(): + async with aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=int(os.getenv('TRANSFER_TIMEOUT', '120'))), + raise_for_status=False + ) as session: + async with session.request( + method=incoming_request.method, + url=target_url, + json=incoming_json_payload, + headers={ + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {os.getenv("CLOSEDAI_KEY")}' + } + ) as response: + async for chunk in response.content.iter_any(): + chunk = f'{chunk.decode("utf8")}\n\n' + + yield chunk diff --git a/api/proxies.py b/api/proxies.py index d97b817..7bdee24 100644 --- a/api/proxies.py +++ b/api/proxies.py @@ -3,6 +3,8 @@ import os import socket import asyncio +import aiohttp +import aiohttp_socks from dotenv import load_dotenv @@ -25,10 +27,51 @@ class Proxy: self.username = username self.password = password -active_proxy = Proxy( + async def initialize_connector(self, connector): + async with aiohttp.ClientSession( + connector=connector, + timeout=aiohttp.ClientTimeout(total=10), + raise_for_status=True + ) as session: + async with session.request( + method='get', + url='https://checkip.amazonaws.com', + headers={'Content-Type': 'application/json'} + ) as response: + detected_ip = await response.text() + print(f'Detected IP: {detected_ip}') + return detected_ip.strip() + + async def get_connector(self): + proxy_types = { + 'http': aiohttp_socks.ProxyType.HTTP, + 'https': aiohttp_socks.ProxyType.HTTP, + 'socks4': aiohttp_socks.ProxyType.SOCKS4, + 'socks5': aiohttp_socks.ProxyType.SOCKS5 + } + + connector = aiohttp_socks.ProxyConnector( + proxy_type=proxy_types[self.proxy_type], + host=self.ip_address, + port=self.port, + rdns=False, + username=self.username, + password=self.password + ) + + await self.initialize_connector(connector) + + # Logging to check the connector state + print("Connector: Is closed?", connector.closed) + print("Connector: Is connected?", connector._connected) + + return connector + + +default_proxy = Proxy( proxy_type=os.getenv('PROXY_TYPE', 'http'), host=os.getenv('PROXY_HOST', '127.0.0.1'), - port=int(os.getenv('PROXY_PORT', 8080)), + port=int(os.getenv('PROXY_PORT', '8080')), username=os.getenv('PROXY_USER'), password=os.getenv('PROXY_PASS') ) diff --git a/api/security.py b/api/security.py deleted file mode 100644 index 735d5d9..0000000 --- a/api/security.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Security checks for the API. Checks if the IP is masked etc.""" - -import os -import asyncio - -from rich import print -from tests import check_proxy -from dotenv import load_dotenv - -load_dotenv() -is_proxy_enabled = bool(os.getenv('PROXY_HOST', None)) - -class InsecureIPError(Exception): - """Raised when the IP address of the server is not secure.""" - -async def ip_protection_check(): - """Makes sure that the actual server IP address is not exposed to the public.""" - - if not is_proxy_enabled: - print('[yellow]WARN: The proxy is not enabled. \ -Skipping IP protection check.[/yellow]') - return True - - actual_ips = os.getenv('ACTUAL_IPS', '').split() - - if actual_ips: - # run the async function check_proxy() and get its result - response_ip = await check_proxy() - - for actual_ip in actual_ips: - if actual_ip in response_ip: - raise InsecureIPError(f'IP pattern "{actual_ip}" is in the values of ACTUAL_IPS of the\ -.env file. Enable a VPN or proxy to continue.') - - if is_proxy_enabled: - print(f'[green]GOOD: The IP "{response_ip}" was detected, which seems to be a proxy. Great![/green]') - return True - - else: - print('[yellow]WARN: ACTUAL_IPS is not set in the .env file or empty.\ -This means that the real IP of the server could be exposed. If you\'re using something\ -like Cloudflare or Repl.it, you can ignore this warning.[/yellow]') - -if __name__ == '__main__': - ip_protection_check() diff --git a/api/tests.py b/api/tests.py deleted file mode 100644 index fe4f6e0..0000000 --- a/api/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import httpx -import proxies -import asyncio -import aiohttp -import aiohttp_socks - -from rich import print -from dotenv import load_dotenv - -load_dotenv() - -async def check_proxy(): - """Checks if the proxy is working.""" - - proxy = proxies.active_proxy - - connector = aiohttp_socks.ProxyConnector( - proxy_type=aiohttp_socks.ProxyType.SOCKS5, - host=proxy.ip_address, - port=proxy.port, - rdns=False, - username=proxy.username, - password=proxy.password - ) - - async with aiohttp.ClientSession(connector=connector) as session: - async with session.get('https://echo.hoppscotch.io/') as response: - json_data = await response.json() - return json_data['headers']['x-forwarded-for'] - -async def check_api(): - """Checks if the API is working.""" - - model = 'gpt-3.5-turbo' - messages = [ - { - 'role': 'user', - 'content': '2+2=' - }, - ] - - headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + os.getenv('CLOSEDAI_KEY') - } - - json_data = { - 'model': model, - 'messages': messages, - 'stream': True - } - - async with httpx.AsyncClient(timeout=20) as client: - async with client.stream('POST', 'https://api.openai.com/v1/chat/completions', headers=headers, json=json_data) as response: - response.raise_for_status() - async for chunk in response.aiter_text(): - print(chunk.strip()) - -if __name__ == '__main__': - # asyncio.run(check_api()) - asyncio.run(check_proxy()) diff --git a/api/transfer.py b/api/transfer.py index d4a7bd0..5269477 100644 --- a/api/transfer.py +++ b/api/transfer.py @@ -2,17 +2,16 @@ import os import json -import logging import aiohttp -import aiohttp_socks +import logging +import starlette +import netclient import proxies from dotenv import load_dotenv -import starlette -from starlette.responses import StreamingResponse -from starlette.background import BackgroundTask +from starlette.background import StreamingResponse load_dotenv() @@ -32,61 +31,30 @@ EXCLUDED_HEADERS = [ 'connection' ] -proxy = proxies.active_proxy -proxy_connector = aiohttp_socks.ProxyConnector( - proxy_type=aiohttp_socks.ProxyType.SOCKS5, - host=proxy.ip_address, - port=proxy.port, - rdns=False, - username=proxy.username, - password=proxy.password -) - -async def transfer_streaming_response(incoming_request, target_endpoint: str='https://api.openai.com/v1'): +async def handle_api_request(incoming_request, target_endpoint: str=''): """Transfer a streaming response from the incoming request to the target endpoint""" + if not target_endpoint: + target_endpoint = os.getenv('CLOSEDAI_ENDPOINT') + + target_url = f'{target_endpoint}{incoming_request.url.path}'.replace('/v1/v1', '/v1') + logging.info('TRANSFER %s -> %s', incoming_request.url.path, target_url) + + if target_url.endswith('/v1'): + return starlette.responses.Response(status_code=200, content='200. /v1 is the API endpoint root path.') if incoming_request.headers.get('Authorization') != f'Bearer {os.getenv("DEMO_AUTH")}': - return starlette.responses.Response( - status_code=403, - content='Invalid API key' - ) + return starlette.responses.Response(status_code=403, content='Invalid API key') try: - incoming_json_payload = await incoming_request.json() + payload = await incoming_request.json() except json.decoder.JSONDecodeError: - incoming_json_payload = {} + payload = {} - async def receive_target_stream(): - connector = aiohttp_socks.ProxyConnector( - proxy_type=aiohttp_socks.ProxyType.SOCKS5, - host=proxy.ip_address, - port=proxy.port, - rdns=False, - username=proxy.username, - password=proxy.password - ) - async with aiohttp.ClientSession( - connector=connector, - timeout=aiohttp.ClientTimeout(total=int(os.getenv('TRANSFER_TIMEOUT', '120'))), - raise_for_status=True - ) as session: - target_url = f'{target_endpoint}{incoming_request.url.path}'.replace('/v1/v1', '/v1') - logging.info('TRANSFER %s -> %s', incoming_request.url.path, target_url) + target_provider = 'moe' - async with session.request( - method=incoming_request.method, - url=target_url, - json=incoming_json_payload, - headers={ - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {os.getenv("CLOSEDAI_KEY")}' - } - ) as response: - async for chunk in response.content.iter_any(): - chunk = f'{chunk.decode("utf8")}\n\n' - logging.debug(chunk) - yield chunk + if 'temperature' in payload or 'functions' in payload: + target_provider = 'closed' return StreamingResponse( - content=receive_target_stream() + content=netclient.receive_target_stream() ) diff --git a/proxy_exception.txt b/proxy_exception.txt new file mode 100644 index 0000000..3ed221b --- /dev/null +++ b/proxy_exception.txt @@ -0,0 +1,64 @@ +Traceback (most recent call last): + File "/usr/local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 429, in run_asgi + result = await app( # type: ignore[func-returns-value] + File "/usr/local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__ + return await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/fastapi/applications.py", line 284, in __call__ + await super().__call__(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__ + await self.middleware_stack(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__ + raise exc + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__ + await self.app(scope, receive, _send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__ + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__ + raise exc + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__ + await self.app(scope, receive, sender) + File "/usr/local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__ + raise e + File "/usr/local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__ + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__ + await route.handle(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 69, in app + await response(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 270, in __call__ + async with anyio.create_task_group() as task_group: + File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 662, in __aexit__ + raise exceptions[0] + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 273, in wrap + await func() + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 262, in stream_response + async for chunk in self.body_iterator: + File "/home/nova-api/api/transfer.py", line 62, in receive_target_stream + async with session.request( + File "/usr/local/lib/python3.10/site-packages/aiohttp/client.py", line 1138, in __aenter__ + self._resp = await self._coro + File "/usr/local/lib/python3.10/site-packages/aiohttp/client.py", line 535, in _request + conn = await self._connector.connect( + File "/usr/local/lib/python3.10/site-packages/aiohttp/connector.py", line 542, in connect + proto = await self._create_connection(req, traces, timeout) + File "/usr/local/lib/python3.10/site-packages/aiohttp/connector.py", line 907, in _create_connection + _, proto = await self._create_direct_connection(req, traces, timeout) + File "/usr/local/lib/python3.10/site-packages/aiohttp/connector.py", line 1175, in _create_direct_connection + transp, proto = await self._wrap_create_connection( + File "/usr/local/lib/python3.10/site-packages/aiohttp_socks/connector.py", line 72, in _wrap_create_connection + stream = await proxy.connect( + File "/usr/local/lib/python3.10/site-packages/python_socks/async_/asyncio/v2/_proxy.py", line 59, in connect + return await self._connect( + File "/usr/local/lib/python3.10/site-packages/python_socks/async_/asyncio/v2/_proxy.py", line 100, in _connect + await self._negotiate( + File "/usr/local/lib/python3.10/site-packages/python_socks/async_/asyncio/v2/_proxy.py", line 164, in _negotiate + await proto.negotiate() + File "/usr/local/lib/python3.10/site-packages/python_socks/_proto/socks5_async.py", line 42, in negotiate + await self._socks_auth() + File "/usr/local/lib/python3.10/site-packages/python_socks/_proto/socks5_async.py", line 55, in _socks_auth + res.validate() + File "/usr/local/lib/python3.10/site-packages/python_socks/_proto/socks5.py", line 134, in validate + raise ProxyError('Username and password ' # pragma: no cover +python_socks._errors.ProxyError: Username and password authentication failure diff --git a/session_exception.txt b/session_exception.txt new file mode 100644 index 0000000..5669224 --- /dev/null +++ b/session_exception.txt @@ -0,0 +1,44 @@ +Traceback (most recent call last): + File "/usr/local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 429, in run_asgi + result = await app( # type: ignore[func-returns-value] + File "/usr/local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__ + return await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/fastapi/applications.py", line 284, in __call__ + await super().__call__(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__ + await self.middleware_stack(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__ + raise exc + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__ + await self.app(scope, receive, _send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__ + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__ + raise exc + File "/usr/local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__ + await self.app(scope, receive, sender) + File "/usr/local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__ + raise e + File "/usr/local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__ + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__ + await route.handle(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle + await self.app(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 69, in app + await response(scope, receive, send) + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 270, in __call__ + async with anyio.create_task_group() as task_group: + File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 662, in __aexit__ + raise exceptions[0] + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 273, in wrap + await func() + File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 262, in stream_response + async for chunk in self.body_iterator: + File "/home/nova-api/api/transfer.py", line 60, in receive_target_stream + async with session.request( + File "/usr/local/lib/python3.10/site-packages/aiohttp/client.py", line 1138, in __aenter__ + self._resp = await self._coro + File "/usr/local/lib/python3.10/site-packages/aiohttp/client.py", line 399, in _request + raise RuntimeError("Session is closed") +RuntimeError: Session is closed \ No newline at end of file