antimatter-gateway 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.
- antimatter_gateway-0.1.0/PKG-INFO +8 -0
- antimatter_gateway-0.1.0/pyproject.toml +19 -0
- antimatter_gateway-0.1.0/setup.cfg +4 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway/qr.py +71 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway/router.py +76 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway/server.py +224 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway/tunnel.py +52 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/PKG-INFO +8 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/SOURCES.txt +11 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/dependency_links.txt +1 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/entry_points.txt +3 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/requires.txt +5 -0
- antimatter_gateway-0.1.0/src/antimatter_gateway.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "antimatter-gateway"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
dependencies = [
|
|
5
|
+
"websockets>=12.0",
|
|
6
|
+
"qrcode>=7.4.2",
|
|
7
|
+
"antimatter-shared-config",
|
|
8
|
+
"antimatter-shared-crypto",
|
|
9
|
+
"antimatter-shared-protocol"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.uv.sources]
|
|
13
|
+
antimatter-shared-config = { path = "../shared-config" }
|
|
14
|
+
antimatter-shared-crypto = { path = "../shared-crypto" }
|
|
15
|
+
antimatter-shared-protocol = { path = "../shared-protocol" }
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
antimatter = "antimatter_gateway.server:main"
|
|
19
|
+
antimatter-pair = "antimatter_gateway.qr:main"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
import qrcode
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def generate_qr_payload(
|
|
8
|
+
cloudflare_url: str | None,
|
|
9
|
+
pairing_token: str,
|
|
10
|
+
gateway_x25519_pub: str,
|
|
11
|
+
client_id: str | None = None
|
|
12
|
+
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Generates the canonical Deep Link payload for Android pairing.
|
|
15
|
+
Includes the X25519 public key for the E2EE ECDH handshake.
|
|
16
|
+
"""
|
|
17
|
+
# For now, default to local dev if cloudflare is off
|
|
18
|
+
base_url = "https://antimatter.saifmukhtar.dev/connect"
|
|
19
|
+
|
|
20
|
+
params = {
|
|
21
|
+
"url": cloudflare_url or "ws://127.0.0.1:8765",
|
|
22
|
+
"token": pairing_token,
|
|
23
|
+
"x25519_pub": gateway_x25519_pub
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if client_id:
|
|
27
|
+
params["cid"] = client_id
|
|
28
|
+
|
|
29
|
+
query = urllib.parse.urlencode(params)
|
|
30
|
+
return f"{base_url}?{query}"
|
|
31
|
+
|
|
32
|
+
def print_qr_to_terminal(payload: str) -> None:
|
|
33
|
+
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4)
|
|
34
|
+
qr.add_data(payload)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
qr.print_ascii(invert=True)
|
|
38
|
+
except Exception:
|
|
39
|
+
logger.warning("Failed to print ASCII QR. Terminal might not support it.")
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
from antimatter_shared_config.config import load_config
|
|
43
|
+
config = load_config()
|
|
44
|
+
if not config.gateway_priv_x25519 or not config.pairing_token:
|
|
45
|
+
print("Error: Gateway not initialized. Please run 'antimatter-gateway' first.")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# We construct the public key from the private key if needed, or simply assume it's in config
|
|
49
|
+
# Actually, E2EESession handles derivation.
|
|
50
|
+
from antimatter_crypto.e2ee import E2EESession
|
|
51
|
+
e2ee = E2EESession(role="gateway", private_key_b64=config.gateway_priv_x25519)
|
|
52
|
+
|
|
53
|
+
payload = generate_qr_payload(
|
|
54
|
+
cloudflare_url=config.cloudflare_url,
|
|
55
|
+
pairing_token=config.pairing_token,
|
|
56
|
+
gateway_x25519_pub=e2ee.public_key_b64,
|
|
57
|
+
client_id=config.cloudflare_client_id
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print("\n" + "="*50)
|
|
61
|
+
print("ANTIMATTER E2EE GATEWAY SECURE PAIRING")
|
|
62
|
+
print("="*50)
|
|
63
|
+
if config.cloudflare_url:
|
|
64
|
+
print(f"\nTunnel: {config.cloudflare_url}")
|
|
65
|
+
|
|
66
|
+
print("\nScan this QR Code with the Antimatter App:\n")
|
|
67
|
+
print_qr_to_terminal(payload)
|
|
68
|
+
print("="*50)
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from antimatter_crypto.e2ee import E2EESession
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class MessageRouter:
|
|
9
|
+
def __init__(self, gateway=None):
|
|
10
|
+
self.gateway = gateway
|
|
11
|
+
self.clients = set()
|
|
12
|
+
self.adapters = {} # id -> {"name": str, "ws": websocket}
|
|
13
|
+
|
|
14
|
+
def add_client(self, websocket):
|
|
15
|
+
self.clients.add(websocket)
|
|
16
|
+
# When a client connects, instantly send them the list of agents
|
|
17
|
+
asyncio.create_task(self.broadcast_system_state())
|
|
18
|
+
|
|
19
|
+
def remove_client(self, websocket):
|
|
20
|
+
self.clients.discard(websocket)
|
|
21
|
+
|
|
22
|
+
async def register_adapter(self, agent_id: str, agent_name: str, websocket):
|
|
23
|
+
self.adapters[agent_id] = {"name": agent_name, "ws": websocket}
|
|
24
|
+
await self.broadcast_system_state()
|
|
25
|
+
|
|
26
|
+
async def unregister_adapter(self, agent_id: str):
|
|
27
|
+
if agent_id in self.adapters:
|
|
28
|
+
del self.adapters[agent_id]
|
|
29
|
+
await self.broadcast_system_state()
|
|
30
|
+
|
|
31
|
+
async def broadcast_system_state(self):
|
|
32
|
+
# Implementation to send AVAILABLE_AGENTS to clients
|
|
33
|
+
if not self.clients or not self.gateway:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
agents = [
|
|
37
|
+
{"id": aid, "name": info["name"], "status": "online"}
|
|
38
|
+
for aid, info in self.adapters.items()
|
|
39
|
+
]
|
|
40
|
+
payload = {"type": "AVAILABLE_AGENTS", "agents": agents}
|
|
41
|
+
|
|
42
|
+
await self.broadcast_to_clients(payload, self.gateway.e2ee)
|
|
43
|
+
|
|
44
|
+
async def route_to_adapter(self, parsed_cmd: dict, e2ee: E2EESession, websocket):
|
|
45
|
+
"""
|
|
46
|
+
Receives decrypted command from client, routes to local adapter.
|
|
47
|
+
"""
|
|
48
|
+
agent_id = parsed_cmd.get("agentId")
|
|
49
|
+
if not agent_id:
|
|
50
|
+
logger.warning(f"No agentId specified in command: {parsed_cmd.get('type')}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
adapter = self.adapters.get(agent_id)
|
|
54
|
+
if not adapter:
|
|
55
|
+
logger.warning(f"Target agent {agent_id} is offline. Dropping command.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
await adapter["ws"].send(json.dumps(parsed_cmd))
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Failed to route to adapter {agent_id}: {e}")
|
|
62
|
+
|
|
63
|
+
async def broadcast_to_clients(self, payload: dict, e2ee: E2EESession):
|
|
64
|
+
"""
|
|
65
|
+
Takes raw JSON payload from an adapter, encrypts it with the
|
|
66
|
+
server-to-client key, and sends it to all Android apps.
|
|
67
|
+
"""
|
|
68
|
+
plaintext = json.dumps(payload)
|
|
69
|
+
envelope = e2ee.encrypt(plaintext, direction="output")
|
|
70
|
+
payload_str = json.dumps(envelope)
|
|
71
|
+
|
|
72
|
+
for client in list(self.clients):
|
|
73
|
+
try:
|
|
74
|
+
await client.send(payload_str)
|
|
75
|
+
except Exception:
|
|
76
|
+
self.clients.discard(client)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import websockets
|
|
6
|
+
from websockets.exceptions import ConnectionClosed
|
|
7
|
+
|
|
8
|
+
from antimatter_shared_config.config import load_config, save_config, AntimatterConfig
|
|
9
|
+
from antimatter_crypto.auth import Ed25519Auth
|
|
10
|
+
from antimatter_crypto.e2ee import E2EESession
|
|
11
|
+
from .qr import generate_qr_payload, print_qr_to_terminal
|
|
12
|
+
from .tunnel import CloudflaredManager
|
|
13
|
+
from .router import MessageRouter
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Rate Limiting
|
|
18
|
+
_ip_failure_counts: dict[str, dict] = {}
|
|
19
|
+
RATE_LIMIT_MAX_FAILURES = 5
|
|
20
|
+
RATE_LIMIT_WINDOW = 60
|
|
21
|
+
|
|
22
|
+
def _record_failure(ip: str):
|
|
23
|
+
now = time.monotonic()
|
|
24
|
+
data = _ip_failure_counts.get(ip, {"count": 0, "reset_at": 0.0})
|
|
25
|
+
if now >= data["reset_at"]:
|
|
26
|
+
data = {"count": 0, "reset_at": now + RATE_LIMIT_WINDOW}
|
|
27
|
+
data["count"] += 1
|
|
28
|
+
_ip_failure_counts[ip] = data
|
|
29
|
+
|
|
30
|
+
class GatewayServer:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.config = load_config()
|
|
33
|
+
self.auth = Ed25519Auth(self.config.private_key_pem)
|
|
34
|
+
self.router = MessageRouter(gateway=self)
|
|
35
|
+
|
|
36
|
+
# Initialize E2EE Gateway Session
|
|
37
|
+
self.e2ee = E2EESession(role="gateway", private_key_b64=self.config.gateway_priv_x25519)
|
|
38
|
+
|
|
39
|
+
# Persist keys if newly generated
|
|
40
|
+
needs_save = False
|
|
41
|
+
if not self.config.private_key_pem:
|
|
42
|
+
self.config.private_key_pem = self.auth.private_key_base64
|
|
43
|
+
self.config.pairing_token = self.auth.pairing_token
|
|
44
|
+
needs_save = True
|
|
45
|
+
|
|
46
|
+
if not self.config.gateway_priv_x25519:
|
|
47
|
+
self.config.gateway_priv_x25519 = self.e2ee.private_key_b64
|
|
48
|
+
needs_save = True
|
|
49
|
+
|
|
50
|
+
if needs_save:
|
|
51
|
+
save_config(self.config)
|
|
52
|
+
|
|
53
|
+
async def handler(self, websocket):
|
|
54
|
+
ip = getattr(websocket, "remote_address", ("unknown",))[0]
|
|
55
|
+
now = time.monotonic()
|
|
56
|
+
|
|
57
|
+
# 1. Rate Limiting Check
|
|
58
|
+
rate_data = _ip_failure_counts.get(ip)
|
|
59
|
+
if rate_data and rate_data["count"] >= RATE_LIMIT_MAX_FAILURES and now < rate_data["reset_at"]:
|
|
60
|
+
logger.warning(f"Rate limited connection rejected: {ip}")
|
|
61
|
+
await websocket.close(1008, "Rate Limited")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# 2. Origin Validation
|
|
65
|
+
# In a real setup, we check req.headers.origin against cloudflareaccess.com
|
|
66
|
+
# For simplicity in python websockets, we handle it natively or trust localhost.
|
|
67
|
+
|
|
68
|
+
authenticated = False
|
|
69
|
+
e2ee_established = False
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
async for message in websocket:
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(message)
|
|
75
|
+
except Exception:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
msg_type = data.get("type")
|
|
79
|
+
|
|
80
|
+
msg_type = data.get("type")
|
|
81
|
+
|
|
82
|
+
# Adapter Registration (Local IPC)
|
|
83
|
+
if msg_type == "REGISTER_ADAPTER":
|
|
84
|
+
agent_id = data.get("id")
|
|
85
|
+
agent_name = data.get("name")
|
|
86
|
+
if agent_id and agent_name:
|
|
87
|
+
logger.info(f"Adapter registered: {agent_name} ({agent_id})")
|
|
88
|
+
await self.router.register_adapter(agent_id, agent_name, websocket)
|
|
89
|
+
# We break out of the auth loop into a dedicated adapter loop
|
|
90
|
+
await self._adapter_loop(websocket, agent_id)
|
|
91
|
+
return
|
|
92
|
+
else:
|
|
93
|
+
await websocket.close(1008, "Missing adapter id/name")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# State 1: Legacy Ed25519 Auth Challenge (Client)
|
|
97
|
+
if msg_type == "AUTH_CHALLENGE":
|
|
98
|
+
challenge = data.get("challenge")
|
|
99
|
+
if not challenge:
|
|
100
|
+
await websocket.close(1008, "Missing challenge")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
sig = self.auth.sign_challenge(challenge)
|
|
104
|
+
await websocket.send(json.dumps({"type": "AUTH_RESPONSE", "signature": sig}))
|
|
105
|
+
authenticated = True
|
|
106
|
+
logger.info(f"Client {ip} authenticated successfully.")
|
|
107
|
+
|
|
108
|
+
# Add client to router once authenticated
|
|
109
|
+
self.router.add_client(websocket)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if not authenticated:
|
|
113
|
+
_record_failure(ip)
|
|
114
|
+
await websocket.close(1008, "Unauthorized")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# State 2: E2EE Handshake
|
|
118
|
+
if msg_type == "HELLO":
|
|
119
|
+
client_pubkey = data.get("pubkey")
|
|
120
|
+
if not client_pubkey:
|
|
121
|
+
await websocket.close(1008, "Missing X25519 pubkey")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
self.e2ee.derive_session_keys(client_pubkey)
|
|
125
|
+
e2ee_established = True
|
|
126
|
+
logger.info("E2EE Session established (ECDH + HKDF).")
|
|
127
|
+
|
|
128
|
+
# Notify adapters
|
|
129
|
+
await self.router.broadcast_system_state()
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if not e2ee_established:
|
|
133
|
+
await websocket.send(json.dumps({"type": "ERROR", "message": "E2EE Handshake required."}))
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# State 3: E2EE Encrypted Payload Routing
|
|
137
|
+
if "iv" in data and "ct" in data and "aad" in data:
|
|
138
|
+
try:
|
|
139
|
+
# Decrypt client->server command
|
|
140
|
+
plaintext = self.e2ee.decrypt(data, expected_direction="cmd:")
|
|
141
|
+
parsed_cmd = json.loads(plaintext)
|
|
142
|
+
|
|
143
|
+
# Route to local adapter via IPC
|
|
144
|
+
await self.router.route_to_adapter(parsed_cmd, self.e2ee, websocket)
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
logger.error(f"E2EE Decryption/AAD failed: {e}")
|
|
147
|
+
await websocket.close(1008, "Encryption Error")
|
|
148
|
+
return
|
|
149
|
+
else:
|
|
150
|
+
logger.warning("Unencrypted message received post-handshake. Dropping.")
|
|
151
|
+
|
|
152
|
+
except ConnectionClosed:
|
|
153
|
+
logger.info(f"Connection closed: {ip}")
|
|
154
|
+
finally:
|
|
155
|
+
self.router.remove_client(websocket)
|
|
156
|
+
|
|
157
|
+
async def _adapter_loop(self, websocket, agent_id: str):
|
|
158
|
+
"""
|
|
159
|
+
Dedicated loop for listening to incoming messages from a local adapter.
|
|
160
|
+
When an adapter sends a message, it needs to be broadcast or routed to clients.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
async for message in websocket:
|
|
164
|
+
try:
|
|
165
|
+
data = json.loads(message)
|
|
166
|
+
except Exception:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Currently we only have one e2ee session and one client.
|
|
170
|
+
# If we have multiple clients, we need to map adapter messages to clients.
|
|
171
|
+
# For now, we broadcast to all connected, authenticated clients.
|
|
172
|
+
await self.router.broadcast_to_clients(data, self.e2ee)
|
|
173
|
+
|
|
174
|
+
except ConnectionClosed:
|
|
175
|
+
logger.info(f"Adapter connection closed: {agent_id}")
|
|
176
|
+
finally:
|
|
177
|
+
await self.router.unregister_adapter(agent_id)
|
|
178
|
+
|
|
179
|
+
async def start(self):
|
|
180
|
+
logger.info("Starting Gateway WebSocket server on ws://127.0.0.1:8765")
|
|
181
|
+
|
|
182
|
+
# Start Tunnel if configured
|
|
183
|
+
tunnel_url = self.config.cloudflare_url
|
|
184
|
+
self.cf_manager = None
|
|
185
|
+
|
|
186
|
+
if not tunnel_url:
|
|
187
|
+
logger.info("Starting Cloudflare Quick Tunnel...")
|
|
188
|
+
self.cf_manager = CloudflaredManager(8765)
|
|
189
|
+
if await self.cf_manager.start():
|
|
190
|
+
try:
|
|
191
|
+
await asyncio.wait_for(self.cf_manager.ready_event.wait(), timeout=15.0)
|
|
192
|
+
tunnel_url = self.cf_manager.url
|
|
193
|
+
except asyncio.TimeoutError:
|
|
194
|
+
logger.warning("Quick tunnel timeout.")
|
|
195
|
+
|
|
196
|
+
payload = generate_qr_payload(
|
|
197
|
+
cloudflare_url=tunnel_url,
|
|
198
|
+
pairing_token=self.auth.pairing_token,
|
|
199
|
+
gateway_x25519_pub=self.e2ee.public_key_b64,
|
|
200
|
+
client_id=self.config.cloudflare_client_id
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
print("\n" + "="*50)
|
|
204
|
+
print("ANTIMATTER E2EE GATEWAY SECURE PAIRING")
|
|
205
|
+
print("="*50)
|
|
206
|
+
if tunnel_url:
|
|
207
|
+
print(f"\nTunnel: {tunnel_url}")
|
|
208
|
+
|
|
209
|
+
print("\nScan this QR Code with the Antimatter App:\n")
|
|
210
|
+
print_qr_to_terminal(payload)
|
|
211
|
+
print("="*50)
|
|
212
|
+
|
|
213
|
+
async with websockets.serve(self.handler, "127.0.0.1", 8765):
|
|
214
|
+
await asyncio.Future()
|
|
215
|
+
|
|
216
|
+
async def main_async():
|
|
217
|
+
server = GatewayServer()
|
|
218
|
+
await server.start()
|
|
219
|
+
|
|
220
|
+
def main():
|
|
221
|
+
asyncio.run(main_async())
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class CloudflaredManager:
|
|
8
|
+
def __init__(self, port: int):
|
|
9
|
+
self.port = port
|
|
10
|
+
self.process = None
|
|
11
|
+
self.url = None
|
|
12
|
+
self.ready_event = asyncio.Event()
|
|
13
|
+
|
|
14
|
+
async def start(self):
|
|
15
|
+
try:
|
|
16
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
17
|
+
"cloudflared", "tunnel", "--url", f"http://127.0.0.1:{self.port}",
|
|
18
|
+
stdout=asyncio.subprocess.PIPE,
|
|
19
|
+
stderr=asyncio.subprocess.PIPE
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Cloudflared prints to stderr
|
|
23
|
+
asyncio.create_task(self._read_stderr())
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
logger.error("cloudflared CLI not found. Please install it to use Quick Tunnels.")
|
|
26
|
+
return False
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
async def _read_stderr(self):
|
|
30
|
+
url_pattern = re.compile(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com")
|
|
31
|
+
while True:
|
|
32
|
+
line = await self.process.stderr.readline()
|
|
33
|
+
if not line:
|
|
34
|
+
break
|
|
35
|
+
line_str = line.decode('utf-8').strip()
|
|
36
|
+
|
|
37
|
+
logger.info(f"[Cloudflared] {line_str}")
|
|
38
|
+
|
|
39
|
+
if not self.url:
|
|
40
|
+
match = url_pattern.search(line_str)
|
|
41
|
+
if match:
|
|
42
|
+
# Convert https to wss
|
|
43
|
+
self.url = match.group(0).replace("https://", "wss://")
|
|
44
|
+
self.ready_event.set()
|
|
45
|
+
|
|
46
|
+
async def stop(self):
|
|
47
|
+
if self.process:
|
|
48
|
+
self.process.terminate()
|
|
49
|
+
try:
|
|
50
|
+
await asyncio.wait_for(self.process.wait(), timeout=5.0)
|
|
51
|
+
except asyncio.TimeoutError:
|
|
52
|
+
self.process.kill()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/antimatter_gateway/qr.py
|
|
3
|
+
src/antimatter_gateway/router.py
|
|
4
|
+
src/antimatter_gateway/server.py
|
|
5
|
+
src/antimatter_gateway/tunnel.py
|
|
6
|
+
src/antimatter_gateway.egg-info/PKG-INFO
|
|
7
|
+
src/antimatter_gateway.egg-info/SOURCES.txt
|
|
8
|
+
src/antimatter_gateway.egg-info/dependency_links.txt
|
|
9
|
+
src/antimatter_gateway.egg-info/entry_points.txt
|
|
10
|
+
src/antimatter_gateway.egg-info/requires.txt
|
|
11
|
+
src/antimatter_gateway.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
antimatter_gateway
|