fustor-receiver-http 0.8.1__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.
- fustor_receiver_http-0.8.1/PKG-INFO +86 -0
- fustor_receiver_http-0.8.1/README.md +70 -0
- fustor_receiver_http-0.8.1/pyproject.toml +40 -0
- fustor_receiver_http-0.8.1/setup.cfg +4 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http/__init__.py +375 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/PKG-INFO +86 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/SOURCES.txt +9 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/dependency_links.txt +1 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/entry_points.txt +2 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/requires.txt +3 -0
- fustor_receiver_http-0.8.1/src/fustor_receiver_http.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-receiver-http
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: Fustor HTTP Receiver - Transport layer for Fusion to receive events from Agents
|
|
5
|
+
Author-email: Huajin Wang <wanghuajin999@163.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/excelwang/fustor/tree/master/extensions/receiver-http
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/excelwang/fustor/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: fustor-core
|
|
14
|
+
Requires-Dist: fastapi>=0.109.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
|
|
17
|
+
# fustor-receiver-http
|
|
18
|
+
|
|
19
|
+
HTTP Receiver for Fustor Fusion - implements the transport layer for receiving events from Agents.
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
This package provides an HTTP-based implementation of the `Receiver` transport abstraction. It creates FastAPI routers that handle session management and event ingestion.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install fustor-receiver-http
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from fustor_receiver_http import HTTPReceiver, SessionInfo
|
|
35
|
+
|
|
36
|
+
# Create receiver
|
|
37
|
+
receiver = HTTPReceiver(
|
|
38
|
+
receiver_id="main-receiver",
|
|
39
|
+
config={"session_timeout_seconds": 30}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Register API keys for pipes
|
|
43
|
+
receiver.register_api_key("fk_abc123", "pipe-1")
|
|
44
|
+
|
|
45
|
+
# Register callbacks
|
|
46
|
+
async def on_session_created(session_id, task_id, pipe_id, client_info):
|
|
47
|
+
# Handle session creation
|
|
48
|
+
return SessionInfo(
|
|
49
|
+
session_id=session_id,
|
|
50
|
+
task_id=task_id,
|
|
51
|
+
pipe_id=pipe_id,
|
|
52
|
+
role="leader",
|
|
53
|
+
created_at=time.time(),
|
|
54
|
+
last_heartbeat=time.time()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def on_event_received(session_id, events, source_type, is_end):
|
|
58
|
+
# Process events
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
receiver.register_callbacks(
|
|
62
|
+
on_session_created=on_session_created,
|
|
63
|
+
on_event_received=on_event_received,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Mount routers in FastAPI app
|
|
67
|
+
app.include_router(receiver.get_session_router(), prefix="/api/v1/pipe/session")
|
|
68
|
+
app.include_router(receiver.get_ingestion_router(), prefix="/api/v1/pipe/ingest")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API Endpoints
|
|
72
|
+
|
|
73
|
+
### Session Router
|
|
74
|
+
|
|
75
|
+
- `POST /` - Create a new session
|
|
76
|
+
- `POST /{session_id}/heartbeat` - Send heartbeat
|
|
77
|
+
- `DELETE /{session_id}` - Terminate session
|
|
78
|
+
|
|
79
|
+
### Ingestion Router
|
|
80
|
+
|
|
81
|
+
- `POST /{session_id}/events` - Ingest event batch
|
|
82
|
+
|
|
83
|
+
## Entry Points
|
|
84
|
+
|
|
85
|
+
This package registers itself as:
|
|
86
|
+
- `fustor.receivers:http` - Receiver registry
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# fustor-receiver-http
|
|
2
|
+
|
|
3
|
+
HTTP Receiver for Fustor Fusion - implements the transport layer for receiving events from Agents.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides an HTTP-based implementation of the `Receiver` transport abstraction. It creates FastAPI routers that handle session management and event ingestion.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fustor-receiver-http
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fustor_receiver_http import HTTPReceiver, SessionInfo
|
|
19
|
+
|
|
20
|
+
# Create receiver
|
|
21
|
+
receiver = HTTPReceiver(
|
|
22
|
+
receiver_id="main-receiver",
|
|
23
|
+
config={"session_timeout_seconds": 30}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Register API keys for pipes
|
|
27
|
+
receiver.register_api_key("fk_abc123", "pipe-1")
|
|
28
|
+
|
|
29
|
+
# Register callbacks
|
|
30
|
+
async def on_session_created(session_id, task_id, pipe_id, client_info):
|
|
31
|
+
# Handle session creation
|
|
32
|
+
return SessionInfo(
|
|
33
|
+
session_id=session_id,
|
|
34
|
+
task_id=task_id,
|
|
35
|
+
pipe_id=pipe_id,
|
|
36
|
+
role="leader",
|
|
37
|
+
created_at=time.time(),
|
|
38
|
+
last_heartbeat=time.time()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def on_event_received(session_id, events, source_type, is_end):
|
|
42
|
+
# Process events
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
receiver.register_callbacks(
|
|
46
|
+
on_session_created=on_session_created,
|
|
47
|
+
on_event_received=on_event_received,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Mount routers in FastAPI app
|
|
51
|
+
app.include_router(receiver.get_session_router(), prefix="/api/v1/pipe/session")
|
|
52
|
+
app.include_router(receiver.get_ingestion_router(), prefix="/api/v1/pipe/ingest")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## API Endpoints
|
|
56
|
+
|
|
57
|
+
### Session Router
|
|
58
|
+
|
|
59
|
+
- `POST /` - Create a new session
|
|
60
|
+
- `POST /{session_id}/heartbeat` - Send heartbeat
|
|
61
|
+
- `DELETE /{session_id}` - Terminate session
|
|
62
|
+
|
|
63
|
+
### Ingestion Router
|
|
64
|
+
|
|
65
|
+
- `POST /{session_id}/events` - Ingest event batch
|
|
66
|
+
|
|
67
|
+
## Entry Points
|
|
68
|
+
|
|
69
|
+
This package registers itself as:
|
|
70
|
+
- `fustor.receivers:http` - Receiver registry
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fustor-receiver-http"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Fustor HTTP Receiver - Transport layer for Fusion to receive events from Agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"fustor-core",
|
|
18
|
+
"fastapi>=0.109.0",
|
|
19
|
+
"pydantic>=2.0.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[[project.authors]]
|
|
23
|
+
name = "Huajin Wang"
|
|
24
|
+
email = "wanghuajin999@163.com"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/excelwang/fustor/tree/master/extensions/receiver-http"
|
|
28
|
+
"Bug Tracker" = "https://github.com/excelwang/fustor/issues"
|
|
29
|
+
|
|
30
|
+
# Entry point for receiver discovery
|
|
31
|
+
[project.entry-points."fustor.receivers"]
|
|
32
|
+
http = "fustor_receiver_http:HTTPReceiver"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools_scm]
|
|
35
|
+
root = "../.."
|
|
36
|
+
version_scheme = "post-release"
|
|
37
|
+
local_scheme = "dirty-tag"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fustor HTTP Receiver - Transport layer for Fusion to receive events from Agents.
|
|
3
|
+
|
|
4
|
+
This package implements the HTTP transport protocol for receiving events
|
|
5
|
+
on the Fusion side. It provides FastAPI routers that can be mounted into
|
|
6
|
+
the Fusion application.
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional, Callable, Awaitable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from fustor_core.transport import Receiver
|
|
17
|
+
from fustor_core.event import EventBase, EventType, MessageSource
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --- Pydantic Models for API ---
|
|
23
|
+
|
|
24
|
+
class CreateSessionRequest(BaseModel):
|
|
25
|
+
"""Request payload for creating a new session."""
|
|
26
|
+
task_id: str
|
|
27
|
+
client_info: Optional[Dict[str, Any]] = None
|
|
28
|
+
session_timeout_seconds: Optional[int] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CreateSessionResponse(BaseModel):
|
|
32
|
+
"""Response for session creation."""
|
|
33
|
+
session_id: str
|
|
34
|
+
role: str # 'leader' or 'follower'
|
|
35
|
+
session_timeout_seconds: int
|
|
36
|
+
message: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EventBatch(BaseModel):
|
|
40
|
+
"""Batch of events to ingest."""
|
|
41
|
+
events: List[EventBase]
|
|
42
|
+
source_type: str = "message" # 'message', 'snapshot', 'audit', 'scan_complete'
|
|
43
|
+
is_end: bool = False
|
|
44
|
+
metadata: Optional[Dict[str, Any]] = None # Extra info e.g., scan_path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HeartbeatResponse(BaseModel):
|
|
48
|
+
status: str
|
|
49
|
+
role: Optional[str] = None
|
|
50
|
+
message: Optional[str] = None
|
|
51
|
+
can_realtime: Optional[bool] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- Session Handler Protocol ---
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class SessionInfo:
|
|
58
|
+
"""Information about an active session."""
|
|
59
|
+
session_id: str
|
|
60
|
+
task_id: str
|
|
61
|
+
view_id: str
|
|
62
|
+
role: str # 'leader' or 'follower'
|
|
63
|
+
created_at: float
|
|
64
|
+
last_heartbeat: float
|
|
65
|
+
can_realtime: bool = False
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def pipe_id(self) -> str:
|
|
69
|
+
"""Deprecated alias for view_id."""
|
|
70
|
+
import warnings
|
|
71
|
+
warnings.warn("pipe_id is deprecated, use view_id instead", DeprecationWarning, stacklevel=2)
|
|
72
|
+
return self.view_id
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Type aliases for callbacks
|
|
76
|
+
SessionCreatedCallback = Callable[[str, str, str, Dict[str, Any], int], Awaitable[SessionInfo]]
|
|
77
|
+
EventReceivedCallback = Callable[[str, List[EventBase], str, bool], Awaitable[bool]]
|
|
78
|
+
HeartbeatCallback = Callable[[str, bool], Awaitable[Dict[str, Any]]]
|
|
79
|
+
SessionClosedCallback = Callable[[str], Awaitable[None]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HTTPReceiver(Receiver):
|
|
83
|
+
"""
|
|
84
|
+
HTTP-based Receiver implementation for Fustor Fusion.
|
|
85
|
+
|
|
86
|
+
This receiver creates FastAPI routers that handle:
|
|
87
|
+
- Session creation and management
|
|
88
|
+
- Event batch ingestion
|
|
89
|
+
- Heartbeat processing
|
|
90
|
+
|
|
91
|
+
The receiver delegates actual processing to registered callbacks.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
receiver_id: str,
|
|
97
|
+
bind_host: str = "0.0.0.0",
|
|
98
|
+
port: int = 8101,
|
|
99
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
100
|
+
config: Optional[Dict[str, Any]] = None
|
|
101
|
+
):
|
|
102
|
+
super().__init__(receiver_id, bind_host, port, credentials or {}, config)
|
|
103
|
+
|
|
104
|
+
# Callbacks for event processing
|
|
105
|
+
self._on_session_created: Optional[SessionCreatedCallback] = None
|
|
106
|
+
self._on_event_received: Optional[EventReceivedCallback] = None
|
|
107
|
+
self._on_heartbeat: Optional[HeartbeatCallback] = None
|
|
108
|
+
self._on_session_closed: Optional[SessionClosedCallback] = None
|
|
109
|
+
self._on_scan_complete: Optional[Callable[[str, str], Awaitable[None]]] = None # session_id, path
|
|
110
|
+
|
|
111
|
+
# API key to pipe mapping
|
|
112
|
+
self._api_key_to_pipe: Dict[str, str] = {}
|
|
113
|
+
self._api_key_cache: Dict[str, str] = {}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Session timeout configuration
|
|
117
|
+
self.session_timeout_seconds = config.get("session_timeout_seconds", 30) if config else 30
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Create routers
|
|
121
|
+
self._session_router = self._create_session_router()
|
|
122
|
+
self._ingestion_router = self._create_ingestion_router()
|
|
123
|
+
|
|
124
|
+
def register_callbacks(
|
|
125
|
+
self,
|
|
126
|
+
on_session_created: Optional[SessionCreatedCallback] = None,
|
|
127
|
+
on_event_received: Optional[EventReceivedCallback] = None,
|
|
128
|
+
on_heartbeat: Optional[HeartbeatCallback] = None,
|
|
129
|
+
on_session_closed: Optional[SessionClosedCallback] = None,
|
|
130
|
+
on_scan_complete: Optional[Callable[[str, str], Awaitable[None]]] = None,
|
|
131
|
+
):
|
|
132
|
+
"""Register callbacks for event processing."""
|
|
133
|
+
if on_session_created:
|
|
134
|
+
self._on_session_created = on_session_created
|
|
135
|
+
if on_event_received:
|
|
136
|
+
self._on_event_received = on_event_received
|
|
137
|
+
if on_heartbeat:
|
|
138
|
+
self._on_heartbeat = on_heartbeat
|
|
139
|
+
if on_session_closed:
|
|
140
|
+
self._on_session_closed = on_session_closed
|
|
141
|
+
if on_scan_complete:
|
|
142
|
+
self._on_scan_complete = on_scan_complete
|
|
143
|
+
|
|
144
|
+
def register_api_key(self, api_key: str, pipe_id: str):
|
|
145
|
+
"""Register an API key for a pipe."""
|
|
146
|
+
self._api_key_to_pipe[api_key] = pipe_id
|
|
147
|
+
self._api_key_cache.clear() # Invalidate cache
|
|
148
|
+
self.logger.debug(f"Registered API key for pipe {pipe_id}")
|
|
149
|
+
|
|
150
|
+
async def validate_credential(self, credential: Dict[str, Any]) -> Optional[str]:
|
|
151
|
+
"""
|
|
152
|
+
Validate incoming credential.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
credential: The credential to validate (expects {"api_key": "..."})
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Associated pipe_id if valid, None if invalid
|
|
159
|
+
"""
|
|
160
|
+
api_key = credential.get("api_key") or credential.get("key")
|
|
161
|
+
if not api_key:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# Check cache first
|
|
165
|
+
if api_key in self._api_key_cache:
|
|
166
|
+
return self._api_key_cache[api_key]
|
|
167
|
+
|
|
168
|
+
# Check mapping
|
|
169
|
+
if api_key in self._api_key_to_pipe:
|
|
170
|
+
pipe_id = self._api_key_to_pipe[api_key]
|
|
171
|
+
self._api_key_cache[api_key] = pipe_id
|
|
172
|
+
return pipe_id
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
async def start(self) -> None:
|
|
177
|
+
"""Start the receiver (routers are mounted externally)."""
|
|
178
|
+
self.logger.info(f"HTTP Receiver {self.id} ready on {self.get_address()}")
|
|
179
|
+
|
|
180
|
+
async def stop(self) -> None:
|
|
181
|
+
"""Stop the receiver gracefully."""
|
|
182
|
+
self.logger.info(f"HTTP Receiver {self.id} stopping")
|
|
183
|
+
|
|
184
|
+
def get_session_router(self) -> APIRouter:
|
|
185
|
+
"""Get the session management router."""
|
|
186
|
+
return self._session_router
|
|
187
|
+
|
|
188
|
+
def get_ingestion_router(self) -> APIRouter:
|
|
189
|
+
"""Get the event ingestion router."""
|
|
190
|
+
return self._ingestion_router
|
|
191
|
+
|
|
192
|
+
def _create_session_router(self) -> APIRouter:
|
|
193
|
+
"""Create the session management router."""
|
|
194
|
+
router = APIRouter(tags=["Session"])
|
|
195
|
+
receiver = self # Capture self for closures
|
|
196
|
+
|
|
197
|
+
@router.post("/", response_model=CreateSessionResponse)
|
|
198
|
+
async def create_session(
|
|
199
|
+
payload: CreateSessionRequest,
|
|
200
|
+
request: Request,
|
|
201
|
+
):
|
|
202
|
+
"""Create a new session for event ingestion."""
|
|
203
|
+
# Extract API key from header
|
|
204
|
+
api_key = request.headers.get("X-API-Key")
|
|
205
|
+
if not api_key:
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
208
|
+
detail="API key required"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
pipe_id = await receiver.validate_credential({"api_key": api_key})
|
|
212
|
+
if not pipe_id:
|
|
213
|
+
raise HTTPException(
|
|
214
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
215
|
+
detail="Invalid API key"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
session_id = str(uuid.uuid4())
|
|
219
|
+
|
|
220
|
+
# Use client-requested timeout if provided, otherwise fallback to receiver config
|
|
221
|
+
session_timeout_seconds = payload.session_timeout_seconds or receiver.session_timeout_seconds
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if receiver._on_session_created:
|
|
225
|
+
try:
|
|
226
|
+
session_info = await receiver._on_session_created(
|
|
227
|
+
session_id,
|
|
228
|
+
payload.task_id,
|
|
229
|
+
pipe_id,
|
|
230
|
+
payload.client_info or {},
|
|
231
|
+
session_timeout_seconds
|
|
232
|
+
)
|
|
233
|
+
return CreateSessionResponse(
|
|
234
|
+
session_id=session_info.session_id,
|
|
235
|
+
role=session_info.role,
|
|
236
|
+
session_timeout_seconds=session_timeout_seconds,
|
|
237
|
+
message="Session created successfully"
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
receiver.logger.error(f"Failed to create session: {e}")
|
|
241
|
+
raise HTTPException(
|
|
242
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
243
|
+
detail=str(e)
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
raise HTTPException(
|
|
247
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
248
|
+
detail="Session handler not configured"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@router.post("/{session_id}/heartbeat", response_model=HeartbeatResponse)
|
|
252
|
+
async def heartbeat(session_id: str, request: Request):
|
|
253
|
+
"""Send a heartbeat to maintain session."""
|
|
254
|
+
# Extract can_realtime from payload (if any)
|
|
255
|
+
try:
|
|
256
|
+
payload = await request.json()
|
|
257
|
+
can_realtime = payload.get("can_realtime", False)
|
|
258
|
+
except Exception:
|
|
259
|
+
can_realtime = False
|
|
260
|
+
|
|
261
|
+
logger.info(f"Received heartbeat for session {session_id}, can_realtime={can_realtime}")
|
|
262
|
+
|
|
263
|
+
if receiver._on_heartbeat:
|
|
264
|
+
try:
|
|
265
|
+
result = await receiver._on_heartbeat(session_id, can_realtime)
|
|
266
|
+
if result and result.get("status") == "error":
|
|
267
|
+
raise HTTPException(
|
|
268
|
+
status_code=419,
|
|
269
|
+
detail=result.get("message", "Session obsoleted")
|
|
270
|
+
)
|
|
271
|
+
return HeartbeatResponse(
|
|
272
|
+
status=result.get("status", "ok"),
|
|
273
|
+
role=result.get("role"),
|
|
274
|
+
message=result.get("message")
|
|
275
|
+
)
|
|
276
|
+
except HTTPException:
|
|
277
|
+
raise
|
|
278
|
+
except Exception as e:
|
|
279
|
+
receiver.logger.warning(f"Heartbeat failed for {session_id}: {e}")
|
|
280
|
+
return HeartbeatResponse(status="error", message=str(e))
|
|
281
|
+
|
|
282
|
+
return HeartbeatResponse(status="ok")
|
|
283
|
+
|
|
284
|
+
@router.delete("/{session_id}")
|
|
285
|
+
async def terminate_session(session_id: str, request: Request):
|
|
286
|
+
"""Terminate a session."""
|
|
287
|
+
if receiver._on_session_closed:
|
|
288
|
+
await receiver._on_session_closed(session_id)
|
|
289
|
+
return {"status": "terminated", "session_id": session_id}
|
|
290
|
+
|
|
291
|
+
return router
|
|
292
|
+
|
|
293
|
+
def _create_ingestion_router(self) -> APIRouter:
|
|
294
|
+
"""Create the event ingestion router."""
|
|
295
|
+
router = APIRouter(tags=["Ingestion"])
|
|
296
|
+
receiver = self
|
|
297
|
+
|
|
298
|
+
@router.post("/{session_id}/events")
|
|
299
|
+
async def ingest_events(
|
|
300
|
+
session_id: str,
|
|
301
|
+
batch: EventBatch,
|
|
302
|
+
request: Request,
|
|
303
|
+
):
|
|
304
|
+
"""Ingest a batch of events."""
|
|
305
|
+
|
|
306
|
+
# Handle scan_complete notification
|
|
307
|
+
if batch.source_type == "scan_complete" and batch.metadata:
|
|
308
|
+
scan_path = batch.metadata.get("scan_path")
|
|
309
|
+
if scan_path and receiver._on_scan_complete:
|
|
310
|
+
try:
|
|
311
|
+
await receiver._on_scan_complete(session_id, scan_path)
|
|
312
|
+
return {"status": "ok", "phase": "scan_complete"}
|
|
313
|
+
except Exception as e:
|
|
314
|
+
receiver.logger.error(f"Scan complete handling failed: {e}")
|
|
315
|
+
return {"status": "ok", "phase": "scan_complete"}
|
|
316
|
+
|
|
317
|
+
if receiver._on_event_received:
|
|
318
|
+
try:
|
|
319
|
+
success = await receiver._on_event_received(
|
|
320
|
+
session_id, batch.events, batch.source_type, batch.is_end
|
|
321
|
+
)
|
|
322
|
+
if success:
|
|
323
|
+
return {"status": "ok", "count": len(batch.events)}
|
|
324
|
+
else:
|
|
325
|
+
raise HTTPException(
|
|
326
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
327
|
+
detail="Failed to process events"
|
|
328
|
+
)
|
|
329
|
+
except HTTPException:
|
|
330
|
+
raise
|
|
331
|
+
except Exception as e:
|
|
332
|
+
receiver.logger.error(f"Event ingestion failed: {e}")
|
|
333
|
+
raise HTTPException(
|
|
334
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
335
|
+
detail=str(e)
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
raise HTTPException(
|
|
339
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
340
|
+
detail="Event handler not configured"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return router
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# Factory function for creating receiver with standard configuration
|
|
347
|
+
def create_http_receiver(
|
|
348
|
+
receiver_id: str = "default",
|
|
349
|
+
config: Optional[Dict[str, Any]] = None
|
|
350
|
+
) -> HTTPReceiver:
|
|
351
|
+
"""
|
|
352
|
+
Create an HTTP receiver with standard configuration.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
receiver_id: Unique identifier for this receiver
|
|
356
|
+
config: Optional configuration dict
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Configured HTTPReceiver instance
|
|
360
|
+
"""
|
|
361
|
+
return HTTPReceiver(
|
|
362
|
+
receiver_id=receiver_id,
|
|
363
|
+
config=config or {}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
__all__ = [
|
|
368
|
+
"HTTPReceiver",
|
|
369
|
+
"SessionInfo",
|
|
370
|
+
"CreateSessionRequest",
|
|
371
|
+
"CreateSessionResponse",
|
|
372
|
+
"EventBatch",
|
|
373
|
+
"HeartbeatResponse",
|
|
374
|
+
"create_http_receiver",
|
|
375
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-receiver-http
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: Fustor HTTP Receiver - Transport layer for Fusion to receive events from Agents
|
|
5
|
+
Author-email: Huajin Wang <wanghuajin999@163.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/excelwang/fustor/tree/master/extensions/receiver-http
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/excelwang/fustor/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: fustor-core
|
|
14
|
+
Requires-Dist: fastapi>=0.109.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
|
|
17
|
+
# fustor-receiver-http
|
|
18
|
+
|
|
19
|
+
HTTP Receiver for Fustor Fusion - implements the transport layer for receiving events from Agents.
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
This package provides an HTTP-based implementation of the `Receiver` transport abstraction. It creates FastAPI routers that handle session management and event ingestion.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install fustor-receiver-http
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from fustor_receiver_http import HTTPReceiver, SessionInfo
|
|
35
|
+
|
|
36
|
+
# Create receiver
|
|
37
|
+
receiver = HTTPReceiver(
|
|
38
|
+
receiver_id="main-receiver",
|
|
39
|
+
config={"session_timeout_seconds": 30}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Register API keys for pipes
|
|
43
|
+
receiver.register_api_key("fk_abc123", "pipe-1")
|
|
44
|
+
|
|
45
|
+
# Register callbacks
|
|
46
|
+
async def on_session_created(session_id, task_id, pipe_id, client_info):
|
|
47
|
+
# Handle session creation
|
|
48
|
+
return SessionInfo(
|
|
49
|
+
session_id=session_id,
|
|
50
|
+
task_id=task_id,
|
|
51
|
+
pipe_id=pipe_id,
|
|
52
|
+
role="leader",
|
|
53
|
+
created_at=time.time(),
|
|
54
|
+
last_heartbeat=time.time()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def on_event_received(session_id, events, source_type, is_end):
|
|
58
|
+
# Process events
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
receiver.register_callbacks(
|
|
62
|
+
on_session_created=on_session_created,
|
|
63
|
+
on_event_received=on_event_received,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Mount routers in FastAPI app
|
|
67
|
+
app.include_router(receiver.get_session_router(), prefix="/api/v1/pipe/session")
|
|
68
|
+
app.include_router(receiver.get_ingestion_router(), prefix="/api/v1/pipe/ingest")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API Endpoints
|
|
72
|
+
|
|
73
|
+
### Session Router
|
|
74
|
+
|
|
75
|
+
- `POST /` - Create a new session
|
|
76
|
+
- `POST /{session_id}/heartbeat` - Send heartbeat
|
|
77
|
+
- `DELETE /{session_id}` - Terminate session
|
|
78
|
+
|
|
79
|
+
### Ingestion Router
|
|
80
|
+
|
|
81
|
+
- `POST /{session_id}/events` - Ingest event batch
|
|
82
|
+
|
|
83
|
+
## Entry Points
|
|
84
|
+
|
|
85
|
+
This package registers itself as:
|
|
86
|
+
- `fustor.receivers:http` - Receiver registry
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/fustor_receiver_http/__init__.py
|
|
4
|
+
src/fustor_receiver_http.egg-info/PKG-INFO
|
|
5
|
+
src/fustor_receiver_http.egg-info/SOURCES.txt
|
|
6
|
+
src/fustor_receiver_http.egg-info/dependency_links.txt
|
|
7
|
+
src/fustor_receiver_http.egg-info/entry_points.txt
|
|
8
|
+
src/fustor_receiver_http.egg-info/requires.txt
|
|
9
|
+
src/fustor_receiver_http.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fustor_receiver_http
|