any-analysis 0.1.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.
@@ -0,0 +1,8 @@
1
+ tests/
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[codz]
6
+ *$py.class
7
+
8
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AhAlpha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: any-analysis
3
+ Version: 0.1.0
4
+ Summary: a backend server utility for `any-capture`, providing a simple interface for packet analysis.
5
+ Author: AhAlpha
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Requires-Python: >=3.8
20
+ Requires-Dist: alptools
21
+ Requires-Dist: fastapi
22
+ Requires-Dist: uvicorn
23
+ Description-Content-Type: text/markdown
24
+
25
+ # any-analysis-py
26
+ a backend server utility for `any-capture`, providing a simple interface for packet analysis.
27
+
28
+ > This library currently supports only HTTP connections. It will be migrated to a WebSocket server in a future release.
@@ -0,0 +1,4 @@
1
+ # any-analysis-py
2
+ a backend server utility for `any-capture`, providing a simple interface for packet analysis.
3
+
4
+ > This library currently supports only HTTP connections. It will be migrated to a WebSocket server in a future release.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "any-analysis"
7
+ dynamic = ["version"]
8
+ description = 'a backend server utility for `any-capture`, providing a simple interface for packet analysis.'
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ keywords = []
13
+ authors = [{ name = "AhAlpha" }]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: Implementation :: CPython",
25
+ "Programming Language :: Python :: Implementation :: PyPy",
26
+ ]
27
+ dependencies = ["alptools", "fastapi", "uvicorn"]
28
+
29
+ [tool.hatch.version]
30
+ path = "src/any_analysis/__about__.py"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/any_analysis"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .__about__ import __version__
2
+
3
+ from .capture import AnyCapture
4
+ from .receiver import ReliableTcpReceiver
@@ -0,0 +1,20 @@
1
+ from atools.io_tools import IOReader
2
+
3
+
4
+ class NetReader(IOReader):
5
+ @property
6
+ def varint(self) -> int:
7
+ result = 0
8
+ bit = 0
9
+ while True:
10
+ if self.offset == self.size:
11
+ if bit != 0:
12
+ raise IOError(f"Unexpected end of input while reading varint, the file may be incomplete.")
13
+ return None
14
+ byte = self.u8
15
+ result |= (byte & 0x7F) << bit
16
+ bit += 7
17
+ if not (byte & 0x80):
18
+ if byte == 0 and bit != 7:
19
+ raise IOError(f"Abnormal termination while reading varint, the file might not be in protobuf format.")
20
+ return result
@@ -0,0 +1,52 @@
1
+ import typing
2
+ import uvicorn
3
+ from contextlib import asynccontextmanager
4
+ from fastapi import FastAPI, Request, Response
5
+ from . import objects
6
+
7
+
8
+ class AnyCapture:
9
+ def __init__(self, host="127.0.0.1", port=8900):
10
+ self.host = host
11
+ self.port = port
12
+ self.app = FastAPI()
13
+ self.http_handler = None
14
+ self.tcp_handler = None
15
+
16
+ @self.app.post("/http")
17
+ async def __on_http_event(request: Request):
18
+ if self.http_handler is None:
19
+ return Response(status_code=204)
20
+ http_stream = objects.HttpStream.decode(await request.body())
21
+ await self.http_handler(http_stream)
22
+ return Response(status_code=204)
23
+
24
+ @self.app.post("/tcp")
25
+ async def __on_tcp_event(request: Request):
26
+ if self.tcp_handler is None:
27
+ return Response(status_code=204)
28
+ tcp_stream = objects.TcpStream.decode(await request.body())
29
+ await self.tcp_handler(tcp_stream)
30
+ return Response(status_code=204)
31
+
32
+ def on_http_event(self):
33
+ def decorator(func: typing.Callable):
34
+ self.http_handler = func
35
+ return func
36
+
37
+ return decorator
38
+
39
+ def on_tcp_event(self):
40
+ def decorator(func: typing.Callable):
41
+ self.tcp_handler = func
42
+ return func
43
+
44
+ return decorator
45
+
46
+ def run(self):
47
+ @asynccontextmanager
48
+ async def lifespan(app: FastAPI):
49
+ yield
50
+
51
+ self.app.router.lifespan_context = lifespan
52
+ uvicorn.run(self.app, host=self.host, port=self.port, access_log=False)
@@ -0,0 +1,2 @@
1
+ from .http_stream import HttpStream, HttpEvent, HttpMethod
2
+ from .tcp_stream import TcpStream, TcpEvent
@@ -0,0 +1,68 @@
1
+ import zlib
2
+ import gzip
3
+ from enum import IntEnum
4
+ from atools.logger import logger
5
+ from atools.object_tools import Object
6
+
7
+ from ..base.reader import NetReader
8
+
9
+
10
+ class HttpEvent(IntEnum):
11
+ REQUEST = 1
12
+ RESPONSE = 2
13
+ ERROR = 3
14
+
15
+
16
+ class HttpMethod(IntEnum):
17
+ GET = 1
18
+ POST = 2
19
+ PUT = 3
20
+ DELETE = 4
21
+ HEAD = 5
22
+ OPTIONS = 6
23
+ CONNECT = 7
24
+ PATCH = 8
25
+ TRACE = 9
26
+ OTHER = 255
27
+
28
+
29
+ class HttpStream(Object):
30
+ id: int
31
+ event: HttpEvent
32
+ method: HttpMethod
33
+ url: str
34
+ status: int | None
35
+ headers: dict[str, str]
36
+ body: bytes
37
+
38
+ @staticmethod
39
+ def decode(buf: bytes) -> HttpStream:
40
+ _ = NetReader(buf)
41
+ http_stream = HttpStream()
42
+ http_stream.id = _.varint
43
+ http_stream.event = HttpEvent(_.u8)
44
+ http_stream.method = HttpMethod(_.u8)
45
+ http_stream.url = _.str(_.varint)
46
+ if http_stream.event == HttpEvent.RESPONSE:
47
+ http_stream.status = _.varint
48
+ else:
49
+ http_stream.status = None
50
+ http_stream.headers = {}
51
+ for _i_ in range(_.varint):
52
+ key = _.str(_.varint)
53
+ val = _.str(_.varint)
54
+ http_stream.headers[key] = val
55
+ http_stream.body = _.bytes(_.varint)
56
+ if _.offset != _.size:
57
+ logger.warning(f"package may be corrupted, {_.size - _.offset} trailing bytes.")
58
+ return http_stream
59
+
60
+ def auto_decompress(self):
61
+ content_encoding = next((value for key, value in self.headers.items() if key.lower() == "content-encoding"), "")
62
+ encodings = [encoding.strip().lower() for encoding in content_encoding.split(",") if encoding.strip()]
63
+ for encoding in reversed(encodings):
64
+ if encoding in {"gzip", "x-gzip"}:
65
+ try:
66
+ self.body = gzip.decompress(self.body)
67
+ except (gzip.BadGzipFile, EOFError, zlib.error) as exc:
68
+ raise ValueError("Invalid gzip-compressed HTTP body") from exc
@@ -0,0 +1,33 @@
1
+ from enum import IntEnum
2
+ from atools.object_tools import Object
3
+
4
+ from ..base.reader import NetReader
5
+
6
+
7
+ class TcpEvent(IntEnum):
8
+ ABOUT = 1
9
+ OK = 2
10
+ SEND = 3
11
+ RECEIVE = 4
12
+ CLIENT_CLOSE = 5
13
+ SERVER_CLOSE = 6
14
+ DISCONNECT = 7
15
+
16
+
17
+ class TcpStream(Object):
18
+ id: int
19
+ event: TcpEvent
20
+ address: str
21
+ chunk_id: int
22
+ body: bytes
23
+
24
+ @staticmethod
25
+ def decode(buf: bytes) -> TcpStream:
26
+ _ = NetReader(buf)
27
+ tcp_stream = TcpStream()
28
+ tcp_stream.id = _.varint
29
+ tcp_stream.event = TcpEvent(_.u8)
30
+ tcp_stream.address = _.str(_.varint)
31
+ tcp_stream.chunk_id = _.varint
32
+ tcp_stream.body = _.bytes(_.varint)
33
+ return tcp_stream
@@ -0,0 +1,20 @@
1
+ import asyncio
2
+ from . import objects
3
+
4
+
5
+ class ReliableTcpReceiver:
6
+ def __init__(self, chunk_id: int):
7
+ self.lock = asyncio.Lock()
8
+ self.next_chunk_id = chunk_id
9
+ self.pending_chunks: dict[int, objects.TcpStream] = {}
10
+
11
+ def on_tcp_packet(self, tcp_stream: objects.TcpStream):
12
+ ready = []
13
+ if tcp_stream.chunk_id < self.next_chunk_id:
14
+ return
15
+ self.pending_chunks[tcp_stream.chunk_id] = tcp_stream
16
+ while self.next_chunk_id in self.pending_chunks:
17
+ ready.append(self.pending_chunks.pop(self.next_chunk_id))
18
+ self.next_chunk_id += 1
19
+ for ordered_stream in ready:
20
+ yield ordered_stream
@@ -0,0 +1,8 @@
1
+ import re
2
+
3
+
4
+ def url_to_name(url: str):
5
+ url = re.sub(r"^https?:\/\/", "", url)
6
+ url = re.sub(r"[\?#].*$", "", url)
7
+ url = re.sub(r"[./:\\*\?\"<>|]", "_", url)
8
+ return url