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.
Files changed (27) hide show
  1. {firegex-3.1.0/firegex.egg-info → firegex-3.2.1}/PKG-INFO +4 -1
  2. firegex-3.2.1/firegex/__init__.py +5 -0
  3. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/__init__.py +6 -2
  4. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/data.py +1 -0
  5. firegex-3.2.1/firegex/nfproxy/models/http.py +622 -0
  6. {firegex-3.1.0 → firegex-3.2.1/firegex.egg-info}/PKG-INFO +4 -1
  7. {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/requires.txt +3 -0
  8. firegex-3.2.1/requirements.txt +9 -0
  9. {firegex-3.1.0 → firegex-3.2.1}/setup.py +1 -1
  10. firegex-3.1.0/firegex/__init__.py +0 -5
  11. firegex-3.1.0/firegex/nfproxy/models/http.py +0 -422
  12. firegex-3.1.0/requirements.txt +0 -6
  13. {firegex-3.1.0 → firegex-3.2.1}/MANIFEST.in +0 -0
  14. {firegex-3.1.0 → firegex-3.2.1}/README.md +0 -0
  15. {firegex-3.1.0 → firegex-3.2.1}/fgex +0 -0
  16. {firegex-3.1.0 → firegex-3.2.1}/firegex/__main__.py +0 -0
  17. {firegex-3.1.0 → firegex-3.2.1}/firegex/cli.py +0 -0
  18. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/__init__.py +0 -0
  19. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/exceptions.py +0 -0
  20. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/internals/models.py +0 -0
  21. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/models/__init__.py +0 -0
  22. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/models/tcp.py +0 -0
  23. {firegex-3.1.0 → firegex-3.2.1}/firegex/nfproxy/proxysim/__init__.py +0 -0
  24. {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/SOURCES.txt +0 -0
  25. {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/dependency_links.txt +0 -0
  26. {firegex-3.1.0 → firegex-3.2.1}/firegex.egg-info/top_level.txt +0 -0
  27. {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.0
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
@@ -0,0 +1,5 @@
1
+
2
+ __version__ = "3.2.1" if "{" not in "3.2.1" else "0.0.0"
3
+
4
+ #Exported functions
5
+ __all__ = []
@@ -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
- for ele in try_to_call(new_params):
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.0
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
@@ -1,6 +1,9 @@
1
1
  typer==0.15.2
2
2
  pydantic>=2
3
3
  typing-extensions>=4.7.1
4
+ zstd
5
+ brotli
4
6
  watchfiles
5
7
  fgex
8
+ websockets
6
9
  pyllhttp
@@ -0,0 +1,9 @@
1
+ typer==0.15.2
2
+ pydantic>=2
3
+ typing-extensions>=4.7.1
4
+ zstd # waiting for pull request to be merged
5
+ brotli # waiting for pull request to be merged
6
+ watchfiles
7
+ fgex
8
+ websockets
9
+ pyllhttp
@@ -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.0"
9
+ VERSION = "3.2.1"
10
10
 
11
11
  setuptools.setup(
12
12
  name="firegex",
@@ -1,5 +0,0 @@
1
-
2
- __version__ = "3.1.0" if "{" not in "3.1.0" else "0.0.0"
3
-
4
- #Exported functions
5
- __all__ = []
@@ -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
@@ -1,6 +0,0 @@
1
- typer==0.15.2
2
- pydantic>=2
3
- typing-extensions>=4.7.1
4
- watchfiles
5
- fgex
6
- pyllhttp
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes