firegex 3.1.0__tar.gz → 3.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {firegex-3.1.0/firegex.egg-info → firegex-3.2.1}/PKG-INFO +4 -1
- firegex-3.2.1/firegex/__init__.py +5 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/__init__.py +6 -2
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/data.py +1 -0
- firegex-3.2.1/firegex/nfproxy/models/http.py +622 -0
- {firegex-3.1.0 → firegex-3.2.1/firegex.egg-info}/PKG-INFO +4 -1
- {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/requires.txt +3 -0
- firegex-3.2.1/requirements.txt +9 -0
- {firegex-3.1.0 → firegex-3.2.1}/setup.py +1 -1
- firegex-3.1.0/firegex/__init__.py +0 -5
- firegex-3.1.0/firegex/nfproxy/models/http.py +0 -422
- firegex-3.1.0/requirements.txt +0 -6
- {firegex-3.1.0 → firegex-3.2.1}/MANIFEST.in +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/README.md +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/fgex +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/__main__.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/cli.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/__init__.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/exceptions.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/models.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/models/__init__.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/models/tcp.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/proxysim/__init__.py +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/SOURCES.txt +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/dependency_links.txt +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/top_level.txt +0 -0
- {firegex-3.1.0 → firegex-3.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: firegex
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: Firegex client
|
|
5
5
|
Home-page: https://github.com/pwnzer0tt1/firegex
|
|
6
6
|
Author: Pwnzer0tt1
|
|
@@ -13,8 +13,11 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
Requires-Dist: typer==0.15.2
|
|
14
14
|
Requires-Dist: pydantic>=2
|
|
15
15
|
Requires-Dist: typing-extensions>=4.7.1
|
|
16
|
+
Requires-Dist: zstd
|
|
17
|
+
Requires-Dist: brotli
|
|
16
18
|
Requires-Dist: watchfiles
|
|
17
19
|
Requires-Dist: fgex
|
|
20
|
+
Requires-Dist: websockets
|
|
18
21
|
Requires-Dist: pyllhttp
|
|
19
22
|
Dynamic: author
|
|
20
23
|
Dynamic: author-email
|
|
@@ -121,8 +121,7 @@ def handle_packet(glob: dict) -> None:
|
|
|
121
121
|
new_params = params.copy()
|
|
122
122
|
for ele in params[i]:
|
|
123
123
|
new_params[i] = ele
|
|
124
|
-
|
|
125
|
-
yield ele
|
|
124
|
+
yield from try_to_call(new_params)
|
|
126
125
|
is_base_call = False
|
|
127
126
|
break
|
|
128
127
|
if is_base_call:
|
|
@@ -166,4 +165,9 @@ def compile(glob:dict) -> None:
|
|
|
166
165
|
internal_data.invalid_encoding_action = glob["FGEX_INVALID_ENCODING_ACTION"]
|
|
167
166
|
|
|
168
167
|
PacketHandlerResult(glob).reset_result()
|
|
168
|
+
|
|
169
|
+
def fake_exit(*_a, **_k):
|
|
170
|
+
print("WARNING: This function should not be called", flush=True)
|
|
171
|
+
|
|
172
|
+
glob["exit"] = fake_exit
|
|
169
173
|
|
|
@@ -106,6 +106,7 @@ class DataStreamCtx:
|
|
|
106
106
|
self.__data = glob["__firegex_pyfilter_ctx"]
|
|
107
107
|
self.filter_glob = glob
|
|
108
108
|
self.current_pkt = RawPacket._fetch_packet(self) if init_pkt else None
|
|
109
|
+
self.call_mem = {} #A memory space valid only for the current packet handler
|
|
109
110
|
|
|
110
111
|
@property
|
|
111
112
|
def filter_call_info(self) -> list[FilterHandler]:
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import pyllhttp
|
|
2
|
+
from firegex.nfproxy.internals.exceptions import NotReadyToRun
|
|
3
|
+
from firegex.nfproxy.internals.data import DataStreamCtx
|
|
4
|
+
from firegex.nfproxy.internals.exceptions import StreamFullDrop, StreamFullReject, RejectConnection, DropPacket
|
|
5
|
+
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from collections import deque
|
|
8
|
+
from zstd import ZSTD_uncompress
|
|
9
|
+
import gzip
|
|
10
|
+
import io
|
|
11
|
+
import zlib
|
|
12
|
+
import brotli
|
|
13
|
+
import traceback
|
|
14
|
+
from websockets.frames import Frame
|
|
15
|
+
from websockets.extensions.permessage_deflate import PerMessageDeflate
|
|
16
|
+
from pyllhttp import PAUSED_H2_UPGRADE, PAUSED_UPGRADE
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class InternalHTTPMessage:
|
|
20
|
+
"""Internal class to handle HTTP messages"""
|
|
21
|
+
url: str|None = field(default=None)
|
|
22
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
23
|
+
lheaders: dict[str, str] = field(default_factory=dict) # lowercase copy of the headers
|
|
24
|
+
body: bytes|None = field(default=None)
|
|
25
|
+
body_decoded: bool = field(default=False)
|
|
26
|
+
headers_complete: bool = field(default=False)
|
|
27
|
+
message_complete: bool = field(default=False)
|
|
28
|
+
status: str|None = field(default=None)
|
|
29
|
+
total_size: int = field(default=0)
|
|
30
|
+
user_agent: str = field(default_factory=str)
|
|
31
|
+
content_encoding: str = field(default=str)
|
|
32
|
+
content_type: str = field(default=str)
|
|
33
|
+
keep_alive: bool = field(default=False)
|
|
34
|
+
should_upgrade: bool = field(default=False)
|
|
35
|
+
http_version: str = field(default=str)
|
|
36
|
+
method: str = field(default=str)
|
|
37
|
+
content_length: int = field(default=0)
|
|
38
|
+
stream: bytes = field(default_factory=bytes)
|
|
39
|
+
ws_stream: list[Frame] = field(default_factory=list) # Decoded websocket stream
|
|
40
|
+
upgrading_to_h2: bool = field(default=False)
|
|
41
|
+
upgrading_to_ws: bool = field(default=False)
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class InternalHttpBuffer:
|
|
45
|
+
"""Internal class to handle HTTP messages"""
|
|
46
|
+
_url_buffer: bytes = field(default_factory=bytes)
|
|
47
|
+
_raw_header_fields: dict[str, str|list[str]] = field(default_factory=dict)
|
|
48
|
+
_header_fields: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
_body_buffer: bytes = field(default_factory=bytes)
|
|
50
|
+
_status_buffer: bytes = field(default_factory=bytes)
|
|
51
|
+
_current_header_field: bytes = field(default_factory=bytes)
|
|
52
|
+
_current_header_value: bytes = field(default_factory=bytes)
|
|
53
|
+
_ws_packet_stream: bytes = field(default_factory=bytes)
|
|
54
|
+
|
|
55
|
+
class InternalCallbackHandler():
|
|
56
|
+
|
|
57
|
+
buffers = InternalHttpBuffer()
|
|
58
|
+
msg = InternalHTTPMessage()
|
|
59
|
+
save_body = True
|
|
60
|
+
raised_error = False
|
|
61
|
+
has_begun = False
|
|
62
|
+
messages: deque[InternalHTTPMessage] = deque()
|
|
63
|
+
_ws_extentions = None
|
|
64
|
+
_ws_raised_error = False
|
|
65
|
+
|
|
66
|
+
def reset_data(self):
|
|
67
|
+
self.msg = InternalHTTPMessage()
|
|
68
|
+
self.buffers = InternalHttpBuffer()
|
|
69
|
+
self.messages.clear()
|
|
70
|
+
|
|
71
|
+
def on_message_begin(self):
|
|
72
|
+
self.buffers = InternalHttpBuffer()
|
|
73
|
+
self.msg = InternalHTTPMessage()
|
|
74
|
+
self.has_begun = True
|
|
75
|
+
|
|
76
|
+
def on_url(self, url):
|
|
77
|
+
self.buffers._url_buffer += url
|
|
78
|
+
self.msg.total_size += len(url)
|
|
79
|
+
|
|
80
|
+
def on_url_complete(self):
|
|
81
|
+
self.msg.url = self.buffers._url_buffer.decode(errors="ignore")
|
|
82
|
+
self.buffers._url_buffer = b""
|
|
83
|
+
|
|
84
|
+
def on_status(self, status: bytes):
|
|
85
|
+
self.msg.total_size += len(status)
|
|
86
|
+
self.buffers._status_buffer += status
|
|
87
|
+
|
|
88
|
+
def on_status_complete(self):
|
|
89
|
+
self.msg.status = self.buffers._status_buffer.decode(errors="ignore")
|
|
90
|
+
self.buffers._status_buffer = b""
|
|
91
|
+
|
|
92
|
+
def on_header_field(self, field):
|
|
93
|
+
self.msg.total_size += len(field)
|
|
94
|
+
self.buffers._current_header_field += field
|
|
95
|
+
|
|
96
|
+
def on_header_field_complete(self):
|
|
97
|
+
pass # Nothing to do
|
|
98
|
+
|
|
99
|
+
def on_header_value(self, value):
|
|
100
|
+
self.msg.total_size += len(value)
|
|
101
|
+
self.buffers._current_header_value += value
|
|
102
|
+
|
|
103
|
+
def on_header_value_complete(self):
|
|
104
|
+
if self.buffers._current_header_field:
|
|
105
|
+
k, v = self.buffers._current_header_field.decode(errors="ignore"), self.buffers._current_header_value.decode(errors="ignore")
|
|
106
|
+
old_value = self.buffers._raw_header_fields.get(k, None)
|
|
107
|
+
|
|
108
|
+
# raw headers are stored as thay were, considering to check changes between headers encoding
|
|
109
|
+
if isinstance(old_value, list):
|
|
110
|
+
old_value.append(v)
|
|
111
|
+
elif isinstance(old_value, str):
|
|
112
|
+
self.buffers._raw_header_fields[k] = [old_value, v]
|
|
113
|
+
else:
|
|
114
|
+
self.buffers._raw_header_fields[k] = v
|
|
115
|
+
|
|
116
|
+
# Decoding headers normally
|
|
117
|
+
kl = k.lower()
|
|
118
|
+
if kl in self.buffers._header_fields:
|
|
119
|
+
self.buffers._header_fields[kl] += f", {v}" # Should be considered as a single list separated by commas as said in the RFC
|
|
120
|
+
else:
|
|
121
|
+
self.buffers._header_fields[kl] = v
|
|
122
|
+
|
|
123
|
+
self.buffers._current_header_field = b""
|
|
124
|
+
self.buffers._current_header_value = b""
|
|
125
|
+
|
|
126
|
+
def on_headers_complete(self):
|
|
127
|
+
self.msg.headers = self.buffers._raw_header_fields
|
|
128
|
+
self.msg.lheaders = self.buffers._header_fields
|
|
129
|
+
self.buffers._raw_header_fields = {}
|
|
130
|
+
self.buffers._current_header_field = b""
|
|
131
|
+
self.buffers._current_header_value = b""
|
|
132
|
+
self.msg.headers_complete = True
|
|
133
|
+
self.msg.method = self.method_parsed
|
|
134
|
+
self.msg.content_length = self.content_length_parsed
|
|
135
|
+
self.msg.should_upgrade = self.should_upgrade
|
|
136
|
+
self.msg.keep_alive = self.keep_alive
|
|
137
|
+
self.msg.http_version = self.http_version
|
|
138
|
+
self.msg.content_type = self.content_type
|
|
139
|
+
self.msg.content_encoding = self.content_encoding
|
|
140
|
+
self.msg.user_agent = self.user_agent
|
|
141
|
+
|
|
142
|
+
def on_body(self, body: bytes):
|
|
143
|
+
if self.save_body:
|
|
144
|
+
self.msg.total_size += len(body)
|
|
145
|
+
self.buffers._body_buffer += body
|
|
146
|
+
|
|
147
|
+
def on_message_complete(self):
|
|
148
|
+
self.msg.body = self.buffers._body_buffer
|
|
149
|
+
self.msg.should_upgrade = self.should_upgrade
|
|
150
|
+
self.buffers._body_buffer = b""
|
|
151
|
+
encodings = [ele.strip() for ele in self.content_encoding.lower().split(",")]
|
|
152
|
+
decode_success = True
|
|
153
|
+
decoding_body = self.msg.body
|
|
154
|
+
for enc in reversed(encodings):
|
|
155
|
+
if not enc:
|
|
156
|
+
continue
|
|
157
|
+
if enc == "deflate":
|
|
158
|
+
try:
|
|
159
|
+
decompress = zlib.decompressobj(-zlib.MAX_WBITS)
|
|
160
|
+
decoding_body = decompress.decompress(decoding_body)
|
|
161
|
+
decoding_body += decompress.flush()
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"Error decompressing deflate: {e}: skipping", flush=True)
|
|
164
|
+
decode_success = False
|
|
165
|
+
break
|
|
166
|
+
elif enc == "br":
|
|
167
|
+
try:
|
|
168
|
+
decoding_body = brotli.decompress(decoding_body)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Error decompressing brotli: {e}: skipping", flush=True)
|
|
171
|
+
decode_success = False
|
|
172
|
+
break
|
|
173
|
+
elif enc == "gzip" or enc == "x-gzip": #https://datatracker.ietf.org/doc/html/rfc2616#section-3.5
|
|
174
|
+
try:
|
|
175
|
+
if "gzip" in self.content_encoding.lower():
|
|
176
|
+
with gzip.GzipFile(fileobj=io.BytesIO(decoding_body)) as f:
|
|
177
|
+
decoding_body = f.read()
|
|
178
|
+
except Exception as e:
|
|
179
|
+
print(f"Error decompressing gzip: {e}: skipping", flush=True)
|
|
180
|
+
decode_success = False
|
|
181
|
+
break
|
|
182
|
+
elif enc == "zstd":
|
|
183
|
+
try:
|
|
184
|
+
decoding_body = ZSTD_uncompress(decoding_body)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Error decompressing zstd: {e}: skipping", flush=True)
|
|
187
|
+
decode_success = False
|
|
188
|
+
break
|
|
189
|
+
elif enc == "identity":
|
|
190
|
+
pass # No need to do anything https://datatracker.ietf.org/doc/html/rfc2616#section-3.5 (it's possible to be found also if it should't be used)
|
|
191
|
+
else:
|
|
192
|
+
decode_success = False
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if decode_success:
|
|
196
|
+
self.msg.body = decoding_body
|
|
197
|
+
self.msg.body_decoded = True
|
|
198
|
+
|
|
199
|
+
self.msg.message_complete = True
|
|
200
|
+
self.has_begun = False
|
|
201
|
+
if not self._packet_to_stream():
|
|
202
|
+
self.messages.append(self.msg)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def user_agent(self) -> str:
|
|
206
|
+
return self.msg.lheaders.get("user-agent", "")
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def content_encoding(self) -> str:
|
|
210
|
+
return self.msg.lheaders.get("content-encoding", "")
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def content_type(self) -> str:
|
|
214
|
+
return self.msg.lheaders.get("content-type", "")
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def keep_alive(self) -> bool:
|
|
218
|
+
return self.should_keep_alive
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def should_upgrade(self) -> bool:
|
|
222
|
+
return self.is_upgrading
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def http_version(self) -> str:
|
|
226
|
+
if self.major and self.minor:
|
|
227
|
+
return f"{self.major}.{self.minor}"
|
|
228
|
+
else:
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def method_parsed(self) -> str:
|
|
233
|
+
return self.method
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def total_size(self) -> int:
|
|
237
|
+
"""Total size used by the parser"""
|
|
238
|
+
tot = self.msg.total_size
|
|
239
|
+
for msg in self.messages:
|
|
240
|
+
tot += msg.total_size
|
|
241
|
+
return tot
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def content_length_parsed(self) -> int:
|
|
245
|
+
return self.content_length
|
|
246
|
+
|
|
247
|
+
def _is_input(self) -> bool:
|
|
248
|
+
raise NotImplementedError()
|
|
249
|
+
|
|
250
|
+
def _packet_to_stream(self):
|
|
251
|
+
return self.should_upgrade and self.save_body
|
|
252
|
+
|
|
253
|
+
def _stream_parser(self, data: bytes):
|
|
254
|
+
if self.msg.upgrading_to_ws:
|
|
255
|
+
if self._ws_raised_error:
|
|
256
|
+
self.msg.stream += data
|
|
257
|
+
self.msg.total_size += len(data)
|
|
258
|
+
return
|
|
259
|
+
self.buffers._ws_packet_stream += data
|
|
260
|
+
while True:
|
|
261
|
+
try:
|
|
262
|
+
new_frame, self.buffers._ws_packet_stream = self._parse_websocket_frame(self.buffers._ws_packet_stream)
|
|
263
|
+
except Exception:
|
|
264
|
+
print("[WARNING] Websocket parsing failed, passing data to stream...", flush=True)
|
|
265
|
+
traceback.print_exc()
|
|
266
|
+
self._ws_raised_error = True
|
|
267
|
+
self.msg.stream += self.buffers._ws_packet_stream
|
|
268
|
+
self.buffers._ws_packet_stream = b""
|
|
269
|
+
self.msg.total_size += len(data)
|
|
270
|
+
return
|
|
271
|
+
if new_frame is None:
|
|
272
|
+
break
|
|
273
|
+
self.msg.ws_stream.append(new_frame)
|
|
274
|
+
self.msg.total_size += len(new_frame.data)
|
|
275
|
+
if self.msg.upgrading_to_h2:
|
|
276
|
+
self.msg.total_size += len(data)
|
|
277
|
+
self.msg.stream += data
|
|
278
|
+
|
|
279
|
+
def _parse_websocket_ext(self):
|
|
280
|
+
ext_ws = []
|
|
281
|
+
req_ext = []
|
|
282
|
+
for ele in self.msg.lheaders.get("sec-websocket-extensions", "").split(","):
|
|
283
|
+
for xt in ele.split(";"):
|
|
284
|
+
req_ext.append(xt.strip().lower())
|
|
285
|
+
|
|
286
|
+
for ele in req_ext:
|
|
287
|
+
if ele == "permessage-deflate":
|
|
288
|
+
ext_ws.append(PerMessageDeflate(False, False, 15, 15))
|
|
289
|
+
return ext_ws
|
|
290
|
+
|
|
291
|
+
def _parse_websocket_frame(self, data: bytes) -> tuple[Frame|None, bytes]:
|
|
292
|
+
if self._ws_extentions is None:
|
|
293
|
+
if self._is_input():
|
|
294
|
+
self._ws_extentions = [] # Fallback to no options
|
|
295
|
+
else:
|
|
296
|
+
self._ws_extentions = self._parse_websocket_ext() # Extentions used are choosen by the server response
|
|
297
|
+
read_buffering = bytearray()
|
|
298
|
+
def read_exact(n: int):
|
|
299
|
+
nonlocal read_buffering
|
|
300
|
+
buffer = bytearray(read_buffering)
|
|
301
|
+
while len(buffer) < n:
|
|
302
|
+
data = yield
|
|
303
|
+
if data is None:
|
|
304
|
+
raise RuntimeError("Should not send None to this generator")
|
|
305
|
+
buffer.extend(data)
|
|
306
|
+
new_data = bytes(buffer[:n])
|
|
307
|
+
read_buffering = buffer[n:]
|
|
308
|
+
return new_data
|
|
309
|
+
|
|
310
|
+
parsing = Frame.parse(read_exact, extensions=self._ws_extentions, mask=self._is_input())
|
|
311
|
+
parsing.send(None)
|
|
312
|
+
try:
|
|
313
|
+
parsing.send(bytearray(data))
|
|
314
|
+
except StopIteration as e:
|
|
315
|
+
return e.value, read_buffering
|
|
316
|
+
|
|
317
|
+
return None, read_buffering
|
|
318
|
+
|
|
319
|
+
def parse_data(self, data: bytes):
|
|
320
|
+
if self._packet_to_stream(): # This is a websocket upgrade!
|
|
321
|
+
self._stream_parser(data)
|
|
322
|
+
else:
|
|
323
|
+
try:
|
|
324
|
+
reason, consumed = self.execute(data)
|
|
325
|
+
if reason == PAUSED_UPGRADE:
|
|
326
|
+
self.msg.upgrading_to_ws = True
|
|
327
|
+
self.msg.message_complete = True
|
|
328
|
+
self._stream_parser(data[consumed:])
|
|
329
|
+
elif reason == PAUSED_H2_UPGRADE:
|
|
330
|
+
self.msg.upgrading_to_h2 = True
|
|
331
|
+
self.msg.message_complete = True
|
|
332
|
+
self._stream_parser(data[consumed:])
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.raised_error = True
|
|
335
|
+
raise e
|
|
336
|
+
|
|
337
|
+
def pop_message(self):
|
|
338
|
+
return self.messages.popleft()
|
|
339
|
+
|
|
340
|
+
def __repr__(self):
|
|
341
|
+
return f"<InternalCallbackHandler msg={self.msg} buffers={self.buffers} save_body={self.save_body} raised_error={self.raised_error} has_begun={self.has_begun} messages={self.messages}>"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class InternalHttpRequest(InternalCallbackHandler, pyllhttp.Request):
|
|
345
|
+
def __init__(self):
|
|
346
|
+
super(InternalCallbackHandler, self).__init__()
|
|
347
|
+
super(pyllhttp.Request, self).__init__()
|
|
348
|
+
|
|
349
|
+
def _is_input(self):
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
class InternalHttpResponse(InternalCallbackHandler, pyllhttp.Response):
|
|
353
|
+
def __init__(self):
|
|
354
|
+
super(InternalCallbackHandler, self).__init__()
|
|
355
|
+
super(pyllhttp.Response, self).__init__()
|
|
356
|
+
|
|
357
|
+
def _is_input(self):
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
class InternalBasicHttpMetaClass:
|
|
361
|
+
"""Internal class to handle HTTP requests and responses"""
|
|
362
|
+
|
|
363
|
+
def __init__(self, parser: InternalHttpRequest|InternalHttpResponse, msg: InternalHTTPMessage):
|
|
364
|
+
self._parser = parser
|
|
365
|
+
self.raised_error = False
|
|
366
|
+
self._message: InternalHTTPMessage|None = msg
|
|
367
|
+
self._contructor_hook()
|
|
368
|
+
|
|
369
|
+
def _contructor_hook(self):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def total_size(self) -> int:
|
|
374
|
+
"""Total size of the stream"""
|
|
375
|
+
return self._parser.total_size
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def url(self) -> str|None:
|
|
379
|
+
"""URL of the message"""
|
|
380
|
+
return self._message.url
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def headers(self) -> dict[str, str]:
|
|
384
|
+
"""Headers of the message"""
|
|
385
|
+
return self._message.headers
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def user_agent(self) -> str:
|
|
389
|
+
"""User agent of the message"""
|
|
390
|
+
return self._message.user_agent
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def content_encoding(self) -> str:
|
|
394
|
+
"""Content encoding of the message"""
|
|
395
|
+
return self._message.content_encoding
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def body(self) -> bytes:
|
|
399
|
+
"""Body of the message"""
|
|
400
|
+
return self._message.body
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def headers_complete(self) -> bool:
|
|
404
|
+
"""If the headers are complete"""
|
|
405
|
+
return self._message.headers_complete
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def message_complete(self) -> bool:
|
|
409
|
+
"""If the message is complete"""
|
|
410
|
+
return self._message.message_complete
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def http_version(self) -> str:
|
|
414
|
+
"""HTTP version of the message"""
|
|
415
|
+
return self._message.http_version
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def keep_alive(self) -> bool:
|
|
419
|
+
"""If the message should keep alive"""
|
|
420
|
+
return self._message.keep_alive
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def should_upgrade(self) -> bool:
|
|
424
|
+
"""If the message should upgrade"""
|
|
425
|
+
return self._parser.should_upgrade
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def content_length(self) -> int|None:
|
|
429
|
+
"""Content length of the message"""
|
|
430
|
+
return self._message.content_length
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def upgrading_to_h2(self) -> bool:
|
|
434
|
+
"""If the message is upgrading to HTTP/2"""
|
|
435
|
+
return self._message.upgrading_to_h2
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def upgrading_to_ws(self) -> bool:
|
|
439
|
+
"""If the message is upgrading to Websocket"""
|
|
440
|
+
return self._message.upgrading_to_ws
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def ws_stream(self) -> list[Frame]:
|
|
444
|
+
"""Websocket stream"""
|
|
445
|
+
return self._message.ws_stream
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def stream(self) -> bytes:
|
|
449
|
+
"""Stream of the message"""
|
|
450
|
+
return self._message.stream
|
|
451
|
+
|
|
452
|
+
def get_header(self, header: str, default=None) -> str:
|
|
453
|
+
"""Get a header from the message without caring about the case"""
|
|
454
|
+
return self._message.lheaders.get(header.lower(), default)
|
|
455
|
+
|
|
456
|
+
@staticmethod
|
|
457
|
+
def _before_fetch_callable_checks(internal_data: DataStreamCtx) -> bool:
|
|
458
|
+
raise NotImplementedError()
|
|
459
|
+
|
|
460
|
+
@staticmethod
|
|
461
|
+
def _parser_class() -> str:
|
|
462
|
+
raise NotImplementedError()
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def _fetch_packet(cls, internal_data: DataStreamCtx):
|
|
466
|
+
if internal_data.current_pkt is None or internal_data.current_pkt.is_tcp is False:
|
|
467
|
+
raise NotReadyToRun()
|
|
468
|
+
|
|
469
|
+
ParserType = InternalHttpRequest if internal_data.current_pkt.is_input else InternalHttpResponse
|
|
470
|
+
parser_key = f"{cls._parser_class()}_{'in' if internal_data.current_pkt.is_input else 'out'}"
|
|
471
|
+
|
|
472
|
+
parser = internal_data.data_handler_context.get(parser_key, None)
|
|
473
|
+
if parser is None or parser.raised_error:
|
|
474
|
+
parser: InternalHttpRequest|InternalHttpResponse = ParserType()
|
|
475
|
+
internal_data.data_handler_context[parser_key] = parser
|
|
476
|
+
|
|
477
|
+
if not internal_data.call_mem.get(cls._parser_class(), False): #Need to parse HTTP
|
|
478
|
+
internal_data.call_mem[cls._parser_class()] = True
|
|
479
|
+
|
|
480
|
+
#Setting websocket options if needed to the client parser
|
|
481
|
+
if internal_data.current_pkt.is_input:
|
|
482
|
+
ext_opt = internal_data.data_handler_context.get(f"{cls._parser_class()}_ws_options_client")
|
|
483
|
+
if ext_opt is not None and parser._ws_extentions != ext_opt:
|
|
484
|
+
parser._ws_extentions = ext_opt
|
|
485
|
+
|
|
486
|
+
# Memory size managment
|
|
487
|
+
if parser.total_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
|
|
488
|
+
match internal_data.full_stream_action:
|
|
489
|
+
case FullStreamAction.FLUSH:
|
|
490
|
+
# Deleting parser and re-creating it
|
|
491
|
+
parser.messages.clear()
|
|
492
|
+
parser.msg.total_size -= len(parser.msg.stream)
|
|
493
|
+
parser.msg.stream = b""
|
|
494
|
+
parser.msg.total_size -= len(parser.msg.body)
|
|
495
|
+
parser.msg.body = b""
|
|
496
|
+
print("[WARNING] Flushing stream", flush=True)
|
|
497
|
+
if parser.total_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
|
|
498
|
+
parser.reset_data()
|
|
499
|
+
case FullStreamAction.REJECT:
|
|
500
|
+
raise StreamFullReject()
|
|
501
|
+
case FullStreamAction.DROP:
|
|
502
|
+
raise StreamFullDrop()
|
|
503
|
+
case FullStreamAction.ACCEPT:
|
|
504
|
+
raise NotReadyToRun()
|
|
505
|
+
|
|
506
|
+
internal_data.call_mem["headers_were_set"] = parser.msg.headers_complete #This information is usefull for building the real object
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
parser.parse_data(internal_data.current_pkt.data)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
traceback.print_exc()
|
|
512
|
+
match internal_data.invalid_encoding_action:
|
|
513
|
+
case ExceptionAction.REJECT:
|
|
514
|
+
raise RejectConnection()
|
|
515
|
+
case ExceptionAction.DROP:
|
|
516
|
+
raise DropPacket()
|
|
517
|
+
case ExceptionAction.NOACTION:
|
|
518
|
+
raise e
|
|
519
|
+
case ExceptionAction.ACCEPT:
|
|
520
|
+
raise NotReadyToRun()
|
|
521
|
+
|
|
522
|
+
if parser.should_upgrade and not internal_data.current_pkt.is_input:
|
|
523
|
+
#Creating ws_option for the client
|
|
524
|
+
if not internal_data.data_handler_context.get(f"{cls._parser_class()}_ws_options_client"):
|
|
525
|
+
ext = parser._parse_websocket_ext()
|
|
526
|
+
internal_data.data_handler_context[f"{cls._parser_class()}_ws_options_client"] = ext
|
|
527
|
+
|
|
528
|
+
#Once the parsers has been triggered, we can return the object if needed
|
|
529
|
+
if not cls._before_fetch_callable_checks(internal_data):
|
|
530
|
+
raise NotReadyToRun()
|
|
531
|
+
|
|
532
|
+
messages_tosend:list[InternalHTTPMessage] = []
|
|
533
|
+
for i in range(len(parser.messages)):
|
|
534
|
+
messages_tosend.append(parser.pop_message())
|
|
535
|
+
|
|
536
|
+
if len(messages_tosend) > 0:
|
|
537
|
+
internal_data.call_mem["headers_were_set"] = False # New messages completed so the current message headers were not set in this case
|
|
538
|
+
|
|
539
|
+
if not internal_data.call_mem["headers_were_set"] and parser.msg.headers_complete:
|
|
540
|
+
messages_tosend.append(parser.msg) # Also the current message needs to be sent due to complete headers
|
|
541
|
+
|
|
542
|
+
if parser._packet_to_stream():
|
|
543
|
+
messages_tosend.append(parser.msg) # Also the current message needs to beacase a stream is going on
|
|
544
|
+
|
|
545
|
+
messages_to_call = len(messages_tosend)
|
|
546
|
+
|
|
547
|
+
if messages_to_call == 0:
|
|
548
|
+
raise NotReadyToRun()
|
|
549
|
+
elif messages_to_call == 1:
|
|
550
|
+
return cls(parser, messages_tosend[0])
|
|
551
|
+
|
|
552
|
+
return [cls(parser, ele) for ele in messages_tosend]
|
|
553
|
+
|
|
554
|
+
class HttpRequest(InternalBasicHttpMetaClass):
|
|
555
|
+
"""
|
|
556
|
+
HTTP Request handler
|
|
557
|
+
This data handler will be called twice, first with the headers complete, and second with the body complete
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _before_fetch_callable_checks(internal_data: DataStreamCtx):
|
|
562
|
+
return internal_data.current_pkt.is_input
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def method(self) -> bytes:
|
|
566
|
+
"""Method of the request"""
|
|
567
|
+
return self._parser.msg.method
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def _parser_class() -> str:
|
|
571
|
+
return "full_http"
|
|
572
|
+
|
|
573
|
+
def __repr__(self):
|
|
574
|
+
return f"<HttpRequest method={self.method} url={self.url} headers={self.headers} body=[{0 if not self.body else len(self.body)} bytes] http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} content_length={self.content_length} stream={self.stream} ws_stream={self.ws_stream}>"
|
|
575
|
+
|
|
576
|
+
class HttpResponse(InternalBasicHttpMetaClass):
|
|
577
|
+
"""
|
|
578
|
+
HTTP Response handler
|
|
579
|
+
This data handler will be called twice, first with the headers complete, and second with the body complete
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
def _before_fetch_callable_checks(internal_data: DataStreamCtx):
|
|
584
|
+
return not internal_data.current_pkt.is_input
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def status_code(self) -> int:
|
|
588
|
+
"""Status code of the response"""
|
|
589
|
+
return self._parser.msg.status
|
|
590
|
+
|
|
591
|
+
@staticmethod
|
|
592
|
+
def _parser_class() -> str:
|
|
593
|
+
return "full_http"
|
|
594
|
+
|
|
595
|
+
def __repr__(self):
|
|
596
|
+
return f"<HttpResponse status_code={self.status_code} url={self.url} headers={self.headers} body=[{0 if not self.body else len(self.body)} bytes] http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} content_length={self.content_length} stream={self.stream} ws_stream={self.ws_stream}>"
|
|
597
|
+
|
|
598
|
+
class HttpRequestHeader(HttpRequest):
|
|
599
|
+
"""
|
|
600
|
+
HTTP Request Header handler
|
|
601
|
+
This data handler will be called only once, the headers are complete, the body will be empty and not buffered
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
def _contructor_hook(self):
|
|
605
|
+
self._parser.save_body = False
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
def _parser_class() -> str:
|
|
609
|
+
return "header_http"
|
|
610
|
+
|
|
611
|
+
class HttpResponseHeader(HttpResponse):
|
|
612
|
+
"""
|
|
613
|
+
HTTP Response Header handler
|
|
614
|
+
This data handler will be called only once, the headers are complete, the body will be empty and not buffered
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
def _contructor_hook(self):
|
|
618
|
+
self._parser.save_body = False
|
|
619
|
+
|
|
620
|
+
@staticmethod
|
|
621
|
+
def _parser_class() -> str:
|
|
622
|
+
return "header_http"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: firegex
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: Firegex client
|
|
5
5
|
Home-page: https://github.com/pwnzer0tt1/firegex
|
|
6
6
|
Author: Pwnzer0tt1
|
|
@@ -13,8 +13,11 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
Requires-Dist: typer==0.15.2
|
|
14
14
|
Requires-Dist: pydantic>=2
|
|
15
15
|
Requires-Dist: typing-extensions>=4.7.1
|
|
16
|
+
Requires-Dist: zstd
|
|
17
|
+
Requires-Dist: brotli
|
|
16
18
|
Requires-Dist: watchfiles
|
|
17
19
|
Requires-Dist: fgex
|
|
20
|
+
Requires-Dist: websockets
|
|
18
21
|
Requires-Dist: pyllhttp
|
|
19
22
|
Dynamic: author
|
|
20
23
|
Dynamic: author-email
|
|
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
6
6
|
with open('requirements.txt', 'r', encoding='utf-8') as f:
|
|
7
7
|
required = [ele.strip() for ele in f.read().splitlines() if not ele.strip().startswith("#") and ele.strip() != ""]
|
|
8
8
|
|
|
9
|
-
VERSION = "3.1
|
|
9
|
+
VERSION = "3.2.1"
|
|
10
10
|
|
|
11
11
|
setuptools.setup(
|
|
12
12
|
name="firegex",
|
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
import pyllhttp
|
|
2
|
-
from firegex.nfproxy.internals.exceptions import NotReadyToRun
|
|
3
|
-
from firegex.nfproxy.internals.data import DataStreamCtx
|
|
4
|
-
from firegex.nfproxy.internals.exceptions import StreamFullDrop, StreamFullReject, RejectConnection, DropPacket
|
|
5
|
-
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
|
|
6
|
-
from dataclasses import dataclass, field
|
|
7
|
-
from collections import deque
|
|
8
|
-
from typing import Type
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class InternalHTTPMessage:
|
|
12
|
-
"""Internal class to handle HTTP messages"""
|
|
13
|
-
url: str|None = field(default=None)
|
|
14
|
-
headers: dict[str, str] = field(default_factory=dict)
|
|
15
|
-
lheaders: dict[str, str] = field(default_factory=dict) # lowercase copy of the headers
|
|
16
|
-
body: bytes|None = field(default=None)
|
|
17
|
-
headers_complete: bool = field(default=False)
|
|
18
|
-
message_complete: bool = field(default=False)
|
|
19
|
-
status: str|None = field(default=None)
|
|
20
|
-
total_size: int = field(default=0)
|
|
21
|
-
user_agent: str = field(default_factory=str)
|
|
22
|
-
content_encoding: str = field(default=str)
|
|
23
|
-
content_type: str = field(default=str)
|
|
24
|
-
keep_alive: bool = field(default=False)
|
|
25
|
-
should_upgrade: bool = field(default=False)
|
|
26
|
-
http_version: str = field(default=str)
|
|
27
|
-
method: str = field(default=str)
|
|
28
|
-
content_length: int = field(default=0)
|
|
29
|
-
stream: bytes = field(default_factory=bytes)
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class InternalHttpBuffer:
|
|
33
|
-
"""Internal class to handle HTTP messages"""
|
|
34
|
-
_url_buffer: bytes = field(default_factory=bytes)
|
|
35
|
-
_header_fields: dict[bytes, bytes] = field(default_factory=dict)
|
|
36
|
-
_body_buffer: bytes = field(default_factory=bytes)
|
|
37
|
-
_status_buffer: bytes = field(default_factory=bytes)
|
|
38
|
-
_current_header_field: bytes = field(default_factory=bytes)
|
|
39
|
-
_current_header_value: bytes = field(default_factory=bytes)
|
|
40
|
-
|
|
41
|
-
class InternalCallbackHandler():
|
|
42
|
-
|
|
43
|
-
buffers = InternalHttpBuffer()
|
|
44
|
-
msg = InternalHTTPMessage()
|
|
45
|
-
save_body = True
|
|
46
|
-
raised_error = False
|
|
47
|
-
has_begun = False
|
|
48
|
-
messages: deque[InternalHTTPMessage] = deque()
|
|
49
|
-
|
|
50
|
-
def reset_data(self):
|
|
51
|
-
self.msg = InternalHTTPMessage()
|
|
52
|
-
self.buffers = InternalHttpBuffer()
|
|
53
|
-
self.messages.clear()
|
|
54
|
-
|
|
55
|
-
def on_message_begin(self):
|
|
56
|
-
self.buffers = InternalHttpBuffer()
|
|
57
|
-
self.msg = InternalHTTPMessage()
|
|
58
|
-
self.has_begun = True
|
|
59
|
-
|
|
60
|
-
def on_url(self, url):
|
|
61
|
-
self.buffers._url_buffer += url
|
|
62
|
-
self.msg.total_size += len(url)
|
|
63
|
-
|
|
64
|
-
def on_url_complete(self):
|
|
65
|
-
self.msg.url = self.buffers._url_buffer.decode(errors="ignore")
|
|
66
|
-
self.buffers._url_buffer = b""
|
|
67
|
-
|
|
68
|
-
def on_status(self, status: bytes):
|
|
69
|
-
self.msg.total_size += len(status)
|
|
70
|
-
self.buffers._status_buffer += status
|
|
71
|
-
|
|
72
|
-
def on_status_complete(self):
|
|
73
|
-
self.msg.status = self.buffers._status_buffer.decode(errors="ignore")
|
|
74
|
-
self.buffers._status_buffer = b""
|
|
75
|
-
|
|
76
|
-
def on_header_field(self, field):
|
|
77
|
-
self.msg.total_size += len(field)
|
|
78
|
-
self.buffers._current_header_field += field
|
|
79
|
-
|
|
80
|
-
def on_header_field_complete(self):
|
|
81
|
-
pass # Nothing to do
|
|
82
|
-
|
|
83
|
-
def on_header_value(self, value):
|
|
84
|
-
self.msg.total_size += len(value)
|
|
85
|
-
self.buffers._current_header_value += value
|
|
86
|
-
|
|
87
|
-
def on_header_value_complete(self):
|
|
88
|
-
if self.buffers._current_header_field:
|
|
89
|
-
self.buffers._header_fields[self.buffers._current_header_field.decode(errors="ignore")] = self.buffers._current_header_value.decode(errors="ignore")
|
|
90
|
-
self.buffers._current_header_field = b""
|
|
91
|
-
self.buffers._current_header_value = b""
|
|
92
|
-
|
|
93
|
-
def on_headers_complete(self):
|
|
94
|
-
self.msg.headers = self.buffers._header_fields
|
|
95
|
-
self.msg.lheaders = {k.lower(): v for k, v in self.buffers._header_fields.items()}
|
|
96
|
-
self.buffers._header_fields = {}
|
|
97
|
-
self.buffers._current_header_field = b""
|
|
98
|
-
self.buffers._current_header_value = b""
|
|
99
|
-
self.msg.headers_complete = True
|
|
100
|
-
self.msg.method = self.method_parsed
|
|
101
|
-
self.msg.content_length = self.content_length_parsed
|
|
102
|
-
self.msg.should_upgrade = self.should_upgrade
|
|
103
|
-
self.msg.keep_alive = self.keep_alive
|
|
104
|
-
self.msg.http_version = self.http_version
|
|
105
|
-
self.msg.content_type = self.content_type
|
|
106
|
-
self.msg.content_encoding = self.content_encoding
|
|
107
|
-
self.msg.user_agent = self.user_agent
|
|
108
|
-
|
|
109
|
-
def on_body(self, body: bytes):
|
|
110
|
-
if self.save_body:
|
|
111
|
-
self.msg.total_size += len(body)
|
|
112
|
-
self.buffers._body_buffer += body
|
|
113
|
-
|
|
114
|
-
def on_message_complete(self):
|
|
115
|
-
self.msg.body = self.buffers._body_buffer
|
|
116
|
-
self.buffers._body_buffer = b""
|
|
117
|
-
try:
|
|
118
|
-
if "gzip" in self.content_encoding.lower():
|
|
119
|
-
import gzip
|
|
120
|
-
import io
|
|
121
|
-
with gzip.GzipFile(fileobj=io.BytesIO(self.msg.body)) as f:
|
|
122
|
-
self.msg.body = f.read()
|
|
123
|
-
except Exception as e:
|
|
124
|
-
print(f"Error decompressing gzip: {e}: skipping", flush=True)
|
|
125
|
-
self.msg.message_complete = True
|
|
126
|
-
self.has_begun = False
|
|
127
|
-
if not self._packet_to_stream():
|
|
128
|
-
self.messages.append(self.msg)
|
|
129
|
-
|
|
130
|
-
@property
|
|
131
|
-
def user_agent(self) -> str:
|
|
132
|
-
return self.msg.lheaders.get("user-agent", "")
|
|
133
|
-
|
|
134
|
-
@property
|
|
135
|
-
def content_encoding(self) -> str:
|
|
136
|
-
return self.msg.lheaders.get("content-encoding", "")
|
|
137
|
-
|
|
138
|
-
@property
|
|
139
|
-
def content_type(self) -> str:
|
|
140
|
-
return self.msg.lheaders.get("content-type", "")
|
|
141
|
-
|
|
142
|
-
@property
|
|
143
|
-
def keep_alive(self) -> bool:
|
|
144
|
-
return self.should_keep_alive
|
|
145
|
-
|
|
146
|
-
@property
|
|
147
|
-
def should_upgrade(self) -> bool:
|
|
148
|
-
return self.is_upgrading
|
|
149
|
-
|
|
150
|
-
@property
|
|
151
|
-
def http_version(self) -> str:
|
|
152
|
-
if self.major and self.minor:
|
|
153
|
-
return f"{self.major}.{self.minor}"
|
|
154
|
-
else:
|
|
155
|
-
return ""
|
|
156
|
-
|
|
157
|
-
@property
|
|
158
|
-
def method_parsed(self) -> str:
|
|
159
|
-
return self.method
|
|
160
|
-
|
|
161
|
-
@property
|
|
162
|
-
def total_size(self) -> int:
|
|
163
|
-
"""Total size used by the parser"""
|
|
164
|
-
tot = self.msg.total_size
|
|
165
|
-
for msg in self.messages:
|
|
166
|
-
tot += msg.total_size
|
|
167
|
-
return tot
|
|
168
|
-
|
|
169
|
-
@property
|
|
170
|
-
def content_length_parsed(self) -> int:
|
|
171
|
-
return self.content_length
|
|
172
|
-
|
|
173
|
-
def _packet_to_stream(self):
|
|
174
|
-
return self.should_upgrade and self.save_body
|
|
175
|
-
|
|
176
|
-
def parse_data(self, data: bytes):
|
|
177
|
-
if self._packet_to_stream(): # This is a websocket upgrade!
|
|
178
|
-
self.msg.message_complete = True # The message is complete but becomed a stream, so need to be called every time a new packet is received
|
|
179
|
-
self.msg.total_size += len(data)
|
|
180
|
-
self.msg.stream += data #buffering stream
|
|
181
|
-
else:
|
|
182
|
-
try:
|
|
183
|
-
self.execute(data)
|
|
184
|
-
except Exception as e:
|
|
185
|
-
self.raised_error = True
|
|
186
|
-
print(f"Error parsing HTTP packet: {e} with data {data}", flush=True)
|
|
187
|
-
raise e
|
|
188
|
-
|
|
189
|
-
def pop_message(self):
|
|
190
|
-
return self.messages.popleft()
|
|
191
|
-
|
|
192
|
-
def __repr__(self):
|
|
193
|
-
return f"<InternalCallbackHandler msg={self.msg} buffers={self.buffers} save_body={self.save_body} raised_error={self.raised_error} has_begun={self.has_begun} messages={self.messages}>"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class InternalHttpRequest(InternalCallbackHandler, pyllhttp.Request):
|
|
197
|
-
def __init__(self):
|
|
198
|
-
super(InternalCallbackHandler, self).__init__()
|
|
199
|
-
super(pyllhttp.Request, self).__init__()
|
|
200
|
-
|
|
201
|
-
class InternalHttpResponse(InternalCallbackHandler, pyllhttp.Response):
|
|
202
|
-
def __init__(self):
|
|
203
|
-
super(InternalCallbackHandler, self).__init__()
|
|
204
|
-
super(pyllhttp.Response, self).__init__()
|
|
205
|
-
|
|
206
|
-
class InternalBasicHttpMetaClass:
|
|
207
|
-
"""Internal class to handle HTTP requests and responses"""
|
|
208
|
-
|
|
209
|
-
def __init__(self, parser: InternalHttpRequest|InternalHttpResponse, msg: InternalHTTPMessage):
|
|
210
|
-
self._parser = parser
|
|
211
|
-
self.stream = b""
|
|
212
|
-
self.raised_error = False
|
|
213
|
-
self._message: InternalHTTPMessage|None = msg
|
|
214
|
-
self._contructor_hook()
|
|
215
|
-
|
|
216
|
-
def _contructor_hook(self):
|
|
217
|
-
pass
|
|
218
|
-
|
|
219
|
-
@property
|
|
220
|
-
def total_size(self) -> int:
|
|
221
|
-
"""Total size of the stream"""
|
|
222
|
-
return self._parser.total_size
|
|
223
|
-
|
|
224
|
-
@property
|
|
225
|
-
def url(self) -> str|None:
|
|
226
|
-
"""URL of the message"""
|
|
227
|
-
return self._message.url
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def headers(self) -> dict[str, str]:
|
|
231
|
-
"""Headers of the message"""
|
|
232
|
-
return self._message.headers
|
|
233
|
-
|
|
234
|
-
@property
|
|
235
|
-
def user_agent(self) -> str:
|
|
236
|
-
"""User agent of the message"""
|
|
237
|
-
return self._message.user_agent
|
|
238
|
-
|
|
239
|
-
@property
|
|
240
|
-
def content_encoding(self) -> str:
|
|
241
|
-
"""Content encoding of the message"""
|
|
242
|
-
return self._message.content_encoding
|
|
243
|
-
|
|
244
|
-
@property
|
|
245
|
-
def body(self) -> bytes:
|
|
246
|
-
"""Body of the message"""
|
|
247
|
-
return self._message.body
|
|
248
|
-
|
|
249
|
-
@property
|
|
250
|
-
def headers_complete(self) -> bool:
|
|
251
|
-
"""If the headers are complete"""
|
|
252
|
-
return self._message.headers_complete
|
|
253
|
-
|
|
254
|
-
@property
|
|
255
|
-
def message_complete(self) -> bool:
|
|
256
|
-
"""If the message is complete"""
|
|
257
|
-
return self._message.message_complete
|
|
258
|
-
|
|
259
|
-
@property
|
|
260
|
-
def http_version(self) -> str:
|
|
261
|
-
"""HTTP version of the message"""
|
|
262
|
-
return self._message.http_version
|
|
263
|
-
|
|
264
|
-
@property
|
|
265
|
-
def keep_alive(self) -> bool:
|
|
266
|
-
"""If the message should keep alive"""
|
|
267
|
-
return self._message.keep_alive
|
|
268
|
-
|
|
269
|
-
@property
|
|
270
|
-
def should_upgrade(self) -> bool:
|
|
271
|
-
"""If the message should upgrade"""
|
|
272
|
-
return self._message.should_upgrade
|
|
273
|
-
|
|
274
|
-
@property
|
|
275
|
-
def content_length(self) -> int|None:
|
|
276
|
-
"""Content length of the message"""
|
|
277
|
-
return self._message.content_length
|
|
278
|
-
|
|
279
|
-
def get_header(self, header: str, default=None) -> str:
|
|
280
|
-
"""Get a header from the message without caring about the case"""
|
|
281
|
-
return self._message.lheaders.get(header.lower(), default)
|
|
282
|
-
|
|
283
|
-
@staticmethod
|
|
284
|
-
def _associated_parser_class() -> Type[InternalHttpRequest]|Type[InternalHttpResponse]:
|
|
285
|
-
raise NotImplementedError()
|
|
286
|
-
|
|
287
|
-
@staticmethod
|
|
288
|
-
def _before_fetch_callable_checks(internal_data: DataStreamCtx):
|
|
289
|
-
return True
|
|
290
|
-
|
|
291
|
-
@classmethod
|
|
292
|
-
def _fetch_packet(cls, internal_data: DataStreamCtx):
|
|
293
|
-
if internal_data.current_pkt is None or internal_data.current_pkt.is_tcp is False:
|
|
294
|
-
raise NotReadyToRun()
|
|
295
|
-
|
|
296
|
-
ParserType = cls._associated_parser_class()
|
|
297
|
-
|
|
298
|
-
parser = internal_data.data_handler_context.get(cls, None)
|
|
299
|
-
if parser is None or parser.raised_error:
|
|
300
|
-
parser: InternalHttpRequest|InternalHttpResponse = ParserType()
|
|
301
|
-
internal_data.data_handler_context[cls] = parser
|
|
302
|
-
|
|
303
|
-
if not cls._before_fetch_callable_checks(internal_data):
|
|
304
|
-
raise NotReadyToRun()
|
|
305
|
-
|
|
306
|
-
# Memory size managment
|
|
307
|
-
if parser.total_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
|
|
308
|
-
match internal_data.full_stream_action:
|
|
309
|
-
case FullStreamAction.FLUSH:
|
|
310
|
-
# Deleting parser and re-creating it
|
|
311
|
-
parser.messages.clear()
|
|
312
|
-
parser.msg.total_size -= len(parser.msg.stream)
|
|
313
|
-
parser.msg.stream = b""
|
|
314
|
-
parser.msg.total_size -= len(parser.msg.body)
|
|
315
|
-
parser.msg.body = b""
|
|
316
|
-
print("[WARNING] Flushing stream", flush=True)
|
|
317
|
-
if parser.total_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
|
|
318
|
-
parser.reset_data()
|
|
319
|
-
case FullStreamAction.REJECT:
|
|
320
|
-
raise StreamFullReject()
|
|
321
|
-
case FullStreamAction.DROP:
|
|
322
|
-
raise StreamFullDrop()
|
|
323
|
-
case FullStreamAction.ACCEPT:
|
|
324
|
-
raise NotReadyToRun()
|
|
325
|
-
|
|
326
|
-
headers_were_set = parser.msg.headers_complete
|
|
327
|
-
try:
|
|
328
|
-
parser.parse_data(internal_data.current_pkt.data)
|
|
329
|
-
except Exception as e:
|
|
330
|
-
match internal_data.invalid_encoding_action:
|
|
331
|
-
case ExceptionAction.REJECT:
|
|
332
|
-
raise RejectConnection()
|
|
333
|
-
case ExceptionAction.DROP:
|
|
334
|
-
raise DropPacket()
|
|
335
|
-
case ExceptionAction.NOACTION:
|
|
336
|
-
raise e
|
|
337
|
-
case ExceptionAction.ACCEPT:
|
|
338
|
-
raise NotReadyToRun()
|
|
339
|
-
|
|
340
|
-
messages_tosend:list[InternalHTTPMessage] = []
|
|
341
|
-
for i in range(len(parser.messages)):
|
|
342
|
-
messages_tosend.append(parser.pop_message())
|
|
343
|
-
|
|
344
|
-
if len(messages_tosend) > 0:
|
|
345
|
-
headers_were_set = False # New messages completed so the current message headers were not set in this case
|
|
346
|
-
|
|
347
|
-
if not headers_were_set and parser.msg.headers_complete:
|
|
348
|
-
messages_tosend.append(parser.msg) # Also the current message needs to be sent due to complete headers
|
|
349
|
-
|
|
350
|
-
if headers_were_set and parser.msg.message_complete and parser.msg.should_upgrade and parser.save_body:
|
|
351
|
-
messages_tosend.append(parser.msg) # Also the current message needs to beacase a websocket stream is going on
|
|
352
|
-
|
|
353
|
-
messages_to_call = len(messages_tosend)
|
|
354
|
-
|
|
355
|
-
if messages_to_call == 0:
|
|
356
|
-
raise NotReadyToRun()
|
|
357
|
-
elif messages_to_call == 1:
|
|
358
|
-
return cls(parser, messages_tosend[0])
|
|
359
|
-
|
|
360
|
-
return [cls(parser, ele) for ele in messages_tosend]
|
|
361
|
-
|
|
362
|
-
class HttpRequest(InternalBasicHttpMetaClass):
|
|
363
|
-
"""
|
|
364
|
-
HTTP Request handler
|
|
365
|
-
This data handler will be called twice, first with the headers complete, and second with the body complete
|
|
366
|
-
"""
|
|
367
|
-
|
|
368
|
-
@staticmethod
|
|
369
|
-
def _associated_parser_class() -> Type[InternalHttpRequest]:
|
|
370
|
-
return InternalHttpRequest
|
|
371
|
-
|
|
372
|
-
@staticmethod
|
|
373
|
-
def _before_fetch_callable_checks(internal_data: DataStreamCtx):
|
|
374
|
-
return internal_data.current_pkt.is_input
|
|
375
|
-
|
|
376
|
-
@property
|
|
377
|
-
def method(self) -> bytes:
|
|
378
|
-
"""Method of the request"""
|
|
379
|
-
return self._parser.msg.method
|
|
380
|
-
|
|
381
|
-
def __repr__(self):
|
|
382
|
-
return f"<HttpRequest method={self.method} url={self.url} headers={self.headers} body={self.body} http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} content_length={self.content_length} stream={self.stream}>"
|
|
383
|
-
|
|
384
|
-
class HttpResponse(InternalBasicHttpMetaClass):
|
|
385
|
-
"""
|
|
386
|
-
HTTP Response handler
|
|
387
|
-
This data handler will be called twice, first with the headers complete, and second with the body complete
|
|
388
|
-
"""
|
|
389
|
-
|
|
390
|
-
@staticmethod
|
|
391
|
-
def _associated_parser_class() -> Type[InternalHttpResponse]:
|
|
392
|
-
return InternalHttpResponse
|
|
393
|
-
|
|
394
|
-
@staticmethod
|
|
395
|
-
def _before_fetch_callable_checks(internal_data: DataStreamCtx):
|
|
396
|
-
return not internal_data.current_pkt.is_input
|
|
397
|
-
|
|
398
|
-
@property
|
|
399
|
-
def status_code(self) -> int:
|
|
400
|
-
"""Status code of the response"""
|
|
401
|
-
return self._parser.msg.status
|
|
402
|
-
|
|
403
|
-
def __repr__(self):
|
|
404
|
-
return f"<HttpResponse status_code={self.status_code} url={self.url} headers={self.headers} body={self.body} http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} content_length={self.content_length} stream={self.stream}>"
|
|
405
|
-
|
|
406
|
-
class HttpRequestHeader(HttpRequest):
|
|
407
|
-
"""
|
|
408
|
-
HTTP Request Header handler
|
|
409
|
-
This data handler will be called only once, the headers are complete, the body will be empty and not buffered
|
|
410
|
-
"""
|
|
411
|
-
|
|
412
|
-
def _contructor_hook(self):
|
|
413
|
-
self._parser.save_body = False
|
|
414
|
-
|
|
415
|
-
class HttpResponseHeader(HttpResponse):
|
|
416
|
-
"""
|
|
417
|
-
HTTP Response Header handler
|
|
418
|
-
This data handler will be called only once, the headers are complete, the body will be empty and not buffered
|
|
419
|
-
"""
|
|
420
|
-
|
|
421
|
-
def _contructor_hook(self):
|
|
422
|
-
self._parser.save_body = False
|
firegex-3.1.0/requirements.txt
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|