Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- Dockerfile +22 -0
- connection_manager.py +24 -0
- main.py +78 -0
- request_forwarder_classes.py +129 -0
- requirements.txt +6 -0
Dockerfile
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11-slim
|
2 |
+
|
3 |
+
RUN useradd -m -u 1000 user && \
|
4 |
+
mkdir /home/user/app && \
|
5 |
+
chown -R user:user /home/user/app
|
6 |
+
|
7 |
+
WORKDIR /home/user/app
|
8 |
+
|
9 |
+
RUN apt-get update && \
|
10 |
+
apt-get install -y && \
|
11 |
+
rm -rf /var/lib/apt/lists/*
|
12 |
+
|
13 |
+
COPY --chown=user:user ./requirements.txt ./
|
14 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
15 |
+
|
16 |
+
USER user
|
17 |
+
ENV HOME=/home/user \
|
18 |
+
PATH=/home/user/.local/bin:$PATH
|
19 |
+
|
20 |
+
COPY --chown=user:user . .
|
21 |
+
|
22 |
+
CMD ["python", "main.py"]
|
connection_manager.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
|
3 |
+
from fastapi import WebSocket, WebSocketException
|
4 |
+
|
5 |
+
|
6 |
+
class ConnectionManager:
|
7 |
+
def __init__(self, password: str) -> None:
|
8 |
+
self.__password: str = password
|
9 |
+
self.__active_connections: List[WebSocket] = list[WebSocket]()
|
10 |
+
|
11 |
+
async def connect(self, websocket: WebSocket) -> None:
|
12 |
+
authorization: str = websocket.headers.get("Authorization")
|
13 |
+
if not authorization:
|
14 |
+
raise WebSocketException(code=1008, reason="Authorization header missing")
|
15 |
+
token_type, _, token = authorization.partition(' ')
|
16 |
+
if token_type != "Bearer" or not token:
|
17 |
+
raise WebSocketException(code=1008, reason="Invalid authorization header format")
|
18 |
+
if token != self.__password:
|
19 |
+
raise WebSocketException(code=1008, reason="Invalid token")
|
20 |
+
await websocket.accept()
|
21 |
+
self.__active_connections.append(websocket)
|
22 |
+
|
23 |
+
def disconnect(self, websocket: WebSocket) -> None:
|
24 |
+
self.__active_connections.remove(websocket)
|
main.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import uvicorn
|
3 |
+
from fastapi import FastAPI, Request, HTTPException, status, WebSocketDisconnect
|
4 |
+
from fastapi.applications import AppType
|
5 |
+
from fastapi.responses import JSONResponse
|
6 |
+
from requests import Timeout, TooManyRedirects, RequestException, Session
|
7 |
+
|
8 |
+
from connection_manager import *
|
9 |
+
from request_forwarder_classes import *
|
10 |
+
|
11 |
+
PORT: int = int(os.getenv("PORT"))
|
12 |
+
PASSWORD: str = os.getenv("PASSWORD")
|
13 |
+
|
14 |
+
app: AppType = FastAPI()
|
15 |
+
connection_manager = ConnectionManager(PASSWORD)
|
16 |
+
|
17 |
+
|
18 |
+
def __get_response(forwarded_request: ForwardedRequest, session: Session | None = None) -> Tuple[int, Dict]:
|
19 |
+
try:
|
20 |
+
if not session:
|
21 |
+
session = Session()
|
22 |
+
response = session.request(
|
23 |
+
forwarded_request.method,
|
24 |
+
forwarded_request.url,
|
25 |
+
headers=forwarded_request.headers,
|
26 |
+
data=forwarded_request.data,
|
27 |
+
params=forwarded_request.params,
|
28 |
+
auth=forwarded_request.auth,
|
29 |
+
cookies=forwarded_request.cookies,
|
30 |
+
json=forwarded_request.json)
|
31 |
+
forwarded_response = ForwardedResponse.from_response(response, forwarded_request.response_content_type)
|
32 |
+
return status.HTTP_200_OK, {"response": forwarded_response.to_json_dict(), "error": None}
|
33 |
+
except ConnectionError as e:
|
34 |
+
return status.HTTP_418_IM_A_TEAPOT, {"response": None, "error": {"ConnectionError": str(e)}}
|
35 |
+
except Timeout as e:
|
36 |
+
return status.HTTP_418_IM_A_TEAPOT, {"response": None, "error": {"Timeout": str(e)}}
|
37 |
+
except TooManyRedirects as e:
|
38 |
+
return status.HTTP_418_IM_A_TEAPOT, {"response": None, "error": {"TooManyRedirects": str(e)}}
|
39 |
+
except RequestException as e:
|
40 |
+
return status.HTTP_418_IM_A_TEAPOT, {"response": None, "error": {"RequestException": str(e)}}
|
41 |
+
except Exception as e:
|
42 |
+
return status.HTTP_418_IM_A_TEAPOT, {"response": None, "error": str(e)}
|
43 |
+
|
44 |
+
|
45 |
+
@app.websocket("/ws/forward-requests")
|
46 |
+
async def websocket_endpoint(websocket: WebSocket):
|
47 |
+
await connection_manager.connect(websocket)
|
48 |
+
session = Session()
|
49 |
+
try:
|
50 |
+
while True:
|
51 |
+
request = await websocket.receive_json()
|
52 |
+
print(request)
|
53 |
+
forwarded_request = ForwardedRequest.from_json(request)
|
54 |
+
response_status, response = __get_response(forwarded_request, session)
|
55 |
+
await websocket.send_json(response)
|
56 |
+
except WebSocketDisconnect:
|
57 |
+
connection_manager.disconnect(websocket)
|
58 |
+
|
59 |
+
|
60 |
+
@app.post("/forward-request")
|
61 |
+
async def forward_request(forwarded_request: ForwardedRequest, request: Request):
|
62 |
+
authorization: str = request.headers.get("Authorization")
|
63 |
+
if not authorization:
|
64 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header missing")
|
65 |
+
|
66 |
+
token_type, _, token = authorization.partition(' ')
|
67 |
+
if token_type != "Bearer" or not token:
|
68 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authorization header format")
|
69 |
+
|
70 |
+
if token != PASSWORD:
|
71 |
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token")
|
72 |
+
|
73 |
+
response_status, response = __get_response(forwarded_request)
|
74 |
+
return JSONResponse(content=response, status_code=response_status)
|
75 |
+
|
76 |
+
|
77 |
+
if __name__ == '__main__':
|
78 |
+
uvicorn.run("main:app", host="0.0.0.0", port=PORT)
|
request_forwarder_classes.py
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import datetime
|
2 |
+
from typing import Any, Dict, Tuple, Literal
|
3 |
+
|
4 |
+
import requests
|
5 |
+
from pydantic import BaseModel
|
6 |
+
|
7 |
+
RequestMethod = Literal['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH']
|
8 |
+
ResponseType = Literal['raw', 'text', 'json']
|
9 |
+
|
10 |
+
|
11 |
+
class ForwardedRequest(BaseModel):
|
12 |
+
method: RequestMethod
|
13 |
+
url: str
|
14 |
+
headers: Dict[str, str] | None = None
|
15 |
+
data: Dict[str, Any] | Tuple[Tuple[str, Any]] | str | bytes | None = None
|
16 |
+
params: Dict[str, Any] | Tuple[Tuple[str, Any]] | str | None = None
|
17 |
+
auth: Tuple[str, str] | None = None
|
18 |
+
cookies: Dict[str, str] | None = None
|
19 |
+
json: Any | None = None
|
20 |
+
response_content_type: ResponseType = 'text'
|
21 |
+
|
22 |
+
@staticmethod
|
23 |
+
def from_request(request: requests.Request, response_content_type: ResponseType = 'text') -> 'ForwardedRequest':
|
24 |
+
return ForwardedRequest(
|
25 |
+
method=request.method,
|
26 |
+
url=request.url,
|
27 |
+
headers=request.headers,
|
28 |
+
data=request.data,
|
29 |
+
params=request.params,
|
30 |
+
auth=request.auth,
|
31 |
+
cookies=request.cookies,
|
32 |
+
json=request.json,
|
33 |
+
response_content_type=response_content_type
|
34 |
+
)
|
35 |
+
|
36 |
+
@staticmethod
|
37 |
+
def from_json(json_dict: Dict[str, Any]) -> 'ForwardedRequest':
|
38 |
+
return ForwardedRequest(
|
39 |
+
method=json_dict['method'],
|
40 |
+
url=json_dict['url'],
|
41 |
+
headers=json_dict['headers'],
|
42 |
+
data=json_dict['data'],
|
43 |
+
params=json_dict['params'],
|
44 |
+
auth=json_dict['auth'],
|
45 |
+
cookies=json_dict['cookies'],
|
46 |
+
json=json_dict['json'],
|
47 |
+
response_content_type=json_dict['response_content_type'],
|
48 |
+
)
|
49 |
+
|
50 |
+
def to_json_dict(self):
|
51 |
+
return {
|
52 |
+
"method": self.method,
|
53 |
+
"url": self.url,
|
54 |
+
"headers": self.headers,
|
55 |
+
"data": self.data,
|
56 |
+
"params": self.params,
|
57 |
+
"auth": self.auth,
|
58 |
+
"cookies": self.cookies,
|
59 |
+
"json": self.json,
|
60 |
+
"response_content_type": self.response_content_type
|
61 |
+
}
|
62 |
+
|
63 |
+
|
64 |
+
class ForwardedResponse(BaseModel):
|
65 |
+
status_code: int | None = None
|
66 |
+
headers: Dict[str, Any] = dict[str, Any]()
|
67 |
+
url: str | None = None
|
68 |
+
encoding: str | None = None
|
69 |
+
reason: str | None = None
|
70 |
+
cookies: Dict[str, Any] = dict[str, Any]()
|
71 |
+
elapsed: datetime.timedelta = datetime.timedelta(0)
|
72 |
+
content: Any | None
|
73 |
+
content_type: ResponseType = 'text'
|
74 |
+
|
75 |
+
@staticmethod
|
76 |
+
def from_response(response: requests.Response, response_content_type: ResponseType = 'text') -> 'ForwardedResponse':
|
77 |
+
forwarded_response = ForwardedResponse(
|
78 |
+
status_code=response.status_code,
|
79 |
+
headers=response.headers,
|
80 |
+
url=response.url,
|
81 |
+
encoding=response.encoding,
|
82 |
+
reason=response.reason,
|
83 |
+
cookies=response.cookies,
|
84 |
+
elapsed=response.elapsed,
|
85 |
+
content=None,
|
86 |
+
content_type=response_content_type
|
87 |
+
)
|
88 |
+
match forwarded_response.content_type:
|
89 |
+
case 'raw':
|
90 |
+
forwarded_response.content = response.content
|
91 |
+
case 'text':
|
92 |
+
forwarded_response.content = response.text
|
93 |
+
case 'json':
|
94 |
+
forwarded_response.content = response.json()
|
95 |
+
return forwarded_response
|
96 |
+
|
97 |
+
@staticmethod
|
98 |
+
def from_json(json_dict: Dict[str, Any]) -> 'ForwardedResponse':
|
99 |
+
return ForwardedResponse(
|
100 |
+
status_code=json_dict['status_code'],
|
101 |
+
headers=json_dict['headers'],
|
102 |
+
url=json_dict['url'],
|
103 |
+
encoding=json_dict['encoding'],
|
104 |
+
reason=json_dict['reason'],
|
105 |
+
cookies=json_dict['cookies'],
|
106 |
+
elapsed=json_dict['elapsed'],
|
107 |
+
content=json_dict['content'],
|
108 |
+
content_type=json_dict['content_type'],
|
109 |
+
)
|
110 |
+
|
111 |
+
def to_json_dict(self):
|
112 |
+
def serialize_timedelta(timedelta: datetime.timedelta):
|
113 |
+
return {
|
114 |
+
'days': timedelta.days,
|
115 |
+
'seconds': timedelta.seconds,
|
116 |
+
'microseconds': timedelta.microseconds
|
117 |
+
}
|
118 |
+
|
119 |
+
return {
|
120 |
+
"status_code": self.status_code,
|
121 |
+
"headers": self.headers,
|
122 |
+
"url": self.url,
|
123 |
+
"encoding": self.encoding,
|
124 |
+
"reason": self.reason,
|
125 |
+
"cookies": self.cookies,
|
126 |
+
"elapsed": serialize_timedelta(self.elapsed),
|
127 |
+
"content": self.content,
|
128 |
+
"content_type": self.content_type,
|
129 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
uvicorn[standard]
|
2 |
+
fastapi
|
3 |
+
requests
|
4 |
+
pydantic
|
5 |
+
websocket-client
|
6 |
+
wsproto
|