firegex 3.1.0__tar.gz → 3.2.0__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 (26) hide show
  1. {firegex-3.1.0/firegex.egg-info → firegex-3.2.0}/PKG-INFO +4 -1
  2. firegex-3.2.0/firegex/__init__.py +5 -0
  3. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/internals/__init__.py +6 -2
  4. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/models/http.py +193 -24
  5. {firegex-3.1.0 → firegex-3.2.0/firegex.egg-info}/PKG-INFO +4 -1
  6. {firegex-3.1.0 → firegex-3.2.0}/firegex.egg-info/requires.txt +3 -0
  7. firegex-3.2.0/requirements.txt +9 -0
  8. {firegex-3.1.0 → firegex-3.2.0}/setup.py +1 -1
  9. firegex-3.1.0/firegex/__init__.py +0 -5
  10. firegex-3.1.0/requirements.txt +0 -6
  11. {firegex-3.1.0 → firegex-3.2.0}/MANIFEST.in +0 -0
  12. {firegex-3.1.0 → firegex-3.2.0}/README.md +0 -0
  13. {firegex-3.1.0 → firegex-3.2.0}/fgex +0 -0
  14. {firegex-3.1.0 → firegex-3.2.0}/firegex/__main__.py +0 -0
  15. {firegex-3.1.0 → firegex-3.2.0}/firegex/cli.py +0 -0
  16. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/__init__.py +0 -0
  17. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/internals/data.py +0 -0
  18. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/internals/exceptions.py +0 -0
  19. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/internals/models.py +0 -0
  20. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/models/__init__.py +0 -0
  21. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/models/tcp.py +0 -0
  22. {firegex-3.1.0 → firegex-3.2.0}/firegex/nfproxy/proxysim/__init__.py +0 -0
  23. {firegex-3.1.0 → firegex-3.2.0}/firegex.egg-info/SOURCES.txt +0 -0
  24. {firegex-3.1.0 → firegex-3.2.0}/firegex.egg-info/dependency_links.txt +0 -0
  25. {firegex-3.1.0 → firegex-3.2.0}/firegex.egg-info/top_level.txt +0 -0
  26. {firegex-3.1.0 → firegex-3.2.0}/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.0
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.0" if "{" not in "3.2.0" 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
 
@@ -6,6 +6,14 @@ from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
6
6
  from dataclasses import dataclass, field
7
7
  from collections import deque
8
8
  from typing import Type
9
+ from zstd import ZSTD_uncompress
10
+ import gzip
11
+ import io
12
+ import zlib
13
+ import brotli
14
+ from websockets.frames import Frame
15
+ from websockets.extensions.permessage_deflate import PerMessageDeflate
16
+ from pyllhttp import PAUSED_H2_UPGRADE, PAUSED_UPGRADE
9
17
 
10
18
  @dataclass
11
19
  class InternalHTTPMessage:
@@ -14,6 +22,7 @@ class InternalHTTPMessage:
14
22
  headers: dict[str, str] = field(default_factory=dict)
15
23
  lheaders: dict[str, str] = field(default_factory=dict) # lowercase copy of the headers
16
24
  body: bytes|None = field(default=None)
25
+ body_decoded: bool = field(default=False)
17
26
  headers_complete: bool = field(default=False)
18
27
  message_complete: bool = field(default=False)
19
28
  status: str|None = field(default=None)
@@ -27,16 +36,21 @@ class InternalHTTPMessage:
27
36
  method: str = field(default=str)
28
37
  content_length: int = field(default=0)
29
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)
30
42
 
31
43
  @dataclass
32
44
  class InternalHttpBuffer:
33
45
  """Internal class to handle HTTP messages"""
34
46
  _url_buffer: bytes = field(default_factory=bytes)
35
- _header_fields: dict[bytes, bytes] = field(default_factory=dict)
47
+ _raw_header_fields: dict[str, str|list[str]] = field(default_factory=dict)
48
+ _header_fields: dict[str, str] = field(default_factory=dict)
36
49
  _body_buffer: bytes = field(default_factory=bytes)
37
50
  _status_buffer: bytes = field(default_factory=bytes)
38
51
  _current_header_field: bytes = field(default_factory=bytes)
39
52
  _current_header_value: bytes = field(default_factory=bytes)
