correctly supporting HTTP compression + more compression types supported

This commit is contained in:
Domingo Dirutigliano
2025-03-15 10:01:33 +01:00
parent 25d71c4b94
commit ee54671930
7 changed files with 83 additions and 25 deletions

View File

@@ -16,13 +16,17 @@ RUN bun run build
FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest
RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \ RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \ libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \
vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel uv vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel uv git
RUN mkdir -p /execute/modules RUN mkdir -p /execute/modules
WORKDIR /execute WORKDIR /execute
ADD ./backend/requirements.txt /execute/requirements.txt ADD ./backend/requirements.txt /execute/requirements.txt
RUN uv pip install --no-cache --system -r /execute/requirements.txt RUN uv pip install --no-cache --system -r /execute/requirements.txt
RUN git clone https://github.com/domysh/brotli && cd brotli && pip install . && cd .. && rm -rf brotli && \
git clone https://github.com/domysh/python-zstd --recurse && cd python-zstd && pip install . && cd .. && rm -rf python-zstd
COPY ./fgex-lib /execute/fgex-lib COPY ./fgex-lib /execute/fgex-lib
RUN uv pip install --no-cache --system ./fgex-lib RUN uv pip install --no-cache --system ./fgex-lib

View File

@@ -6,6 +6,11 @@ from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
from dataclasses import dataclass, field from dataclasses import dataclass, field
from collections import deque from collections import deque
from typing import Type from typing import Type
from zstd import ZSTD_uncompress
import gzip
import io
import zlib
import brotli
@dataclass @dataclass
class InternalHTTPMessage: class InternalHTTPMessage:
@@ -14,6 +19,7 @@ class InternalHTTPMessage:
headers: dict[str, str] = field(default_factory=dict) headers: dict[str, str] = field(default_factory=dict)
lheaders: dict[str, str] = field(default_factory=dict) # lowercase copy of the headers lheaders: dict[str, str] = field(default_factory=dict) # lowercase copy of the headers
body: bytes|None = field(default=None) body: bytes|None = field(default=None)
body_decoded: bool = field(default=False)
headers_complete: bool = field(default=False) headers_complete: bool = field(default=False)
message_complete: bool = field(default=False) message_complete: bool = field(default=False)
status: str|None = field(default=None) status: str|None = field(default=None)
@@ -114,14 +120,52 @@ class InternalCallbackHandler():
def on_message_complete(self): def on_message_complete(self):
self.msg.body = self.buffers._body_buffer self.msg.body = self.buffers._body_buffer
self.buffers._body_buffer = b"" self.buffers._body_buffer = b""
encodings = [ele.strip() for ele in self.content_encoding.lower().split(",")]
decode_success = True
decoding_body = self.msg.body
for enc in reversed(encodings):
if not enc:
continue
if enc == "deflate":
try:
decompress = zlib.decompressobj(-zlib.MAX_WBITS)
decoding_body = decompress.decompress(decoding_body)
decoding_body += decompress.flush()
except Exception as e:
print(f"Error decompressing deflate: {e}: skipping", flush=True)
decode_success = False
break
elif enc == "br":
try:
decoding_body = brotli.decompress(decoding_body)
except Exception as e:
print(f"Error decompressing brotli: {e}: skipping", flush=True)
decode_success = False
break
elif enc == "gzip":
try: try:
if "gzip" in self.content_encoding.lower(): if "gzip" in self.content_encoding.lower():
import gzip with gzip.GzipFile(fileobj=io.BytesIO(decoding_body)) as f:
import io decoding_body = f.read()
with gzip.GzipFile(fileobj=io.BytesIO(self.msg.body)) as f:
self.msg.body = f.read()
except Exception as e: except Exception as e:
print(f"Error decompressing gzip: {e}: skipping", flush=True) print(f"Error decompressing gzip: {e}: skipping", flush=True)
decode_success = False
break
elif enc == "zstd":
try:
decoding_body = ZSTD_uncompress(decoding_body)
except Exception as e:
print(f"Error decompressing zstd: {e}: skipping", flush=True)
decode_success = False
break
else:
decode_success = False
break
if decode_success:
self.msg.body = decoding_body
self.msg.body_decoded = True
self.msg.message_complete = True self.msg.message_complete = True
self.has_begun = False self.has_begun = False
if not self._packet_to_stream(): if not self._packet_to_stream():

View File

@@ -1,6 +1,8 @@
typer==0.15.2 typer==0.15.2
pydantic>=2 pydantic>=2
typing-extensions>=4.7.1 typing-extensions>=4.7.1
zstd # waiting for pull request to be merged
brotli # waiting for pull request to be merged
watchfiles watchfiles
fgex fgex
pyllhttp pyllhttp

View File

