firegex 4.0.6__tar.gz → 5.0.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.
- firegex-5.0.0/MANIFEST.in +4 -0
- firegex-5.0.0/PKG-INFO +69 -0
- firegex-5.0.0/README.md +49 -0
- {firegex-4.0.6 → firegex-5.0.0}/firegex/__init__.py +1 -1
- firegex-5.0.0/firegex/cli.py +219 -0
- firegex-5.0.0/firegex/pyfilters/__init__.py +58 -0
- firegex-5.0.0/firegex/pyfilters/internals/__init__.py +317 -0
- firegex-5.0.0/firegex/pyfilters/internals/data.py +226 -0
- {firegex-4.0.6/firegex/nfproxy → firegex-5.0.0/firegex/pyfilters}/internals/models.py +11 -5
- firegex-5.0.0/firegex/pyfilters/internals/zstd_compat.py +66 -0
- {firegex-4.0.6/firegex/nfproxy → firegex-5.0.0/firegex/pyfilters}/models/__init__.py +23 -10
- firegex-5.0.0/firegex/pyfilters/models/http.py +1387 -0
- {firegex-4.0.6/firegex/nfproxy → firegex-5.0.0/firegex/pyfilters}/models/tcp.py +4 -4
- {firegex-4.0.6/firegex/nfproxy → firegex-5.0.0/firegex/pyfilters}/proxysim/__init__.py +30 -38
- firegex-5.0.0/firegex/regex/__init__.py +342 -0
- firegex-5.0.0/firegex/regex/sim.py +104 -0
- firegex-5.0.0/firegex.egg-info/PKG-INFO +69 -0
- firegex-5.0.0/firegex.egg-info/SOURCES.txt +33 -0
- firegex-5.0.0/firegex.egg-info/entry_points.txt +2 -0
- {firegex-4.0.6 → firegex-5.0.0}/firegex.egg-info/requires.txt +5 -3
- firegex-5.0.0/llhttp/LICENSE.pyllhttp +21 -0
- firegex-5.0.0/llhttp/PROVENANCE.md +68 -0
- firegex-5.0.0/llhttp/lib/LICENSE.llhttp +22 -0
- firegex-5.0.0/llhttp/lib/api.c +517 -0
- firegex-5.0.0/llhttp/lib/http.c +170 -0
- firegex-5.0.0/llhttp/lib/llhttp.c +10279 -0
- firegex-5.0.0/llhttp/lib/llhttp.h +925 -0
- firegex-5.0.0/llhttp/llhttp_module.c +658 -0
- firegex-5.0.0/pyproject.toml +42 -0
- firegex-5.0.0/setup.py +34 -0
- firegex-4.0.6/MANIFEST.in +0 -1
- firegex-4.0.6/PKG-INFO +0 -57
- firegex-4.0.6/README.md +0 -26
- firegex-4.0.6/fgex +0 -6
- firegex-4.0.6/firegex/cli.py +0 -76
- firegex-4.0.6/firegex/nfproxy/__init__.py +0 -50
- firegex-4.0.6/firegex/nfproxy/internals/__init__.py +0 -173
- firegex-4.0.6/firegex/nfproxy/internals/data.py +0 -166
- firegex-4.0.6/firegex/nfproxy/models/http.py +0 -860
- firegex-4.0.6/firegex.egg-info/PKG-INFO +0 -57
- firegex-4.0.6/firegex.egg-info/SOURCES.txt +0 -22
- firegex-4.0.6/requirements.txt +0 -9
- firegex-4.0.6/setup.py +0 -31
- {firegex-4.0.6 → firegex-5.0.0}/firegex/__main__.py +0 -0
- {firegex-4.0.6/firegex/nfproxy → firegex-5.0.0/firegex/pyfilters}/internals/exceptions.py +0 -0
- {firegex-4.0.6 → firegex-5.0.0}/firegex.egg-info/dependency_links.txt +0 -0
- {firegex-4.0.6 → firegex-5.0.0}/firegex.egg-info/top_level.txt +0 -0
- {firegex-4.0.6 → firegex-5.0.0}/setup.cfg +0 -0
firegex-5.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firegex
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Firegex client
|
|
5
|
+
Author-email: Pwnzer0tt1 <pwnzer0tt1@poliba.it>
|
|
6
|
+
Project-URL: Homepage, https://github.com/pwnzer0tt1/firegex
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: typer
|
|
12
|
+
Requires-Dist: rich
|
|
13
|
+
Requires-Dist: pydantic>=2
|
|
14
|
+
Requires-Dist: typing-extensions>=4.7.1
|
|
15
|
+
Requires-Dist: pycryptodome
|
|
16
|
+
Requires-Dist: watchfiles
|
|
17
|
+
Requires-Dist: websockets
|
|
18
|
+
Requires-Dist: brotli>=1.2.0
|
|
19
|
+
Requires-Dist: zstandard; python_version < "3.14"
|
|
20
|
+
|
|
21
|
+
# Firegex Python Library and CLI
|
|
22
|
+
|
|
23
|
+
This is the Python library for [Firegex](https://github.com/Pwnzer0tt1/firegex), a firewall built for CTF Attack-Defense competitions. It provides `firegex.pyfilters` — the package you write [Python filters](../docs/pyfilter.md) against — `firegex.regex` for pattern rulesets, the `fgex` CLI, and local simulators for both, so a filter can be tried before it ever reaches a running Firegex.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -U firegex
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`fgex` is an alias package for `firegex`: installing either one gives you the same `firegex` module and the `fgex`/`firegex` CLI commands.
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
It carries `firegex.regex`, which binds the same hyperscan firegex matches with, so
|
|
36
|
+
a **pattern ruleset** can be checked and tried without a running instance:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
fgex regex check rules.json # does it compile? what is in it?
|
|
40
|
+
fgex regex test rules.json --sample req.txt # what would it do to this?
|
|
41
|
+
fgex regex proxy rules.json 10.60.3.1 8080 # a local proxy applying it
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The ruleset file is the same shape firegex uses. Matching needs `libhs` (vectorscan)
|
|
45
|
+
installed; without it the tools say so and stop rather than answering with a different
|
|
46
|
+
engine, because a tester that disagrees with production is worse than none.
|
|
47
|
+
|
|
48
|
+
**➡️ [Writing and testing `@pyfilter` scripts](../docs/pyfilter.md)** — the `pyfilter` decorator, every data structure (`RawPacket`, `TCPInputStream`/`TCPOutputStream`, `HttpRequest`/`HttpResponse` and their variants, `HttpHistory`), packet statements, stream limits, the `fgex pyfilters` CLI simulator, and a full worked example.
|
|
49
|
+
|
|
50
|
+
That page is also what's shown in the Firegex web UI itself (the docs button on the service pages) — both are generated from that single file, so they can never drift out of sync.
|
|
51
|
+
|
|
52
|
+
## The rest of Firegex
|
|
53
|
+
|
|
54
|
+
This package is the part you write filters *with*. What runs them is a Firegex instance,
|
|
55
|
+
started from a clone with `python3 run.py` — there is deliberately no deploy command here,
|
|
56
|
+
because any second description of that script's arguments would drift from it.
|
|
57
|
+
|
|
58
|
+
Two guides cover everything an instance does:
|
|
59
|
+
|
|
60
|
+
- **[Services](../docs/services.md)** — a service is a **network layer** (NFQUEUE, the
|
|
61
|
+
terminating proxy, or a hand-off to a proxy you wrote) plus an ordered chain of
|
|
62
|
+
**filters** (hyperscan patterns, or the Python you write with this library). TLS, QUIC
|
|
63
|
+
and every version of HTTP are protocols a service can speak rather than separate things
|
|
64
|
+
to set up.
|
|
65
|
+
- **[Firewall Rules](../docs/firewall.md)** — plain nftables allow/drop/reject rules.
|
|
66
|
+
|
|
67
|
+
The modules this library used to list — `nfregex`, `nfproxy`, `porthijack` and `tls` —
|
|
68
|
+
were folded into services as choices rather than modules in 5.0.0, which is also when
|
|
69
|
+
`firegex.nfproxy` became `firegex.pyfilters`.
|
firegex-5.0.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Firegex Python Library and CLI
|
|
2
|
+
|
|
3
|
+
This is the Python library for [Firegex](https://github.com/Pwnzer0tt1/firegex), a firewall built for CTF Attack-Defense competitions. It provides `firegex.pyfilters` — the package you write [Python filters](../docs/pyfilter.md) against — `firegex.regex` for pattern rulesets, the `fgex` CLI, and local simulators for both, so a filter can be tried before it ever reaches a running Firegex.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -U firegex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`fgex` is an alias package for `firegex`: installing either one gives you the same `firegex` module and the `fgex`/`firegex` CLI commands.
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
It carries `firegex.regex`, which binds the same hyperscan firegex matches with, so
|
|
16
|
+
a **pattern ruleset** can be checked and tried without a running instance:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
fgex regex check rules.json # does it compile? what is in it?
|
|
20
|
+
fgex regex test rules.json --sample req.txt # what would it do to this?
|
|
21
|
+
fgex regex proxy rules.json 10.60.3.1 8080 # a local proxy applying it
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The ruleset file is the same shape firegex uses. Matching needs `libhs` (vectorscan)
|
|
25
|
+
installed; without it the tools say so and stop rather than answering with a different
|
|
26
|
+
engine, because a tester that disagrees with production is worse than none.
|
|
27
|
+
|
|
28
|
+
**➡️ [Writing and testing `@pyfilter` scripts](../docs/pyfilter.md)** — the `pyfilter` decorator, every data structure (`RawPacket`, `TCPInputStream`/`TCPOutputStream`, `HttpRequest`/`HttpResponse` and their variants, `HttpHistory`), packet statements, stream limits, the `fgex pyfilters` CLI simulator, and a full worked example.
|
|
29
|
+
|
|
30
|
+
That page is also what's shown in the Firegex web UI itself (the docs button on the service pages) — both are generated from that single file, so they can never drift out of sync.
|
|
31
|
+
|
|
32
|
+
## The rest of Firegex
|
|
33
|
+
|
|
34
|
+
This package is the part you write filters *with*. What runs them is a Firegex instance,
|
|
35
|
+
started from a clone with `python3 run.py` — there is deliberately no deploy command here,
|
|
36
|
+
because any second description of that script's arguments would drift from it.
|
|
37
|
+
|
|
38
|
+
Two guides cover everything an instance does:
|
|
39
|
+
|
|
40
|
+
- **[Services](../docs/services.md)** — a service is a **network layer** (NFQUEUE, the
|
|
41
|
+
terminating proxy, or a hand-off to a proxy you wrote) plus an ordered chain of
|
|
42
|
+
**filters** (hyperscan patterns, or the Python you write with this library). TLS, QUIC
|
|
43
|
+
and every version of HTTP are protocols a service can speak rather than separate things
|
|
44
|
+
to set up.
|
|
45
|
+
- **[Firewall Rules](../docs/firewall.md)** — plain nftables allow/drop/reject rules.
|
|
46
|
+
|
|
47
|
+
The modules this library used to list — `nfregex`, `nfproxy`, `porthijack` and `tls` —
|
|
48
|
+
were folded into services as choices rather than modules in 5.0.0, which is also when
|
|
49
|
+
`firegex.nfproxy` became `firegex.pyfilters`.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
|
|
2
|
+
#!/usr/bin/env python3
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich import print
|
|
11
|
+
from rich.markup import escape
|
|
12
|
+
from typer import Exit
|
|
13
|
+
|
|
14
|
+
from firegex import __version__
|
|
15
|
+
from firegex.pyfilters.proxysim import run_proxy_simulation
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
context_settings={"help_option_names": ["-h", "--help"]}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def close_cli(code:int=1):
|
|
23
|
+
raise Exit(code)
|
|
24
|
+
|
|
25
|
+
DEV_MODE = __version__ == "0.0.0"
|
|
26
|
+
|
|
27
|
+
def test_connection(host, port, use_ipv6=False):
|
|
28
|
+
family = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
29
|
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sock.settimeout(3)
|
|
33
|
+
sock.connect((host, port))
|
|
34
|
+
return True
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
finally:
|
|
38
|
+
sock.close()
|
|
39
|
+
|
|
40
|
+
@app.command("pyfilters", help="Run your Python filters against a real service, locally")
|
|
41
|
+
def pyfilters(
|
|
42
|
+
filter_file: str = typer.Argument(..., help="The path to the filter file"),
|
|
43
|
+
address: str = typer.Argument(..., help="The address of the target to proxy"),
|
|
44
|
+
port: int = typer.Argument(..., help="The port of the target to proxy"),
|
|
45
|
+
|
|
46
|
+
from_address: str = typer.Option(None, help="The address of the local server"),
|
|
47
|
+
from_port: int = typer.Option(7474, help="The port of the local server"),
|
|
48
|
+
ipv6: bool = typer.Option(False, "-6", help="Use IPv6 for the connection"),
|
|
49
|
+
):
|
|
50
|
+
if from_address is None:
|
|
51
|
+
from_address = "::1" if ipv6 else "127.0.0.1"
|
|
52
|
+
if not os.path.isfile(filter_file):
|
|
53
|
+
print(f"[bold red]'{escape(os.path.abspath(filter_file))}' not found[/]")
|
|
54
|
+
close_cli()
|
|
55
|
+
if not test_connection(address, port, ipv6):
|
|
56
|
+
print(f"[bold red]Can't connect to {escape(address)}:{port}[/]")
|
|
57
|
+
close_cli()
|
|
58
|
+
# No protocol to pass: the filter file says which one it speaks by what it asks for.
|
|
59
|
+
run_proxy_simulation(filter_file, address, port, from_address, from_port, ipv6)
|
|
60
|
+
|
|
61
|
+
regex_app = typer.Typer(
|
|
62
|
+
no_args_is_help=True,
|
|
63
|
+
help="Try a regex ruleset with the engine firegex matches with",
|
|
64
|
+
)
|
|
65
|
+
app.add_typer(regex_app, name="regex")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _load_ruleset(path: str):
|
|
69
|
+
"""Read a ruleset, with the errors an operator can act on.
|
|
70
|
+
|
|
71
|
+
The file is the same shape firegex itself uses, so a ruleset can be written here and
|
|
72
|
+
pasted there, or exported from there and replayed here.
|
|
73
|
+
"""
|
|
74
|
+
from firegex.regex import HyperscanMissing, Rule, available, validate
|
|
75
|
+
|
|
76
|
+
if not available():
|
|
77
|
+
print(f"[bold red]{escape(str(HyperscanMissing()))}[/]")
|
|
78
|
+
close_cli()
|
|
79
|
+
if not os.path.isfile(path):
|
|
80
|
+
print(f"[bold red]'{escape(os.path.abspath(path))}' not found[/]")
|
|
81
|
+
close_cli()
|
|
82
|
+
try:
|
|
83
|
+
with open(path) as f:
|
|
84
|
+
raw = json.load(f)
|
|
85
|
+
except json.JSONDecodeError as e:
|
|
86
|
+
print(f"[bold red]{escape(path)} is not valid JSON: {escape(str(e))}[/]")
|
|
87
|
+
close_cli()
|
|
88
|
+
if not isinstance(raw, list):
|
|
89
|
+
print("[bold red]A ruleset is a list of rules[/]")
|
|
90
|
+
close_cli()
|
|
91
|
+
|
|
92
|
+
rules = []
|
|
93
|
+
broken = False
|
|
94
|
+
for index, item in enumerate(raw):
|
|
95
|
+
try:
|
|
96
|
+
rule = Rule.from_dict(item)
|
|
97
|
+
except (KeyError, ValueError) as e:
|
|
98
|
+
print(f"[bold red]rule {index} is malformed: {escape(str(e))}[/]")
|
|
99
|
+
broken = True
|
|
100
|
+
continue
|
|
101
|
+
# Judged against the mode it would actually run in, so what is accepted here is
|
|
102
|
+
# what firegex will accept.
|
|
103
|
+
why = validate(rule)
|
|
104
|
+
if why:
|
|
105
|
+
print(f"[bold red]{escape(rule.id)}: {escape(why)}[/]")
|
|
106
|
+
broken = True
|
|
107
|
+
continue
|
|
108
|
+
rules.append(rule)
|
|
109
|
+
if broken:
|
|
110
|
+
close_cli()
|
|
111
|
+
if not rules:
|
|
112
|
+
print("[bold red]The ruleset is empty[/]")
|
|
113
|
+
close_cli()
|
|
114
|
+
return rules
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@regex_app.command("check", help="Check that a ruleset compiles, and say what is in it")
|
|
118
|
+
def regex_check(
|
|
119
|
+
rules_file: str = typer.Argument(..., help="The path to the ruleset (JSON)"),
|
|
120
|
+
):
|
|
121
|
+
rules = _load_ruleset(rules_file)
|
|
122
|
+
print(f"[bold green]{len(rules)} rule(s), all valid[/]")
|
|
123
|
+
for rule in rules:
|
|
124
|
+
case = "" if rule.case_sensitive else ", any case"
|
|
125
|
+
print(f" [bold]{escape(rule.id)}[/] /{escape(rule.pattern)}/ "
|
|
126
|
+
f"[dim]{rule.direction.value}{case}, block[/]")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@regex_app.command("test", help="Run a ruleset over a sample and show what it would do")
|
|
130
|
+
def regex_test(
|
|
131
|
+
rules_file: str = typer.Argument(..., help="The path to the ruleset (JSON)"),
|
|
132
|
+
sample_file: str = typer.Option(None, "--sample", "-s",
|
|
133
|
+
help="File to match against; stdin when omitted"),
|
|
134
|
+
from_service: bool = typer.Option(False, "--from-service",
|
|
135
|
+
help="Treat the sample as traffic from the service"),
|
|
136
|
+
):
|
|
137
|
+
from firegex.regex import Ruleset
|
|
138
|
+
|
|
139
|
+
rules = _load_ruleset(rules_file)
|
|
140
|
+
if sample_file:
|
|
141
|
+
if not os.path.isfile(sample_file):
|
|
142
|
+
print(f"[bold red]'{escape(os.path.abspath(sample_file))}' not found[/]")
|
|
143
|
+
close_cli()
|
|
144
|
+
with open(sample_file, "rb") as f:
|
|
145
|
+
sample = f.read()
|
|
146
|
+
else:
|
|
147
|
+
sample = sys.stdin.buffer.read()
|
|
148
|
+
if not sample:
|
|
149
|
+
print("[bold red]Nothing to match against[/]")
|
|
150
|
+
close_cli()
|
|
151
|
+
|
|
152
|
+
ruleset = Ruleset(rules)
|
|
153
|
+
is_input = not from_service
|
|
154
|
+
blocked_by = ruleset.apply(sample, is_input)
|
|
155
|
+
|
|
156
|
+
# Said before the verdict, because it changes what the verdict means: "passed
|
|
157
|
+
# through" from a run that could not try every rule is not the same answer.
|
|
158
|
+
for rule_id, why in ruleset.unscannable.items():
|
|
159
|
+
print(f"[bold yellow]not tried here[/] [bold]{escape(rule_id)}[/]: {escape(why)}. "
|
|
160
|
+
f"It is valid and will run on a real service — only matching it against a "
|
|
161
|
+
f"sample needs a mode hyperscan does not offer for it.")
|
|
162
|
+
|
|
163
|
+
if blocked_by is not None:
|
|
164
|
+
print(f"[bold red]blocked[/] by [bold]{escape(blocked_by)}[/]")
|
|
165
|
+
close_cli(0)
|
|
166
|
+
print("[bold green]passed through[/]"
|
|
167
|
+
+ (" by the rules that could be tried" if ruleset.unscannable else ""))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@regex_app.command("proxy", help="Run a local proxy applying a ruleset, like `fgex pyfilters`")
|
|
171
|
+
def regex_proxy(
|
|
172
|
+
rules_file: str = typer.Argument(..., help="The path to the ruleset (JSON)"),
|
|
173
|
+
address: str = typer.Argument(..., help="The address of the target to proxy"),
|
|
174
|
+
port: int = typer.Argument(..., help="The port of the target to proxy"),
|
|
175
|
+
from_address: str = typer.Option(None, help="The address of the local server"),
|
|
176
|
+
from_port: int = typer.Option(7474, help="The port of the local server"),
|
|
177
|
+
ipv6: bool = typer.Option(False, "-6", help="Use IPv6 for the connection"),
|
|
178
|
+
):
|
|
179
|
+
from firegex.regex import Ruleset
|
|
180
|
+
from firegex.regex.sim import run_regex_simulation
|
|
181
|
+
|
|
182
|
+
rules = _load_ruleset(rules_file)
|
|
183
|
+
if from_address is None:
|
|
184
|
+
from_address = "::1" if ipv6 else "127.0.0.1"
|
|
185
|
+
if not test_connection(address, port, ipv6):
|
|
186
|
+
print(f"[bold red]Can't connect to {escape(address)}:{port}[/]")
|
|
187
|
+
close_cli()
|
|
188
|
+
print("[dim]Blocking matches per chunk here, but across the whole stream on a real "
|
|
189
|
+
"service: a pattern split over two reads is caught there and not here.[/]")
|
|
190
|
+
ruleset = Ruleset(rules)
|
|
191
|
+
# Built before the first connection rather than on it, so the warning is on screen
|
|
192
|
+
# while the operator is still reading the banner instead of appearing mid-traffic.
|
|
193
|
+
for is_input in (True, False):
|
|
194
|
+
ruleset._db(is_input)
|
|
195
|
+
for rule_id, why in ruleset.unscannable.items():
|
|
196
|
+
print(f"[bold yellow]not applied here[/] [bold]{escape(rule_id)}[/]: {escape(why)}. "
|
|
197
|
+
f"It is valid and will run on a real service.")
|
|
198
|
+
run_regex_simulation(ruleset, address, port, from_address, from_port, ipv6)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def version_callback(verison: bool):
|
|
202
|
+
if verison:
|
|
203
|
+
print(__version__, "Development Mode" if DEV_MODE else "Release")
|
|
204
|
+
raise typer.Exit()
|
|
205
|
+
|
|
206
|
+
@app.callback()
|
|
207
|
+
def main(
|
|
208
|
+
verison: bool = typer.Option(False, "--version", "-v", help="Show the version of the client", callback=version_callback),
|
|
209
|
+
):
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
def run():
|
|
213
|
+
try:
|
|
214
|
+
app()
|
|
215
|
+
except KeyboardInterrupt:
|
|
216
|
+
print("[bold yellow]Operation cancelled[/]")
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
run()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from firegex.pyfilters.models import (
|
|
2
|
+
RawPacket,
|
|
3
|
+
TCPInputStream,
|
|
4
|
+
TCPOutputStream,
|
|
5
|
+
TCPClientStream,
|
|
6
|
+
TCPServerStream,
|
|
7
|
+
HttpHistory,
|
|
8
|
+
HttpStreamHistory,
|
|
9
|
+
)
|
|
10
|
+
from firegex.pyfilters.internals.models import Action, FullStreamAction, ExceptionAction
|
|
11
|
+
|
|
12
|
+
ACCEPT = Action.ACCEPT
|
|
13
|
+
DROP = Action.DROP
|
|
14
|
+
REJECT = Action.REJECT
|
|
15
|
+
|
|
16
|
+
#: Set on a function by `@pyfilter`. Marking the function *is* the registration — there
|
|
17
|
+
#: is no registry to keep, to clear between files, or to race two compilations against.
|
|
18
|
+
PYFILTER_MARK = "__firegex_pyfilter__"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pyfilter(func):
|
|
22
|
+
"""Mark a function as a filter, and hand it back unchanged.
|
|
23
|
+
|
|
24
|
+
The mark lives on the function, so the file that defines it holds the whole list:
|
|
25
|
+
`collect_pyfilters` reads a module's globals in definition order. What that replaced
|
|
26
|
+
was a set of *names* on a module-global attribute of this decorator, which had three
|
|
27
|
+
consequences worth not having back — a set has no order, so the order filters ran in
|
|
28
|
+
was whatever hashing produced; the state outlived the file, so every caller had to
|
|
29
|
+
clear it before and after; and a name registered from somewhere the module globals
|
|
30
|
+
could not reach (a nested `def`) was registered and then not found.
|
|
31
|
+
|
|
32
|
+
The function is returned as it is, not wrapped. The wrapper it used to get forwarded
|
|
33
|
+
`*args` and did nothing else, at the cost of a stack frame in every traceback the
|
|
34
|
+
operator has to read.
|
|
35
|
+
"""
|
|
36
|
+
setattr(func, PYFILTER_MARK, True)
|
|
37
|
+
return func
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def collect_pyfilters(namespace: dict) -> list[str]:
|
|
41
|
+
"""The filters a module defines, in the order it defines them.
|
|
42
|
+
|
|
43
|
+
Definition order is the order they run in, which is the only order a reader of the
|
|
44
|
+
file can predict.
|
|
45
|
+
"""
|
|
46
|
+
seen, names = set(), []
|
|
47
|
+
for name, obj in namespace.items():
|
|
48
|
+
if callable(obj) and getattr(obj, PYFILTER_MARK, False) and id(obj) not in seen:
|
|
49
|
+
seen.add(id(obj))
|
|
50
|
+
names.append(name)
|
|
51
|
+
return names
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"ACCEPT", "DROP", "REJECT",
|
|
55
|
+
"Action", "FullStreamAction", "ExceptionAction", "pyfilter", "collect_pyfilters",
|
|
56
|
+
"RawPacket", "TCPInputStream", "TCPOutputStream", "TCPClientStream", "TCPServerStream",
|
|
57
|
+
"HttpHistory", "HttpStreamHistory"
|
|
58
|
+
]
|