53
+ _ws_packet_stream: bytes = field(default_factory=bytes)
40
54
 
41
55
  class InternalCallbackHandler():
42
56
 
@@ -46,6 +60,8 @@ class InternalCallbackHandler():
46
60
  raised_error = False
47
61
  has_begun = False
48
62
  messages: deque[InternalHTTPMessage] = deque()
63
+ _ws_extentions = None
64
+ _ws_raised_error = False
49
65
 
50
66
  def reset_data(self):
51
67
  self.msg = InternalHTTPMessage()
@@ -86,14 +102,31 @@ class InternalCallbackHandler():
86
102
 
87
103
  def on_header_value_complete(self):
88
104
  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")
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
+
90
123
  self.buffers._current_header_field = b""
91
124
  self.buffers._current_header_value = b""
92
125
 
93
126
  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 = {}
127
+ self.msg.headers = self.buffers._raw_header_fields
128
+ self.msg.lheaders = self.buffers._header_fields
129
+ self.buffers._raw_header_fields = {}
97
130
  self.buffers._current_header_field = b""
98
131
  self.buffers._current_header_value = b""
99
132
  self.msg.headers_complete = True
@@ -113,15 +146,56 @@ class InternalCallbackHandler():
113
146
 
114
147
  def on_message_complete(self):
115
148
  self.msg.body = self.buffers._body_buffer
149
+ self.msg.should_upgrade = self.should_upgrade
116
150
  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)
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
+
125
199
  self.msg.message_complete = True
126
200
  self.has_begun = False
127
201
  if not self._packet_to_stream():
@@ -170,20 +244,90 @@ class InternalCallbackHandler():
170
244
  def content_length_parsed(self) -> int:
171
245
  return self.content_length
172
246
 
247
+ def _is_input(self) -> bool:
248
+ raise NotImplementedError()
249
+
173
250
  def _packet_to_stream(self):
174
251
  return self.should_upgrade and self.save_body
175
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 as e:
264
+ self._ws_raised_error = True
265
+ self.msg.stream += self.buffers._ws_packet_stream
266
+ self.buffers._ws_packet_stream = b""
267
+ self.msg.total_size += len(data)
268
+ return
269
+ if new_frame is None:
270
+ break
271
+ self.msg.ws_stream.append(new_frame)
272
+ self.msg.total_size += len(new_frame.data)
273
+ if self.msg.upgrading_to_h2:
274
+ self.msg.total_size += len(data)
275
+ self.msg.stream += data
276
+
277
+ def _parse_websocket_ext(self):
278
+ ext_ws = []
279
+ req_ext = []
280
+ for ele in self.msg.lheaders.get("sec-websocket-extensions", "").split(","):
281
+ for xt in ele.split(";"):
282
+ req_ext.append(xt.strip().lower())
283
+
284
+ for ele in req_ext:
285
+ if ele == "permessage-deflate":
286
+ ext_ws.append(PerMessageDeflate(False, False, 15, 15))
287
+ return ext_ws
288
+
289
+ def _parse_websocket_frame(self, data: bytes) -> tuple[Frame|None, bytes]:
290
+ # mask = is_input
291
+ if self._ws_extentions is None:
292
+ self._ws_extentions = self._parse_websocket_ext()
293
+ read_buffering = bytearray()
294
+ def read_exact(n: int):
295
+ nonlocal read_buffering
296
+ buffer = bytearray(read_buffering)
297
+ while len(buffer) < n:
298
+ data = yield
299
+ if data is None:
300
+ raise RuntimeError("Should not send None to this generator")
301
+ buffer.extend(data)
302
+ new_data = bytes(buffer[:n])
303
+ read_buffering = buffer[n:]
304
+ return new_data
305
+
306
+ parsing = Frame.parse(read_exact, extensions=self._ws_extentions, mask=self._is_input())
307
+ parsing.send(None)
308
+ try:
309
+ parsing.send(bytearray(data))
310
+ except StopIteration as e:
311
+ return e.value, read_buffering
312
+
313
+ return None, read_buffering
314
+
176
315
  def parse_data(self, data: bytes):
177
316
  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
317
+ self._stream_parser(data)
181
318
  else:
182
319
  try:
