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.
- any_analysis-0.1.0/.gitignore +8 -0
- any_analysis-0.1.0/LICENSE.txt +21 -0
- any_analysis-0.1.0/PKG-INFO +28 -0
- any_analysis-0.1.0/README.md +4 -0
- any_analysis-0.1.0/pyproject.toml +33 -0
- any_analysis-0.1.0/src/any_analysis/__about__.py +1 -0
- any_analysis-0.1.0/src/any_analysis/__init__.py +4 -0
- any_analysis-0.1.0/src/any_analysis/base/reader.py +20 -0
- any_analysis-0.1.0/src/any_analysis/capture.py +52 -0
- any_analysis-0.1.0/src/any_analysis/objects/__init__.py +2 -0
- any_analysis-0.1.0/src/any_analysis/objects/http_stream.py +68 -0
- any_analysis-0.1.0/src/any_analysis/objects/tcp_stream.py +33 -0
- any_analysis-0.1.0/src/any_analysis/receiver.py +20 -0
- any_analysis-0.1.0/src/any_analysis/utils.py +8 -0
|
@@ -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,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,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,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
|