@@ -1,4 +1,4 @@
import { ActionIcon, ActionIconProps, Box, Modal, ScrollArea, Title, Tooltip } from "@mantine/core"; import { ActionIcon, ActionIconProps, Box, Container, Modal, ScrollArea, ScrollAreaAutosize, Title, Tooltip } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import { FaBookBookmark } from "react-icons/fa6"; import { FaBookBookmark } from "react-icons/fa6";
import { NFRegexDocs } from "./NFRegex/NFRegexDocs"; import { NFRegexDocs } from "./NFRegex/NFRegexDocs";
@@ -23,6 +23,7 @@ export const DocsButton = ({ doc, ...props }: { doc: EnumToPrimitiveUnion<DocTyp
<Modal opened={open} onClose={() => setOpen(false)} fullScreen title={ <Modal opened={open} onClose={() => setOpen(false)} fullScreen title={
<Title order={2}>Firegex Docs 📕</Title> <Title order={2}>Firegex Docs 📕</Title>
} scrollAreaComponent={ScrollArea.Autosize}> } scrollAreaComponent={ScrollArea.Autosize}>
<Container style={{padding: "1rem", maxWidth:"90vw"}}>
{ {
doc == DocType.NFREGEX ? doc == DocType.NFREGEX ?
<NFRegexDocs />: <NFRegexDocs />:
@@ -32,6 +33,7 @@ export const DocsButton = ({ doc, ...props }: { doc: EnumToPrimitiveUnion<DocTyp
<PortHijackDocs />: <PortHijackDocs />:
<Title order={3}>Docs not found</Title> <Title order={3}>Docs not found</Title>
} }
</Container>
</Modal> </Modal>
</Box> </Box>
} }

View File

@@ -82,7 +82,7 @@ const TCPBadge = () => {
export const NFProxyDocs = () => { export const NFProxyDocs = () => {
return ( return (
<Container size="xl"> <>
<Title order={1}>🌐 Netfilter Proxy Documentation</Title> <Title order={1}>🌐 Netfilter Proxy Documentation</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title> <Title order={2} mt="xl" mb="sm">📖 Overview</Title>
@@ -249,6 +249,9 @@ export const NFProxyDocs = () => {
<List.Item> <List.Item>
<strong>body: </strong> The body of the request (read only). It's None if the body has not arrived yet. <strong>body: </strong> The body of the request (read only). It's None if the body has not arrived yet.
</List.Item> </List.Item>
<List.Item>
<strong>body_decoded: </strong> By default the body will be decoded following the content encoding. gzip, br, deflate and zstd are supported. If the decoding fails and body is not None this paramether will be False.
</List.Item>
<List.Item> <List.Item>
<strong>http_version: </strong> The http version of the request (read only) <strong>http_version: </strong> The http version of the request (read only)
</List.Item> </List.Item>
@@ -308,6 +311,9 @@ export const NFProxyDocs = () => {
<List.Item> <List.Item>
<strong>body: </strong> The body of the response (read only). It's None if the body has not arrived yet. <strong>body: </strong> The body of the response (read only). It's None if the body has not arrived yet.
</List.Item> </List.Item>
<List.Item>
<strong>body_decoded: </strong> By default the body will be decoded following the content encoding. gzip, br, deflate and zstd are supported. If the decoding fails and body is not None this paramether will be False.
</List.Item>
<List.Item> <List.Item>
<strong>http_version: </strong> The http version of the response (read only) <strong>http_version: </strong> The http version of the response (read only)
</List.Item> </List.Item>
@@ -430,6 +436,6 @@ export const NFProxyDocs = () => {
Here's a pyfilter code commented example: Here's a pyfilter code commented example:
<CodeHighlight code={EXAMPLE_PYFILTER} language="python" my="sm"/> <CodeHighlight code={EXAMPLE_PYFILTER} language="python" my="sm"/>
</Text> </Text>
</Container> </>
); );
}; };

View File

@@ -2,7 +2,7 @@ import { Container, Title, Text, List } from "@mantine/core";
export const NFRegexDocs = () => { export const NFRegexDocs = () => {
return ( return (
<Container size="xl"> <>
<Title order={1}>📡 Netfilter Regex Documentation</Title> <Title order={1}>📡 Netfilter Regex Documentation</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title> <Title order={2} mt="xl" mb="sm">📖 Overview</Title>
@@ -64,6 +64,6 @@ export const NFRegexDocs = () => {
</Text> </Text>
</List.Item> </List.Item>
</List> </List>
</Container> </>
); );
}; };

View File

@@ -4,7 +4,7 @@ import { HELP_NFPROXY_SIM } from "../NFProxy/NFProxyDocs"
export const PortHijackDocs = () => { export const PortHijackDocs = () => {
return <Container size="xl"> return <>
<Title order={1}> Hijack port to proxy</Title> <Title order={1}> Hijack port to proxy</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title> <Title order={2} mt="xl" mb="sm">📖 Overview</Title>
@@ -33,5 +33,5 @@ export const PortHijackDocs = () => {
but externaly the packets exists as connections to the original service. This mangle is done only for external packet arriving from the external ip indicated, localhost traffic won't be touched. but externaly the packets exists as connections to the original service. This mangle is done only for external packet arriving from the external ip indicated, localhost traffic won't be touched.
</Text> </Text>
<Space h="xl" /> <Space h="xl" />
</Container> </>
} }