AlexRyder commited on
Commit
2da028e
1 Parent(s): 76ef7bf

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +22 -0
  2. connection_manager.py +24 -0
  3. main.py +78 -0
  4. request_forwarder_classes.py +129 -0
  5. 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