adbproxy 0.1.0__py3-none-any.whl
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.
- adbproxy/__init__.py +2 -0
- adbproxy/__main__.py +5 -0
- adbproxy/cli/__init__.py +1 -0
- adbproxy/cli/device.py +13 -0
- adbproxy/cli/main.py +60 -0
- adbproxy/cli/server.py +13 -0
- adbproxy/config.py +39 -0
- adbproxy/device/__init__.py +1 -0
- adbproxy/device/app.py +532 -0
- adbproxy/device/backend.py +242 -0
- adbproxy/device/banner.py +33 -0
- adbproxy/device/connection.py +505 -0
- adbproxy/device/session.py +304 -0
- adbproxy/device/stream_table.py +125 -0
- adbproxy/observers/__init__.py +1 -0
- adbproxy/observers/events.py +19 -0
- adbproxy/observers/install.py +126 -0
- adbproxy/observers/shell.py +371 -0
- adbproxy/observers/stream.py +94 -0
- adbproxy/observers/sync.py +550 -0
- adbproxy/protocols/__init__.py +1 -0
- adbproxy/protocols/adb.py +66 -0
- adbproxy/protocols/payload/__init__.py +17 -0
- adbproxy/protocols/payload/install.py +44 -0
- adbproxy/protocols/payload/shell_v2.py +28 -0
- adbproxy/protocols/payload/sync.py +180 -0
- adbproxy/protocols/smart_socket/__init__.py +33 -0
- adbproxy/protocols/smart_socket/classifier.py +78 -0
- adbproxy/protocols/smart_socket/conversation.py +335 -0
- adbproxy/protocols/smart_socket/handshake.py +75 -0
- adbproxy/protocols/smart_socket/wire.py +82 -0
- adbproxy/protocols/transport/__init__.py +52 -0
- adbproxy/protocols/transport/codec.py +90 -0
- adbproxy/protocols/transport/constants.py +21 -0
- adbproxy/protocols/transport/errors.py +5 -0
- adbproxy/runtime/__init__.py +1 -0
- adbproxy/runtime/adb_process.py +195 -0
- adbproxy/runtime/capture.py +41 -0
- adbproxy/runtime/logging.py +113 -0
- adbproxy/runtime/sockets.py +14 -0
- adbproxy/server/__init__.py +1 -0
- adbproxy/server/app.py +221 -0
- adbproxy/server/connection.py +378 -0
- adbproxy-0.1.0.dist-info/METADATA +254 -0
- adbproxy-0.1.0.dist-info/RECORD +49 -0
- adbproxy-0.1.0.dist-info/WHEEL +5 -0
- adbproxy-0.1.0.dist-info/entry_points.txt +4 -0
- adbproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- adbproxy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Pure shell_v2 framing: [1B id][4B LE len][data]."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
from adbproxy.config import MAX_V2_BUF
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def split_v2_frames(buf):
|
|
11
|
+
"""切分 v2 帧 [1B id][4B LE len][data]。
|
|
12
|
+
|
|
13
|
+
返回 (frames:list[(id,payload)], leftover:bytearray, ok:bool)。
|
|
14
|
+
ok=False 表示遇到非法 len,调用方应降级为碎片流。
|
|
15
|
+
"""
|
|
16
|
+
frames = []
|
|
17
|
+
pos = 0
|
|
18
|
+
n = len(buf)
|
|
19
|
+
while pos + 5 <= n:
|
|
20
|
+
id_ = buf[pos]
|
|
21
|
+
length = struct.unpack_from("<I", buf, pos + 1)[0]
|
|
22
|
+
if length > MAX_V2_BUF:
|
|
23
|
+
return (frames, bytearray(buf[pos:]), False)
|
|
24
|
+
if pos + 5 + length > n:
|
|
25
|
+
break # 半包,等后续
|
|
26
|
+
frames.append((id_, bytes(buf[pos + 5 : pos + 5 + length])))
|
|
27
|
+
pos += 5 + length
|
|
28
|
+
return (frames, bytearray(buf[pos:]), True)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Pure sync wire framing (file_sync_protocol.h shapes).
|
|
2
|
+
|
|
3
|
+
No capture, no compression, no I/O. One SyncWireDecoder per direction.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from adbproxy.config import (
|
|
12
|
+
MAX_V2_BUF,
|
|
13
|
+
SYNC_DATA_MAX,
|
|
14
|
+
_DENT_V1_SIZE,
|
|
15
|
+
_DENT_V2_SIZE,
|
|
16
|
+
_RECV_V2_SIZE,
|
|
17
|
+
_SEND_V2_SIZE,
|
|
18
|
+
_STAT_V1_SIZE,
|
|
19
|
+
_STAT_V2_SIZE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SyncFrame:
|
|
25
|
+
"""One complete sync frame (id + full raw frame bytes including id)."""
|
|
26
|
+
|
|
27
|
+
id: bytes
|
|
28
|
+
raw: bytes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def plan_sync_frame(id_: bytes, side: str, awaiting_details: str | None):
|
|
32
|
+
"""返回帧分帧方案:("var",) / ("fixed", n) / ("dent", header, namelen_off) / None(未知)。
|
|
33
|
+
|
|
34
|
+
依赖 (id, side, awaiting_details) 区分 SND2/RCV2 双发的 path 帧 vs details struct。
|
|
35
|
+
"""
|
|
36
|
+
if side == "c":
|
|
37
|
+
# client→server:均为请求。路径请求类(8+path_length+path)
|
|
38
|
+
if id_ in (b"STA2", b"LST2", b"STAT", b"LIST", b"SEND", b"RECV"):
|
|
39
|
+
return ("var",)
|
|
40
|
+
if id_ == b"SND2":
|
|
41
|
+
# 双发第二帧 = sync_send_v2(12B 定长);否则 = path 请求
|
|
42
|
+
if awaiting_details == "send":
|
|
43
|
+
return ("fixed", _SEND_V2_SIZE)
|
|
44
|
+
return ("var",)
|
|
45
|
+
if id_ == b"RCV2":
|
|
46
|
+
if awaiting_details == "recv":
|
|
47
|
+
return ("fixed", _RECV_V2_SIZE)
|
|
48
|
+
return ("var",)
|
|
49
|
+
if id_ == b"DATA":
|
|
50
|
+
return ("var",)
|
|
51
|
+
if id_ in (b"DONE", b"QUIT"):
|
|
52
|
+
return ("fixed", 8)
|
|
53
|
+
if id_ == b"FAIL":
|
|
54
|
+
return ("var",)
|
|
55
|
+
return None
|
|
56
|
+
else: # server→client:均为响应
|
|
57
|
+
if id_ in (b"STA2", b"LST2"):
|
|
58
|
+
return ("fixed", _STAT_V2_SIZE) # sync_stat_v2 定长
|
|
59
|
+
if id_ == b"STAT":
|
|
60
|
+
return ("fixed", _STAT_V1_SIZE) # sync_stat_v1 定长
|
|
61
|
+
if id_ == b"DNT2":
|
|
62
|
+
return (
|
|
63
|
+
"dent",
|
|
64
|
+
_DENT_V2_SIZE,
|
|
65
|
+
_DENT_V2_SIZE - 4,
|
|
66
|
+
) # 76B 头 + namelen name
|
|
67
|
+
if id_ == b"DENT":
|
|
68
|
+
return (
|
|
69
|
+
"dent",
|
|
70
|
+
_DENT_V1_SIZE,
|
|
71
|
+
_DENT_V1_SIZE - 4,
|
|
72
|
+
) # 20B 头 + namelen name
|
|
73
|
+
if id_ == b"DATA":
|
|
74
|
+
return ("var",)
|
|
75
|
+
if id_ in (b"DONE", b"OKAY", b"QUIT"):
|
|
76
|
+
return ("fixed", 8)
|
|
77
|
+
if id_ == b"FAIL":
|
|
78
|
+
return ("var",)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SyncWireDecoder:
|
|
83
|
+
"""Incremental per-direction sync framer.
|
|
84
|
+
|
|
85
|
+
Prefer ``try_take_frame(plan_fn)`` so session can update ``awaiting_details``
|
|
86
|
+
between frames inside one TCP chunk (SND2/RCV2 dual-send).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self.buf = bytearray()
|
|
91
|
+
self.bypass = False
|
|
92
|
+
self.degrade_reason: str | None = None
|
|
93
|
+
|
|
94
|
+
def reset(self):
|
|
95
|
+
self.buf = bytearray()
|
|
96
|
+
self.bypass = False
|
|
97
|
+
self.degrade_reason = None
|
|
98
|
+
|
|
99
|
+
def extend(self, data: bytes | bytearray | memoryview) -> None:
|
|
100
|
+
if data and not self.bypass:
|
|
101
|
+
self.buf.extend(data)
|
|
102
|
+
|
|
103
|
+
def try_take_frame(self, plan_fn) -> SyncFrame | None:
|
|
104
|
+
"""Extract at most one complete frame.
|
|
105
|
+
|
|
106
|
+
Returns SyncFrame when a full frame is available, None when more data is
|
|
107
|
+
needed. On framing errors sets ``bypass`` and returns None.
|
|
108
|
+
``plan_fn(id_)`` must return plan_sync_frame-style plan or None.
|
|
109
|
+
"""
|
|
110
|
+
if self.bypass:
|
|
111
|
+
return None
|
|
112
|
+
if len(self.buf) < 4:
|
|
113
|
+
return None
|
|
114
|
+
id_ = bytes(self.buf[:4])
|
|
115
|
+
plan = plan_fn(id_)
|
|
116
|
+
if plan is None:
|
|
117
|
+
self.bypass = True
|
|
118
|
+
self.degrade_reason = f"unknown id {id_!r}"
|
|
119
|
+
return None
|
|
120
|
+
kind = plan[0]
|
|
121
|
+
if kind == "var":
|
|
122
|
+
if len(self.buf) < 8:
|
|
123
|
+
return None
|
|
124
|
+
length = struct.unpack_from("<I", self.buf, 4)[0]
|
|
125
|
+
length_limit = SYNC_DATA_MAX if id_ == b"DATA" else MAX_V2_BUF
|
|
126
|
+
if length > length_limit:
|
|
127
|
+
self.bypass = True
|
|
128
|
+
self.degrade_reason = (
|
|
129
|
+
f"var len too large id={id_!r} len={length} limit={length_limit}"
|
|
130
|
+
)
|
|
131
|
+
return None
|
|
132
|
+
if len(self.buf) < 8 + length:
|
|
133
|
+
return None
|
|
134
|
+
raw = bytes(self.buf[: 8 + length])
|
|
135
|
+
del self.buf[: 8 + length]
|
|
136
|
+
return SyncFrame(id=id_, raw=raw)
|
|
137
|
+
if kind == "fixed":
|
|
138
|
+
size = plan[1]
|
|
139
|
+
if len(self.buf) < size:
|
|
140
|
+
return None
|
|
141
|
+
raw = bytes(self.buf[:size])
|
|
142
|
+
del self.buf[:size]
|
|
143
|
+
return SyncFrame(id=id_, raw=raw)
|
|
144
|
+
# dent: header + namelen name
|
|
145
|
+
header_size = plan[1]
|
|
146
|
+
namelen_off = plan[2]
|
|
147
|
+
if len(self.buf) < header_size:
|
|
148
|
+
return None
|
|
149
|
+
namelen = struct.unpack_from("<I", self.buf, namelen_off)[0]
|
|
150
|
+
if namelen > MAX_V2_BUF:
|
|
151
|
+
self.bypass = True
|
|
152
|
+
self.degrade_reason = (
|
|
153
|
+
f"dent namelen too large id={id_!r} namelen={namelen}"
|
|
154
|
+
)
|
|
155
|
+
return None
|
|
156
|
+
if len(self.buf) < header_size + namelen:
|
|
157
|
+
return None
|
|
158
|
+
raw = bytes(self.buf[: header_size + namelen])
|
|
159
|
+
del self.buf[: header_size + namelen]
|
|
160
|
+
return SyncFrame(id=id_, raw=raw)
|
|
161
|
+
|
|
162
|
+
def feed(
|
|
163
|
+
self,
|
|
164
|
+
data: bytes | bytearray | memoryview,
|
|
165
|
+
side: str,
|
|
166
|
+
awaiting_details: str | None = None,
|
|
167
|
+
) -> list[SyncFrame]:
|
|
168
|
+
"""Append data and emit all complete frames under a fixed awaiting snapshot.
|
|
169
|
+
|
|
170
|
+
For dual-send mid-chunk plan flips, use try_take_frame with a live plan_fn.
|
|
171
|
+
"""
|
|
172
|
+
self.extend(data)
|
|
173
|
+
frames: list[SyncFrame] = []
|
|
174
|
+
while True:
|
|
175
|
+
frame = self.try_take_frame(
|
|
176
|
+
lambda id_, s=side, a=awaiting_details: plan_sync_frame(id_, s, a)
|
|
177
|
+
)
|
|
178
|
+
if frame is None:
|
|
179
|
+
return frames
|
|
180
|
+
frames.append(frame)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Smart-socket protocol primitives (wire / conversation / handshake)."""
|
|
2
|
+
|
|
3
|
+
from adbproxy.protocols.smart_socket.classifier import ServiceClassifier
|
|
4
|
+
from adbproxy.protocols.smart_socket.conversation import SmartEvent, SmartSocketConversation
|
|
5
|
+
from adbproxy.protocols.smart_socket.handshake import HandshakeStatus, SmartSocketHandshake
|
|
6
|
+
from adbproxy.protocols.smart_socket.wire import (
|
|
7
|
+
FAIL,
|
|
8
|
+
OKAY,
|
|
9
|
+
decode_hex4,
|
|
10
|
+
encode_hex4,
|
|
11
|
+
encode_protocol_string,
|
|
12
|
+
format_service_frame,
|
|
13
|
+
parse_status,
|
|
14
|
+
try_parse_hex4_length_prefix,
|
|
15
|
+
try_parse_status,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"FAIL",
|
|
20
|
+
"OKAY",
|
|
21
|
+
"HandshakeStatus",
|
|
22
|
+
"ServiceClassifier",
|
|
23
|
+
"SmartEvent",
|
|
24
|
+
"SmartSocketConversation",
|
|
25
|
+
"SmartSocketHandshake",
|
|
26
|
+
"decode_hex4",
|
|
27
|
+
"encode_hex4",
|
|
28
|
+
"encode_protocol_string",
|
|
29
|
+
"format_service_frame",
|
|
30
|
+
"parse_status",
|
|
31
|
+
"try_parse_hex4_length_prefix",
|
|
32
|
+
"try_parse_status",
|
|
33
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Service classification for smart-socket service strings.
|
|
2
|
+
|
|
3
|
+
Pure helpers: no I/O. Used by server conversation policy, device open path,
|
|
4
|
+
and observers. Install grammar lives in payload.install; classifier re-exports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from adbproxy.protocols.payload.install import (
|
|
10
|
+
is_install_stream_service as _is_install_stream_service,
|
|
11
|
+
)
|
|
12
|
+
from adbproxy.protocols.payload.install import (
|
|
13
|
+
parse_install_size as _parse_install_size,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServiceClassifier:
|
|
18
|
+
"""Classify adb smart-socket service strings (host / shell / sync / install)."""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def parse_service(svc: str):
|
|
22
|
+
"""拆 service 串 -> (kind, args_set, cmd)。
|
|
23
|
+
|
|
24
|
+
kind ∈ {"host","shell","sync","other"};args_set 含 raw/pty/v2 等 flag;
|
|
25
|
+
cmd 为命令文本。shell 真实语法:shell[,args]:cmd(无 v1,v2 是 flag)。
|
|
26
|
+
"""
|
|
27
|
+
if svc.startswith("host:"):
|
|
28
|
+
return ("host", set(), svc)
|
|
29
|
+
if svc.startswith("sync:") or svc == "sync":
|
|
30
|
+
return ("sync", set(), svc)
|
|
31
|
+
if svc.startswith("shell"):
|
|
32
|
+
rest = svc[len("shell") :]
|
|
33
|
+
if rest.startswith(":"):
|
|
34
|
+
return ("shell", set(), rest[1:])
|
|
35
|
+
if rest.startswith(","):
|
|
36
|
+
colon = rest.find(":")
|
|
37
|
+
if colon == -1:
|
|
38
|
+
return ("shell", set(a for a in rest[1:].split(",") if a), "")
|
|
39
|
+
args = set(a for a in rest[1:colon].split(",") if a)
|
|
40
|
+
return ("shell", args, rest[colon + 1 :])
|
|
41
|
+
return ("shell", set(), "")
|
|
42
|
+
return ("other", set(), svc)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def is_host_service(svc: str | None) -> bool:
|
|
46
|
+
# AOSP host-services 家族(host:xxx / host-serial: / host-internal: 等)
|
|
47
|
+
return svc is not None and (svc.startswith("host:") or svc.startswith("host-"))
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def host_response_kind(svc: str | None) -> str:
|
|
51
|
+
"""按 AOSP host service 选择响应 wire shape。"""
|
|
52
|
+
if not ServiceClassifier.is_host_service(svc):
|
|
53
|
+
return "stream"
|
|
54
|
+
if ":track-devices" in svc:
|
|
55
|
+
return "track"
|
|
56
|
+
if svc.startswith("host:tport:"):
|
|
57
|
+
return "fixed8"
|
|
58
|
+
if ":forward:" in svc or ":killforward:" in svc:
|
|
59
|
+
return "forward"
|
|
60
|
+
if svc.startswith("host:transport") or svc == "host:kill":
|
|
61
|
+
return "status"
|
|
62
|
+
return "protocol_string"
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def is_shell_service(svc: str | None) -> bool:
|
|
66
|
+
return svc is not None and svc.startswith("shell")
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def is_sync_service(svc: str | None) -> bool:
|
|
70
|
+
return svc is not None and (svc.startswith("sync:") or svc == "sync")
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def is_install_stream_service(svc: str | None) -> bool:
|
|
74
|
+
return _is_install_stream_service(svc)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def parse_install_size(svc: str | None) -> int | None:
|
|
78
|
+
return _parse_install_size(svc)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Server-side smart-socket conversation policy (pure bypass, no I/O).
|
|
2
|
+
|
|
3
|
+
Incremental multi-shape request/response observation.
|
|
4
|
+
|
|
5
|
+
Connection orchestration (not this module):
|
|
6
|
+
- I2: service classify / set_mode / activate may run **pre-sendall** via
|
|
7
|
+
``feed_request`` / ``try_parse_service_frame`` (parse single-source only).
|
|
8
|
+
- I1/I3: ``sendall(original)`` must not be skipped on observe errors; payload
|
|
9
|
+
``add_*`` runs **post-sendall** (fail-open).
|
|
10
|
+
- Response path typically ``sendall`` then ``feed_response``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from adbproxy.protocols.smart_socket.classifier import ServiceClassifier
|
|
19
|
+
from adbproxy.protocols.smart_socket.wire import FAIL, OKAY
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class SmartEvent:
|
|
24
|
+
"""Observation event emitted by SmartSocketConversation."""
|
|
25
|
+
|
|
26
|
+
kind: str
|
|
27
|
+
# kind values:
|
|
28
|
+
# service_request — svc=str, leftover=bytes
|
|
29
|
+
# status — status=str (OKAY/FAIL)
|
|
30
|
+
# fail_reason — reason=bytes
|
|
31
|
+
# payload — data=bytes (host/shell/sync leftover after status strip)
|
|
32
|
+
# host_payload — data=bytes (alias of payload for host multi-shape)
|
|
33
|
+
data: Any = None
|
|
34
|
+
leftover: bytes = b""
|
|
35
|
+
status: str | None = None
|
|
36
|
+
reason: bytes | None = None
|
|
37
|
+
svc: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SmartSocketConversation:
|
|
41
|
+
"""Per-TCP-connection pure smart-socket observer state.
|
|
42
|
+
|
|
43
|
+
Request path: reassemble ``<hex4><service>`` frames across TCP fragments.
|
|
44
|
+
Response path: multi-shape host policy + first-response OKAY/FAIL strip for
|
|
45
|
+
shell/sync streams. No sockets/threads/subprocess.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
# client request reassembly
|
|
50
|
+
self._service_request_buf = bytearray()
|
|
51
|
+
# shell / sync first response (independent)
|
|
52
|
+
self._shell_first_response = self._new_first_response_state()
|
|
53
|
+
self._sync_first_response = self._new_first_response_state()
|
|
54
|
+
# host multi-shape SM
|
|
55
|
+
self.host_phase = "status"
|
|
56
|
+
self.host_status_buf = bytearray()
|
|
57
|
+
self.host_hex_buf = bytearray()
|
|
58
|
+
self.host_payload_remaining = 0
|
|
59
|
+
self.host_fixed_remaining = 0
|
|
60
|
+
self.host_track_active = False
|
|
61
|
+
self._last_host_svc = None
|
|
62
|
+
self.svc = None # last observed service string (mirrors Connection.svc)
|
|
63
|
+
self._status_events: list[str] = []
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _new_first_response_state():
|
|
67
|
+
return {"phase": "status", "buf": bytearray(), "remaining": 0}
|
|
68
|
+
|
|
69
|
+
def reset_host_state(self):
|
|
70
|
+
self.host_phase = "status"
|
|
71
|
+
self.host_status_buf = bytearray()
|
|
72
|
+
self.host_hex_buf = bytearray()
|
|
73
|
+
self.host_payload_remaining = 0
|
|
74
|
+
self.host_fixed_remaining = 0
|
|
75
|
+
self.host_track_active = False
|
|
76
|
+
|
|
77
|
+
def try_parse_service_frame(self, data: bytes):
|
|
78
|
+
"""尝试解析一帧 4-hex 长度 + service 串(纯解析,无副作用除缓冲)。
|
|
79
|
+
|
|
80
|
+
返回 (svc:str, leftover:bytes) 或 None(数据不足/非协议)。
|
|
81
|
+
"""
|
|
82
|
+
self._service_request_buf.extend(data)
|
|
83
|
+
if len(self._service_request_buf) < 4:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
cmd_len = int(self._service_request_buf[:4].decode("ascii"), 16)
|
|
87
|
+
except ValueError:
|
|
88
|
+
# 非 smart-socket 数据不应长期占用观察缓冲
|
|
89
|
+
self._service_request_buf = bytearray()
|
|
90
|
+
return None
|
|
91
|
+
frame_size = 4 + cmd_len
|
|
92
|
+
if len(self._service_request_buf) < frame_size:
|
|
93
|
+
return None # 半包
|
|
94
|
+
svc = self._service_request_buf[4:frame_size].decode("ascii", errors="ignore")
|
|
95
|
+
leftover = bytes(self._service_request_buf[frame_size:])
|
|
96
|
+
self._service_request_buf = bytearray()
|
|
97
|
+
self.svc = svc
|
|
98
|
+
return (svc, leftover)
|
|
99
|
+
|
|
100
|
+
def feed_request(self, data: bytes) -> list[SmartEvent]:
|
|
101
|
+
"""Observe client→server bytes. Returns service_request events when a frame completes."""
|
|
102
|
+
events: list[SmartEvent] = []
|
|
103
|
+
parsed = self.try_parse_service_frame(data)
|
|
104
|
+
if parsed is not None:
|
|
105
|
+
svc, leftover = parsed
|
|
106
|
+
events.append(
|
|
107
|
+
SmartEvent(
|
|
108
|
+
kind="service_request",
|
|
109
|
+
svc=svc,
|
|
110
|
+
leftover=leftover,
|
|
111
|
+
data=svc,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
return events
|
|
115
|
+
|
|
116
|
+
def feed_response(
|
|
117
|
+
self,
|
|
118
|
+
data: bytes,
|
|
119
|
+
*,
|
|
120
|
+
shell_active: bool = False,
|
|
121
|
+
shell_okay_consumed: bool = False,
|
|
122
|
+
sync_active: bool = False,
|
|
123
|
+
sync_okay_consumed: bool = False,
|
|
124
|
+
svc: str | None = None,
|
|
125
|
+
) -> tuple[list[SmartEvent], bool]:
|
|
126
|
+
"""Observe server→client bytes under current stream activation flags.
|
|
127
|
+
|
|
128
|
+
Returns ``(events, complete)`` where events may include status /
|
|
129
|
+
fail_reason / payload. Does not mutate sockets. Host status words are
|
|
130
|
+
both appended as status events and left in ``_status_events`` until the
|
|
131
|
+
connection drains that side-channel (S5).
|
|
132
|
+
"""
|
|
133
|
+
if svc is not None:
|
|
134
|
+
self.svc = svc
|
|
135
|
+
events: list[SmartEvent] = []
|
|
136
|
+
|
|
137
|
+
if shell_active and not shell_okay_consumed:
|
|
138
|
+
complete, leftover = self.consume_first_response(
|
|
139
|
+
data, self._shell_first_response, emit=events
|
|
140
|
+
)
|
|
141
|
+
if complete and leftover:
|
|
142
|
+
events.append(SmartEvent(kind="payload", data=leftover))
|
|
143
|
+
return events, complete
|
|
144
|
+
if shell_active:
|
|
145
|
+
events.append(SmartEvent(kind="payload", data=data))
|
|
146
|
+
return events, True
|
|
147
|
+
|
|
148
|
+
if sync_active and not sync_okay_consumed:
|
|
149
|
+
complete, leftover = self.consume_first_response(
|
|
150
|
+
data, self._sync_first_response, emit=events
|
|
151
|
+
)
|
|
152
|
+
if complete and leftover:
|
|
153
|
+
events.append(SmartEvent(kind="payload", data=leftover))
|
|
154
|
+
return events, complete
|
|
155
|
+
if sync_active:
|
|
156
|
+
events.append(SmartEvent(kind="payload", data=data))
|
|
157
|
+
return events, True
|
|
158
|
+
|
|
159
|
+
# host / other
|
|
160
|
+
if ServiceClassifier.is_host_service(self.svc):
|
|
161
|
+
leftover = self.parse_host_response(data)
|
|
162
|
+
for st in self._status_events:
|
|
163
|
+
events.append(SmartEvent(kind="status", status=st, data=st))
|
|
164
|
+
else:
|
|
165
|
+
# Stateless first-status strip for non-host; mirror loggable status.
|
|
166
|
+
if data.startswith(OKAY) or data.startswith(FAIL):
|
|
167
|
+
status = data[:4].decode("ascii", errors="ignore")
|
|
168
|
+
events.append(SmartEvent(kind="status", status=status, data=status))
|
|
169
|
+
leftover = self.parse_server_first(data)
|
|
170
|
+
if leftover:
|
|
171
|
+
events.append(SmartEvent(kind="payload", data=leftover))
|
|
172
|
+
return events, True
|
|
173
|
+
|
|
174
|
+
def consume_first_response(self, data, state, emit: list[SmartEvent] | None = None):
|
|
175
|
+
"""重组服务首响应,确保 OKAY/FAIL 及 FAIL protocol-string 不进入 payload。"""
|
|
176
|
+
buf = state["buf"]
|
|
177
|
+
buf.extend(data)
|
|
178
|
+
status_events = emit if emit is not None else []
|
|
179
|
+
|
|
180
|
+
while True:
|
|
181
|
+
if state["phase"] == "status":
|
|
182
|
+
if len(buf) < 4:
|
|
183
|
+
return False, b""
|
|
184
|
+
status = bytes(buf[:4])
|
|
185
|
+
del buf[:4]
|
|
186
|
+
if status not in (OKAY, FAIL):
|
|
187
|
+
payload = status + bytes(buf)
|
|
188
|
+
buf.clear()
|
|
189
|
+
state["phase"] = "done"
|
|
190
|
+
return True, payload
|
|
191
|
+
status_events.append(
|
|
192
|
+
SmartEvent(kind="status", status=status.decode("ascii"), data=status)
|
|
193
|
+
)
|
|
194
|
+
if status == OKAY:
|
|
195
|
+
payload = bytes(buf)
|
|
196
|
+
buf.clear()
|
|
197
|
+
state["phase"] = "done"
|
|
198
|
+
return True, payload
|
|
199
|
+
state["phase"] = "fail_length"
|
|
200
|
+
elif state["phase"] == "fail_length":
|
|
201
|
+
if len(buf) < 4:
|
|
202
|
+
return False, b""
|
|
203
|
+
raw_length = bytes(buf[:4])
|
|
204
|
+
del buf[:4]
|
|
205
|
+
try:
|
|
206
|
+
state["remaining"] = int(raw_length.decode("ascii"), 16)
|
|
207
|
+
except ValueError:
|
|
208
|
+
payload = raw_length + bytes(buf)
|
|
209
|
+
buf.clear()
|
|
210
|
+
state["phase"] = "done"
|
|
211
|
+
return True, payload
|
|
212
|
+
state["phase"] = "fail_reason"
|
|
213
|
+
elif state["phase"] == "fail_reason":
|
|
214
|
+
remaining = state["remaining"]
|
|
215
|
+
if len(buf) < remaining:
|
|
216
|
+
return False, b""
|
|
217
|
+
reason = bytes(buf[:remaining])
|
|
218
|
+
del buf[:remaining]
|
|
219
|
+
status_events.append(SmartEvent(kind="fail_reason", reason=reason, data=reason))
|
|
220
|
+
payload = bytes(buf)
|
|
221
|
+
buf.clear()
|
|
222
|
+
state["phase"] = "done"
|
|
223
|
+
return True, payload
|
|
224
|
+
else:
|
|
225
|
+
payload = bytes(buf)
|
|
226
|
+
buf.clear()
|
|
227
|
+
return True, payload
|
|
228
|
+
|
|
229
|
+
def parse_server_first(self, data: bytes) -> bytes:
|
|
230
|
+
"""非 host 服务的状态剥除(stateless 兜底)。"""
|
|
231
|
+
if not (data.startswith(OKAY) or data.startswith(FAIL)):
|
|
232
|
+
return data
|
|
233
|
+
# status word present; leftover may include host hex for host services
|
|
234
|
+
leftover = data[4:]
|
|
235
|
+
if ServiceClassifier.is_host_service(self.svc) and len(leftover) >= 4:
|
|
236
|
+
hex_str = leftover[:4].decode("ascii", errors="ignore")
|
|
237
|
+
if len(hex_str) == 4 and all(
|
|
238
|
+
c in "0123456789abcdefABCDEF" for c in hex_str
|
|
239
|
+
):
|
|
240
|
+
leftover = leftover[4:]
|
|
241
|
+
return leftover
|
|
242
|
+
|
|
243
|
+
def parse_host_response(self, data: bytes) -> bytes:
|
|
244
|
+
"""按 service wire shape 增量消费 host 响应,返回纯 payload bytes。
|
|
245
|
+
|
|
246
|
+
Status words consumed during this call are appended to ``_status_events``
|
|
247
|
+
for the connection logger (fail-open observer side-channel).
|
|
248
|
+
"""
|
|
249
|
+
self._status_events = []
|
|
250
|
+
# host 服务 svc 变化时复位状态机
|
|
251
|
+
if self._last_host_svc != self.svc:
|
|
252
|
+
self.reset_host_state()
|
|
253
|
+
self._last_host_svc = self.svc
|
|
254
|
+
out = bytearray()
|
|
255
|
+
pos = 0
|
|
256
|
+
n = len(data)
|
|
257
|
+
response_kind = ServiceClassifier.host_response_kind(self.svc)
|
|
258
|
+
while pos < n:
|
|
259
|
+
if self.host_phase == "status":
|
|
260
|
+
need = 4 - len(self.host_status_buf)
|
|
261
|
+
take = min(need, n - pos)
|
|
262
|
+
self.host_status_buf.extend(data[pos : pos + take])
|
|
263
|
+
pos += take
|
|
264
|
+
if len(self.host_status_buf) < 4:
|
|
265
|
+
return bytes(out)
|
|
266
|
+
status = bytes(self.host_status_buf).decode("ascii", errors="ignore")
|
|
267
|
+
self.host_status_buf = bytearray()
|
|
268
|
+
if status in ("OKAY", "FAIL"):
|
|
269
|
+
self._status_events.append(status)
|
|
270
|
+
self.host_track_active = status == "OKAY" and response_kind == "track"
|
|
271
|
+
if status == "FAIL":
|
|
272
|
+
self.host_phase = "hex"
|
|
273
|
+
elif response_kind == "status":
|
|
274
|
+
self.host_phase = "done"
|
|
275
|
+
elif response_kind == "fixed8":
|
|
276
|
+
self.host_fixed_remaining = 8
|
|
277
|
+
self.host_phase = "fixed"
|
|
278
|
+
elif response_kind == "forward":
|
|
279
|
+
self.host_phase = "second_status"
|
|
280
|
+
else:
|
|
281
|
+
self.host_phase = "hex"
|
|
282
|
+
else:
|
|
283
|
+
out.extend(status.encode("ascii", errors="ignore"))
|
|
284
|
+
self.host_phase = "done"
|
|
285
|
+
elif self.host_phase == "second_status":
|
|
286
|
+
need = 4 - len(self.host_status_buf)
|
|
287
|
+
take = min(need, n - pos)
|
|
288
|
+
self.host_status_buf.extend(data[pos : pos + take])
|
|
289
|
+
pos += take
|
|
290
|
+
if len(self.host_status_buf) < 4:
|
|
291
|
+
return bytes(out)
|
|
292
|
+
status = bytes(self.host_status_buf).decode("ascii", errors="ignore")
|
|
293
|
+
self.host_status_buf = bytearray()
|
|
294
|
+
if status in ("OKAY", "FAIL"):
|
|
295
|
+
self._status_events.append(status)
|
|
296
|
+
self.host_phase = "hex"
|
|
297
|
+
else:
|
|
298
|
+
out.extend(status.encode("ascii", errors="ignore"))
|
|
299
|
+
self.host_phase = "done"
|
|
300
|
+
elif self.host_phase == "fixed":
|
|
301
|
+
take = min(self.host_fixed_remaining, n - pos)
|
|
302
|
+
out.extend(data[pos : pos + take])
|
|
303
|
+
pos += take
|
|
304
|
+
self.host_fixed_remaining -= take
|
|
305
|
+
if self.host_fixed_remaining == 0:
|
|
306
|
+
self.host_phase = "done"
|
|
307
|
+
elif self.host_phase == "hex":
|
|
308
|
+
need = 4 - len(self.host_hex_buf)
|
|
309
|
+
take = min(need, n - pos)
|
|
310
|
+
self.host_hex_buf.extend(data[pos : pos + take])
|
|
311
|
+
pos += take
|
|
312
|
+
if len(self.host_hex_buf) < 4:
|
|
313
|
+
return bytes(out)
|
|
314
|
+
hex_str = bytes(self.host_hex_buf).decode("ascii", errors="ignore")
|
|
315
|
+
self.host_hex_buf = bytearray()
|
|
316
|
+
if all(c in "0123456789abcdefABCDEF" for c in hex_str):
|
|
317
|
+
self.host_payload_remaining = int(hex_str, 16)
|
|
318
|
+
self.host_phase = "payload"
|
|
319
|
+
else:
|
|
320
|
+
out.extend(hex_str.encode("ascii", errors="ignore"))
|
|
321
|
+
self.host_phase = "done"
|
|
322
|
+
elif self.host_phase == "payload":
|
|
323
|
+
if self.host_payload_remaining == 0:
|
|
324
|
+
self.host_phase = "hex" if self.host_track_active else "status"
|
|
325
|
+
continue
|
|
326
|
+
take = min(self.host_payload_remaining, n - pos)
|
|
327
|
+
out.extend(data[pos : pos + take])
|
|
328
|
+
pos += take
|
|
329
|
+
self.host_payload_remaining -= take
|
|
330
|
+
if self.host_payload_remaining == 0:
|
|
331
|
+
self.host_phase = "hex" if self.host_track_active else "status"
|
|
332
|
+
else: # "done"
|
|
333
|
+
out.extend(data[pos:])
|
|
334
|
+
pos = n
|
|
335
|
+
return bytes(out)
|