183
- self.execute(data)
320
+ reason, consumed = self.execute(data)
321
+ if reason == PAUSED_UPGRADE:
322
+ self.msg.upgrading_to_ws = True
323
+ self.msg.message_complete = True
324
+ self._stream_parser(data[consumed:])
325
+ elif reason == PAUSED_H2_UPGRADE:
326
+ self.msg.upgrading_to_h2 = True
327
+ self.msg.message_complete = True
328
+ self._stream_parser(data[consumed:])
184
329
  except Exception as e:
185
330
  self.raised_error = True
186
- print(f"Error parsing HTTP packet: {e} with data {data}", flush=True)
187
331
  raise e
188
332
 
189
333
  def pop_message(self):
@@ -197,18 +341,23 @@ class InternalHttpRequest(InternalCallbackHandler, pyllhttp.Request):
197
341
  def __init__(self):
198
342
  super(InternalCallbackHandler, self).__init__()
199
343
  super(pyllhttp.Request, self).__init__()
344
+
345
+ def _is_input(self):
346
+ return True
200
347
 
201
348
  class InternalHttpResponse(InternalCallbackHandler, pyllhttp.Response):
202
349
  def __init__(self):
203
350
  super(InternalCallbackHandler, self).__init__()
204
351
  super(pyllhttp.Response, self).__init__()
352
+
353
+ def _is_input(self):
354
+ return False
205
355
 
206
356
  class InternalBasicHttpMetaClass:
207
357
  """Internal class to handle HTTP requests and responses"""
208
358
 
209
359
  def __init__(self, parser: InternalHttpRequest|InternalHttpResponse, msg: InternalHTTPMessage):
210
360
  self._parser = parser
211
- self.stream = b""
212
361
  self.raised_error = False
213
362
  self._message: InternalHTTPMessage|None = msg
214
363
  self._contructor_hook()
@@ -269,12 +418,32 @@ class InternalBasicHttpMetaClass:
269
418
  @property
270
419
  def should_upgrade(self) -> bool:
271
420
  """If the message should upgrade"""
272
- return self._message.should_upgrade
421
+ return self._parser.should_upgrade
273
422
 
274
423
  @property
275
424
  def content_length(self) -> int|None:
276
425
  """Content length of the message"""
277
426
  return self._message.content_length
427
+
428
+ @property
429
+ def upgrading_to_h2(self) -> bool:
430
+ """If the message is upgrading to HTTP/2"""
431
+ return self._message.upgrading_to_h2
432
+
433
+ @property
434
+ def upgrading_to_ws(self) -> bool:
435
+ """If the message is upgrading to Websocket"""
436
+ return self._message.upgrading_to_ws
437
+
438
+ @property
439
+ def ws_stream(self) -> list[Frame]:
440
+ """Websocket stream"""
441
+ return self._message.ws_stream
442
+
443
+ @property
444
+ def stream(self) -> bytes:
445
+ """Stream of the message"""
446
+ return self._message.stream
278
447
 
279
448
  def get_header(self, header: str, default=None) -> str:
280
449
  """Get a header from the message without caring about the case"""
@@ -347,8 +516,8 @@ class InternalBasicHttpMetaClass:
347
516
  if not headers_were_set and parser.msg.headers_complete:
348
517
  messages_tosend.append(parser.msg) # Also the current message needs to be sent due to complete headers
349
518
 
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
519
+ if parser._packet_to_stream():
520
+ messages_tosend.append(parser.msg) # Also the current message needs to beacase a stream is going on
352
521
 
353
522
  messages_to_call = len(messages_tosend)
354
523
 
@@ -379,7 +548,7 @@ class HttpRequest(InternalBasicHttpMetaClass):
379
548
  return self._parser.msg.method
380
549
 
381
550
  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}>"
551
+ 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}>"
383
552
 
384
553
  class HttpResponse(InternalBasicHttpMetaClass):
385
554
  """
@@ -401,7 +570,7 @@ class HttpResponse(InternalBasicHttpMetaClass):
401
570
  return self._parser.msg.status
402
571
 
403
572
  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}>"
573
+ 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}>"
405
574
 
406
575
  class HttpRequestHeader(HttpRequest):
407
576
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: firegex
3
- Version: 3.1.0
3
+ Version: 3.2.0
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.0"
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,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