distributed-state-network 0.2.1__tar.gz → 0.4.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.
Files changed (35) hide show
  1. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/PKG-INFO +2 -1
  2. distributed_state_network-0.4.0/documentation/ds-node-server.md +192 -0
  3. distributed_state_network-0.4.0/documentation/protocol.md +114 -0
  4. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/pyproject.toml +5 -4
  5. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/dsnode.py +62 -72
  6. distributed_state_network-0.4.0/src/distributed_state_network/handler.py +156 -0
  7. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/test.py +49 -43
  8. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/tests/connections.py +1 -1
  9. distributed_state_network-0.2.1/documentation/ds-node-server.md +0 -58
  10. distributed_state_network-0.2.1/documentation/protocol.md +0 -65
  11. distributed_state_network-0.2.1/src/distributed_state_network/handler.py +0 -144
  12. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/.github/workflows/publish.yml +0 -0
  13. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/.gitignore +0 -0
  14. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/LICENSE +0 -0
  15. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/README.md +0 -0
  16. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/coverage.sh +0 -0
  17. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/documentation/ds-node-config.md +0 -0
  18. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/documentation/ds-node.md +0 -0
  19. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/documentation/usage.md +0 -0
  20. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/__init__.py +0 -0
  21. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/create_key.py +0 -0
  22. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/config.py +0 -0
  23. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/endpoint.py +0 -0
  24. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/hello_packet.py +0 -0
  25. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/msg_types.py +0 -0
  26. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/peers_packet.py +0 -0
  27. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/signed_packet.py +0 -0
  28. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/state_packet.py +0 -0
  29. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/__init__.py +0 -0
  30. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/aes.py +0 -0
  31. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/byte_helper.py +0 -0
  32. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/ecdsa.py +0 -0
  33. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/https.py +0 -0
  34. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/src/distributed_state_network/util/key_manager.py +0 -0
  35. {distributed_state_network-0.2.1 → distributed_state_network-0.4.0}/technical.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed-state-network
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: A tool to distribute the state of a network device to other devices on the network
5
5
  Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
6
6
  Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
@@ -18,6 +18,7 @@ Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: cryptography
20
20
  Requires-Dist: ecdsa
21
+ Requires-Dist: flask
21
22
  Requires-Dist: ipaddress
22
23
  Requires-Dist: logging
23
24
  Requires-Dist: requests
@@ -0,0 +1,192 @@
1
+ ## DSNodeServer
2
+
3
+ UDP server wrapper for DSNode that handles incoming network requests.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNodeServer:
12
+ config: DSNodeConfig
13
+ node: DSNode
14
+ socket: socket.socket
15
+ running: bool
16
+ thread: threading.Thread
17
+ ```
18
+
19
+ ### Constructor
20
+
21
+ **Parameters:**
22
+ - `config` (`DSNodeConfig`): Node configuration
23
+ - `sock` (`Optional[socket.socket]`): Custom UDP socket to use (optional, creates new socket if not provided)
24
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
25
+ - `update_callback` (`Optional[Callable]`): Callback for state update events
26
+
27
+ **Example:**
28
+ ```python
29
+ # Create with default socket
30
+ server = DSNodeServer(config)
31
+
32
+ # Or with custom socket
33
+ custom_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
34
+ custom_socket.bind(("0.0.0.0", 8000))
35
+ server = DSNodeServer(config, sock=custom_socket)
36
+ ```
37
+
38
+ ### Static Methods
39
+
40
+ ### `start(config: DSNodeConfig, sock: Optional[socket.socket] = None, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> DSNodeServer`
41
+ Creates and starts a new DSNodeServer instance with UDP socket.
42
+
43
+ ```python
44
+ server = DSNodeServer.start(config)
45
+ ```
46
+
47
+ **Parameters:**
48
+ - `config` (`DSNodeConfig`): Node configuration
49
+ - `sock` (`Optional[socket.socket]`): Custom UDP socket to use (optional)
50
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events (no parameters)
51
+ - `update_callback` (`Optional[Callable]`): Callback for state update events (no parameters)
52
+
53
+ **Returns:**
54
+ - `DSNodeServer`: Running server instance
55
+
56
+ **Example with bootstrap:**
57
+ ```python
58
+ # Bootstrap node (first node in network)
59
+ bootstrap_config = DSNodeConfig(
60
+ node_id="bootstrap",
61
+ port=8000,
62
+ aes_key_file="network.key",
63
+ bootstrap_nodes=[]
64
+ )
65
+ bootstrap = DSNodeServer.start(bootstrap_config)
66
+
67
+ # Connector node (joins existing network)
68
+ connector_config = DSNodeConfig(
69
+ node_id="connector",
70
+ port=8001,
71
+ aes_key_file="network.key",
72
+ bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
73
+ )
74
+ connector = DSNodeServer.start(connector_config)
75
+ ```
76
+
77
+ ### `generate_key(out_file_path: str) -> None`
78
+ Generates a new AES key file for network encryption. All nodes in the same network must share the same AES key.
79
+
80
+ **Parameters:**
81
+ - `out_file_path` (`str`): Path where the key file will be saved
82
+
83
+ **Example:**
84
+ ```python
85
+ DSNodeServer.generate_key("/path/to/network.key")
86
+ ```
87
+
88
+ ### Instance Methods
89
+
90
+ ### `stop() -> None`
91
+ Gracefully shuts down the server and cleans up resources.
92
+
93
+ **Example:**
94
+ ```python
95
+ server.stop()
96
+ ```
97
+
98
+ ### `serve_forever() -> None`
99
+ Main UDP server loop. Continuously listens for incoming packets and routes them to appropriate handlers. This method is typically called automatically by `start()` in a daemon thread.
100
+
101
+ **Note:** This method blocks until the server is stopped.
102
+
103
+ ## Network Protocol
104
+
105
+ The server uses UDP sockets with the following characteristics:
106
+
107
+ - **Port**: Configurable via DSNodeConfig
108
+ - **Encryption**: All packets encrypted with AES
109
+ - **Authentication**: ECDSA signatures for message verification
110
+ - **Max Packet Size**: 65507 bytes (UDP limit)
111
+ - **Timeout**: 2 seconds with automatic retry (up to 3 attempts)
112
+
113
+ ## Message Types
114
+
115
+ The server handles four types of messages:
116
+
117
+ 1. **HELLO (1)**: Node introduction and public key exchange
118
+ 2. **PEERS (2)**: Request/response for peer list
119
+ 3. **UPDATE (3)**: State synchronization
120
+ 4. **PING (4)**: Connection health check
121
+
122
+ ## Automatic IP Detection
123
+
124
+ When a client connects to a bootstrap server:
125
+
126
+ 1. Server extracts client's IP from UDP packet source address
127
+ 2. Server includes detected IP in HELLO response via `detected_address` field
128
+ 3. Client updates its own address book with the detected IP
129
+ 4. Correct IP propagates to other nodes through peer discovery
130
+
131
+ This eliminates the need for manual IP configuration and works correctly behind NAT.
132
+
133
+ ## Thread Safety
134
+
135
+ The server spawns separate threads for:
136
+ - Main UDP receive loop (`serve_forever`)
137
+ - Packet handling (one thread per packet)
138
+ - Node network tick (periodic health checks)
139
+
140
+ All shared state is protected with appropriate locks.
141
+
142
+ ## Example: Multi-Node Setup
143
+
144
+ ```python
145
+ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
146
+
147
+ # Generate shared AES key (only once)
148
+ DSNodeServer.generate_key("shared.key")
149
+
150
+ # Start bootstrap node
151
+ bootstrap = DSNodeServer.start(DSNodeConfig(
152
+ node_id="bootstrap",
153
+ port=8000,
154
+ aes_key_file="shared.key",
155
+ bootstrap_nodes=[]
156
+ ))
157
+
158
+ # Start additional nodes
159
+ node1 = DSNodeServer.start(DSNodeConfig(
160
+ node_id="node1",
161
+ port=8001,
162
+ aes_key_file="shared.key",
163
+ bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
164
+ ))
165
+
166
+ node2 = DSNodeServer.start(DSNodeConfig(
167
+ node_id="node2",
168
+ port=8002,
169
+ aes_key_file="shared.key",
170
+ bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
171
+ ))
172
+
173
+ # Nodes automatically discover each other through bootstrap
174
+ ```
175
+
176
+ ## Troubleshooting
177
+
178
+ ### Server won't start
179
+ - Check if port is already in use
180
+ - Verify AES key file exists and is readable
181
+ - Ensure firewall allows UDP traffic on specified port
182
+
183
+ ### Nodes can't connect
184
+ - Verify all nodes use the same AES key
185
+ - Check bootstrap node is running and reachable
186
+ - Verify network connectivity (ping bootstrap address)
187
+ - Check firewall rules for UDP ports
188
+
189
+ ### Connection drops
190
+ - Increase timeout if on slow networks
191
+ - Check network stability
192
+ - Verify nodes aren't being stopped unexpectedly
@@ -0,0 +1,114 @@
1
+ ## Network Protocol
2
+
3
+ ### Transport Layer
4
+ The network now uses **HTTP (HyperText Transfer Protocol)** with a Flask server for all communication. This provides:
5
+ - Standard HTTP semantics for request/response patterns
6
+ - Better compatibility with firewalls and proxies
7
+ - Built-in error handling with HTTP status codes
8
+ - Easy integration with monitoring and debugging tools
9
+ - More flexible for future enhancements
10
+
11
+ ### Message Types and Endpoints
12
+ All HTTP requests use specific endpoints to identify the packet type:
13
+
14
+ - **POST /hello**: Exchange node information and credentials
15
+ - **POST /peers**: Request/share peer list
16
+ - **POST /update**: Send/receive state updates
17
+ - **POST /ping**: Connectivity check
18
+
19
+ ### Packet Structure
20
+ Each HTTP request follows this structure:
21
+ 1. **HTTP POST request** to the appropriate endpoint
22
+ 2. **Request Body**: AES Encrypted Payload containing:
23
+ - Message type (1 byte) - for verification
24
+ - Message payload (variable length)
25
+ 3. **Response Body**: AES Encrypted Payload containing:
26
+ - Message type (1 byte) - matches request type
27
+ - Response payload (variable length)
28
+
29
+ ### Message Types
30
+ Internal message type constants (used for verification):
31
+ - **Type 1 (HELLO)**: Exchange node information and credentials
32
+ - **Type 2 (PEERS)**: Request/share peer list
33
+ - **Type 3 (UPDATE)**: Send/receive state updates
34
+ - **Type 4 (PING)**: Connectivity check
35
+
36
+ ### Security
37
+ - All communication is encrypted using AES with a shared key
38
+ - Messages are signed using ECDSA for authentication
39
+ - HTTP request/response bodies are encrypted end-to-end
40
+ - HTTPS/TLS can be added by placing a reverse proxy (nginx, apache) in front of the Flask server
41
+
42
+ ### State Synchronization
43
+ - Nodes maintain a copy of all peers' states
44
+ - Updates are broadcast to all connected peers
45
+ - Timestamps prevent older updates from overwriting newer ones
46
+
47
+ ### HTTP Server Configuration
48
+ - Flask server runs on specified port with threading enabled
49
+ - Default timeout is 5 seconds for requests
50
+ - Maximum retry attempts: 3 with 0.5 second delay between retries
51
+ - Server runs in a separate daemon thread
52
+
53
+ ### HTTP Status Codes
54
+ - **200 OK**: Successful request with response data
55
+ - **204 No Content**: Successful request with no response data (e.g., some HELLO responses)
56
+ - **400 Bad Request**: Malformed request or message type mismatch
57
+ - **401 Unauthorized**: Signature verification failed
58
+ - **406 Not Acceptable**: Invalid data, stale update, or other validation error
59
+ - **500 Internal Server Error**: Unexpected server error
60
+ - **505 HTTP Version Not Supported**: Used for version mismatch errors
61
+
62
+ ## Important Notes
63
+
64
+ 1. **Shared AES Key**: All nodes in the network must use the same AES key file
65
+ 2. **Unique Node IDs**: Each node must have a unique node_id
66
+ 3. **Port Availability**: Ensure the specified HTTP port is available before starting
67
+ 4. **Bootstrap Nodes**: At least one bootstrap node is required to join an existing network
68
+ 5. **Network Tick**: The network performs maintenance checks every 3 seconds
69
+ 6. **Credential Management**: ECDSA keys are automatically generated and stored in `credentials/` directory
70
+ 7. **HTTP Reliability**: The protocol implements retry logic (up to 3 attempts) for failed requests
71
+ 8. **HTTPS Support**: While the base implementation uses HTTP, you can add HTTPS by:
72
+ - Using a reverse proxy (nginx, apache) with SSL certificates
73
+ - Configuring Flask with SSL context (requires certificate files)
74
+
75
+ ## Error Handling
76
+
77
+ Common error codes:
78
+ - **401**: Not Authorized (signature verification failed)
79
+ - **406**: Not Acceptable (invalid data, stale update, or version mismatch)
80
+ - **505**: Version not supported
81
+
82
+ ## Migration from UDP
83
+
84
+ If you're migrating from the UDP-based version:
85
+ 1. Install Flask: The dependency is now included in pyproject.toml
86
+ 2. Update firewall rules to allow HTTP traffic on your ports instead of UDP
87
+ 3. The API remains largely the same, but transport is now HTTP
88
+ 4. HTTP endpoints replace UDP message types for routing
89
+ 5. Consider adding HTTPS via reverse proxy for production deployments
90
+
91
+ ## Example URLs
92
+
93
+ For a node running on `192.168.1.100:8080`:
94
+ - HELLO endpoint: `http://192.168.1.100:8080/hello`
95
+ - PEERS endpoint: `http://192.168.1.100:8080/peers`
96
+ - UPDATE endpoint: `http://192.168.1.100:8080/update`
97
+ - PING endpoint: `http://192.168.1.100:8080/ping`
98
+
99
+ ## Request Flow
100
+
101
+ 1. **Client prepares message**: Message type byte + payload
102
+ 2. **Client encrypts**: AES encryption applied to entire message
103
+ 3. **Client sends**: HTTP POST to appropriate endpoint
104
+ 4. **Server receives**: Flask routes to appropriate handler
105
+ 5. **Server decrypts**: AES decryption applied
106
+ 6. **Server validates**: Message type verification, signature checks
107
+ 7. **Server processes**: Business logic executed
108
+ 8. **Server prepares response**: Message type byte + response payload
109
+ 9. **Server encrypts**: AES encryption applied
110
+ 10. **Server sends**: HTTP response with encrypted body
111
+ 11. **Client receives**: Response status and body
112
+ 12. **Client decrypts**: AES decryption applied
113
+ 13. **Client validates**: Message type verification
114
+ 14. **Client processes**: Response data used
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling", "cryptography", "logging", "ipaddress", "requests", "ecdsa"]
2
+ requires = ["hatchling", "cryptography", "logging", "ipaddress", "requests", "ecdsa", "flask"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "distributed-state-network"
7
- version = "0.2.1"
7
+ version = "0.4.0"
8
8
  authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
9
  description = "A tool to distribute the state of a network device to other devices on the network"
10
10
  keywords = ["distributed", "networking"]
@@ -26,7 +26,8 @@ dependencies = [
26
26
  "logging",
27
27
  "ipaddress",
28
28
  "requests",
29
- "ecdsa"
29
+ "ecdsa",
30
+ "flask"
30
31
  ]
31
32
 
32
33
  [project.urls]
@@ -34,4 +35,4 @@ Homepage = "https://github.com/erinclemmer/distributed_state_network"
34
35
  Issues = "https://github.com/erinclemmer/distributed_state_network/issues"
35
36
 
36
37
  [tool.hatch.build.targets.wheel]
37
- packages = ["src/distributed_state_network"]
38
+ packages = ["src/distributed_state_network"]
@@ -1,8 +1,8 @@
1
1
  import os
2
2
  import time
3
- import socket
4
3
  import logging
5
4
  import threading
5
+ import requests
6
6
  from typing import Dict, List, Optional, Callable
7
7
 
8
8
  from distributed_state_network.objects.endpoint import Endpoint
@@ -16,7 +16,7 @@ from distributed_state_network.util.key_manager import CredentialManager
16
16
  from distributed_state_network.util.aes import aes_encrypt, aes_decrypt
17
17
 
18
18
  TICK_INTERVAL = 3
19
- UDP_TIMEOUT = 2 # seconds
19
+ HTTP_TIMEOUT = 2 # seconds
20
20
 
21
21
  # Message type constants (must match handler.py)
22
22
  MSG_HELLO = 1
@@ -24,6 +24,14 @@ MSG_PEERS = 2
24
24
  MSG_UPDATE = 3
25
25
  MSG_PING = 4
26
26
 
27
+ # Map message types to endpoint paths
28
+ MSG_TYPE_TO_PATH = {
29
+ MSG_HELLO: '/hello',
30
+ MSG_PEERS: '/peers',
31
+ MSG_UPDATE: '/update',
32
+ MSG_PING: '/ping'
33
+ }
34
+
27
35
  class DSNode:
28
36
  config: DSNodeConfig
29
37
  address_book: Dict[str, Endpoint]
@@ -34,13 +42,12 @@ class DSNode:
34
42
  self,
35
43
  config: DSNodeConfig,
36
44
  version: str,
37
- sock: Optional[socket.socket] = None,
38
45
  disconnect_callback: Optional[Callable] = None,
39
46
  update_callback: Optional[Callable] = None
40
47
  ):
41
48
  self.config = config
42
49
  self.version = version
43
- self.socket = sock # Use the server's socket for sending
50
+ self.server = None # Reference to the Flask server
44
51
 
45
52
  self.cred_manager = CredentialManager(config.node_id)
46
53
  self.cred_manager.generate_keys()
@@ -61,15 +68,11 @@ class DSNode:
61
68
  if not os.path.exists(config.aes_key_file):
62
69
  raise Exception(f"Could not find aes key file in {config.aes_key_file}")
63
70
 
64
- # Response tracking for request/response matching
65
- self.pending_responses = {}
66
- self.response_lock = threading.Lock()
67
-
68
71
  threading.Thread(target=self.network_tick, daemon=True).start()
69
72
 
70
- def set_socket(self, sock: socket.socket):
71
- """Set the socket to use for sending requests"""
72
- self.socket = sock
73
+ def set_server(self, server):
74
+ """Set reference to the Flask server"""
75
+ self.server = server
73
76
 
74
77
  def get_aes_key(self):
75
78
  with open(self.config.aes_key_file, 'rb') as f:
@@ -102,83 +105,68 @@ class DSNode:
102
105
  if self.disconnect_cb is not None:
103
106
  self.disconnect_cb()
104
107
 
105
- def send_udp_request(self, endpoint: Endpoint, msg_type: int, payload: bytes, retries: int = 0) -> bytes:
106
- """Send UDP request and wait for response using the server's socket"""
107
- if self.socket is None:
108
- raise Exception("Socket not set. Cannot send UDP request.")
109
-
108
+ def send_http_request(self, endpoint: Endpoint, msg_type: int, payload: bytes, retries: int = 0) -> bytes:
109
+ """Send HTTP request and wait for response"""
110
110
  try:
111
111
  # Prepend message type to payload
112
112
  data_with_type = bytes([msg_type]) + payload
113
113
  encrypted_data = self.encrypt_data(data_with_type)
114
114
 
115
- # Create a unique ID for this request based on endpoint and msg type
116
- request_id = (endpoint.address, endpoint.port, msg_type)
115
+ # Determine the URL path based on message type
116
+ path = MSG_TYPE_TO_PATH.get(msg_type, '/unknown')
117
+ url = f"http://{endpoint.address}:{endpoint.port}{path}"
117
118
 
118
- # Set up response tracking
119
- response_event = threading.Event()
120
- with self.response_lock:
121
- self.pending_responses[request_id] = {'event': response_event, 'data': None}
119
+ # Send HTTP POST request
120
+ response = requests.post(
121
+ url,
122
+ data=encrypted_data,
123
+ headers={'Content-Type': 'application/octet-stream'},
124
+ timeout=HTTP_TIMEOUT
125
+ )
122
126
 
123
- # Send the packet using the server's socket
124
- self.socket.sendto(encrypted_data, (endpoint.address, endpoint.port))
127
+ # Check response status
128
+ if response.status_code == 204:
129
+ # No content - valid for some responses like successful HELLO with no data
130
+ return b''
131
+ elif response.status_code != 200:
132
+ raise Exception(f"HTTP error: {response.status_code}")
125
133
 
126
- # Wait for response
127
- if response_event.wait(timeout=UDP_TIMEOUT):
128
- with self.response_lock:
129
- response_data = self.pending_responses[request_id]['data']
130
- del self.pending_responses[request_id]
131
-
132
- if response_data is None:
133
- raise Exception("Response was None")
134
-
135
- return response_data
136
- else:
137
- # Timeout
138
- with self.response_lock:
139
- if request_id in self.pending_responses:
140
- del self.pending_responses[request_id]
141
- raise socket.timeout("Request timed out")
134
+ # Decrypt the response
135
+ decrypted = self.decrypt_data(response.content)
136
+
137
+ if len(decrypted) < 1:
138
+ raise Exception("Empty response")
142
139
 
143
- except socket.timeout:
140
+ # First byte is message type
141
+ response_msg_type = decrypted[0]
142
+ if response_msg_type != msg_type:
143
+ raise Exception(f"Response message type mismatch: expected {msg_type}, got {response_msg_type}")
144
+
145
+ # Return the body (everything after the message type byte)
146
+ return decrypted[1:]
147
+
148
+ except requests.exceptions.Timeout:
144
149
  if retries < 2:
145
150
  time.sleep(0.5)
146
- return self.send_udp_request(endpoint, msg_type, payload, retries + 1)
151
+ return self.send_http_request(endpoint, msg_type, payload, retries + 1)
147
152
  else:
148
- raise Exception(f"UDP request to {endpoint.to_string()} timed out")
149
- except Exception as e:
153
+ raise Exception(f"HTTP request to {endpoint.to_string()} timed out")
154
+ except requests.exceptions.RequestException as e:
150
155
  if retries < 2:
151
156
  time.sleep(0.5)
152
- return self.send_udp_request(endpoint, msg_type, payload, retries + 1)
157
+ return self.send_http_request(endpoint, msg_type, payload, retries + 1)
153
158
  else:
154
- raise Exception(f"UDP request to {endpoint.to_string()} failed: {e}")
155
-
156
- def handle_response(self, data: bytes, addr: tuple):
157
- """Handle incoming response and match it to pending request"""
158
- try:
159
- # Decrypt the data
160
- decrypted = self.decrypt_data(data)
161
-
162
- if len(decrypted) == 0:
163
- return
164
-
165
- # First byte is message type
166
- msg_type = decrypted[0]
167
- body = decrypted[1:]
168
-
169
- # Find matching pending request
170
- request_id = (addr[0], addr[1], msg_type)
171
-
172
- with self.response_lock:
173
- if request_id in self.pending_responses:
174
- self.pending_responses[request_id]['data'] = body
175
- self.pending_responses[request_id]['event'].set()
159
+ raise Exception(f"HTTP request to {endpoint.to_string()} failed: {e}")
176
160
  except Exception as e:
177
- self.logger.error(f"Error handling response from {addr}: {e}")
161
+ if retries < 2:
162
+ time.sleep(0.5)
163
+ return self.send_http_request(endpoint, msg_type, payload, retries + 1)
164
+ else:
165
+ raise Exception(f"HTTP request to {endpoint.to_string()} failed: {e}")
178
166
 
179
167
  def send_request_to_node(self, node_id: str, msg_type: int, payload: bytes) -> bytes:
180
168
  con = self.connection_from_node(node_id)
181
- return self.send_udp_request(con, msg_type, payload)
169
+ return self.send_http_request(con, msg_type, payload)
182
170
 
183
171
  def encrypt_data(self, data: bytes) -> bytes:
184
172
  return aes_encrypt(self.get_aes_key(), data)
@@ -226,7 +214,7 @@ class DSNode:
226
214
  self.logger.info(f"HELLO => {con.to_string()}")
227
215
 
228
216
  payload = self.my_hello_packet().to_bytes()
229
- content = self.send_udp_request(con, MSG_HELLO, payload)
217
+ content = self.send_http_request(con, MSG_HELLO, payload)
230
218
 
231
219
  # Get the response packet
232
220
  pkt = HelloPacket.from_bytes(content)
@@ -256,7 +244,7 @@ class DSNode:
256
244
 
257
245
  return pkt.node_id
258
246
 
259
- def handle_hello(self, data: bytes, detected_address: str = None) -> bytes:
247
+ def handle_hello(self, data: bytes, detected_address: str = None) -> bytes | None:
260
248
  pkt = HelloPacket.from_bytes(data)
261
249
  self.logger.info(f"Received HELLO from {pkt.node_id}")
262
250
  if pkt.version != self.version:
@@ -268,6 +256,8 @@ class DSNode:
268
256
 
269
257
  if pkt.node_id not in self.address_book:
270
258
  self.address_book[pkt.node_id] = pkt.connection
259
+ else:
260
+ return None
271
261
 
272
262
  if pkt.node_id not in self.node_states:
273
263
  self.node_states[pkt.node_id] = StatePacket(pkt.node_id, 0, b'', { })
@@ -284,7 +274,7 @@ class DSNode:
284
274
  self.my_con(),
285
275
  self.cred_manager.my_public(),
286
276
  None,
287
- None # No certificate for UDP
277
+ None # No certificate for HTTP
288
278
  )
289
279
  pkt.sign(self.cred_manager.my_private())
290
280
  return pkt
@@ -0,0 +1,156 @@
1
+ import sys
2
+ import threading
3
+ import logging
4
+ from typing import Callable, Optional
5
+ from flask import Flask, request, Response
6
+ from distributed_state_network.dsnode import DSNode
7
+ from distributed_state_network.objects.config import DSNodeConfig
8
+ from distributed_state_network.util.aes import generate_aes_key
9
+ from distributed_state_network.util import stop_thread
10
+
11
+ VERSION = "0.4.0"
12
+ logging.basicConfig(level=logging.INFO)
13
+
14
+ # Silence Flask and Werkzeug logging
15
+ logging.getLogger('werkzeug').setLevel(logging.ERROR)
16
+
17
+ # Message type constants
18
+ MSG_HELLO = 1
19
+ MSG_PEERS = 2
20
+ MSG_UPDATE = 3
21
+ MSG_PING = 4
22
+
23
+ class DSNodeServer:
24
+ def __init__(
25
+ self,
26
+ config: DSNodeConfig,
27
+ disconnect_callback: Optional[Callable] = None,
28
+ update_callback: Optional[Callable] = None
29
+ ):
30
+ self.config = config
31
+ self.running = False
32
+
33
+ # Create Flask app
34
+ self.app = Flask(__name__)
35
+ self.app.logger.setLevel(logging.ERROR) # Reduce Flask's logging noise
36
+
37
+ # Create DSNode
38
+ self.node = DSNode(config, VERSION, disconnect_callback, update_callback)
39
+ self.node.set_server(self) # Give node reference to server for making HTTP requests
40
+
41
+ # Set up Flask routes
42
+ self._setup_routes()
43
+
44
+ def _setup_routes(self):
45
+ """Set up Flask routes for each message type"""
46
+
47
+ @self.app.route('/hello', methods=['POST'])
48
+ def handle_hello_route():
49
+ return self._handle_request(MSG_HELLO, request.data, request.remote_addr)
50
+
51
+ @self.app.route('/peers', methods=['POST'])
52
+ def handle_peers_route():
53
+ return self._handle_request(MSG_PEERS, request.data, request.remote_addr)
54
+
55
+ @self.app.route('/update', methods=['POST'])
56
+ def handle_update_route():
57
+ return self._handle_request(MSG_UPDATE, request.data, request.remote_addr)
58
+
59
+ @self.app.route('/ping', methods=['POST'])
60
+ def handle_ping_route():
61
+ return self._handle_request(MSG_PING, request.data, request.remote_addr)
62
+
63
+ def _handle_request(self, msg_type: int, data: bytes, remote_addr: str) -> Response:
64
+ if not self.running:
65
+ return Response(status=500)
66
+ """Handle incoming HTTP request"""
67
+ try:
68
+ # Decrypt the data
69
+ decrypted = self.node.decrypt_data(data)
70
+
71
+ if len(decrypted) < 1:
72
+ return Response(status=400)
73
+
74
+ # First byte should be message type (for verification)
75
+ received_msg_type = decrypted[0]
76
+ body = decrypted[1:]
77
+
78
+ if received_msg_type != msg_type:
79
+ self.node.logger.error(f"Message type mismatch: expected {msg_type}, got {received_msg_type}")
80
+ return Response(status=400)
81
+
82
+ response_data = None
83
+
84
+ if msg_type == MSG_HELLO:
85
+ # Pass the detected IP address to handle_hello
86
+ response_data = self.node.handle_hello(body, remote_addr)
87
+
88
+ elif msg_type == MSG_PEERS:
89
+ response_data = self.node.handle_peers(body)
90
+
91
+ elif msg_type == MSG_UPDATE:
92
+ response_data = self.node.handle_update(body)
93
+
94
+ elif msg_type == MSG_PING:
95
+ response_data = b''
96
+
97
+ # Send response if handler returned data
98
+ if response_data is not None:
99
+ # Prepend message type to response
100
+ response_with_type = bytes([msg_type]) + response_data
101
+ encrypted_response = self.node.encrypt_data(response_with_type)
102
+ return Response(encrypted_response, status=200, mimetype='application/octet-stream')
103
+ else:
104
+ return Response(status=204) # No content
105
+
106
+ except Exception as e:
107
+ if len(e.args) >= 2 and isinstance(e.args[0], int):
108
+ # Error with HTTP status code
109
+ self.node.logger.error(f"Error handling {msg_type} from {remote_addr}: {e.args[1]}")
110
+ return Response(status=e.args[0])
111
+ else:
112
+ self.node.logger.error(f"Error handling {msg_type} from {remote_addr}: {e}")
113
+ return Response(status=500)
114
+
115
+ def stop(self):
116
+ self.node.shutting_down = True
117
+ self.running = False
118
+ if hasattr(self, 'thread'):
119
+ stop_thread(self.thread)
120
+
121
+ def serve_forever(self, port: int):
122
+ if self.running:
123
+ return
124
+ # Suppress Flask startup messages
125
+ cli = sys.modules['flask.cli']
126
+ cli.show_server_banner = lambda *x: None
127
+
128
+ self.running = True
129
+ self.app.run(host='0.0.0.0', port=port, threaded=True, use_reloader=False)
130
+ self.node.logger.info(f'Started DSNode on HTTP port {port}')
131
+
132
+ @staticmethod
133
+ def generate_key(out_file_path: str):
134
+ key = generate_aes_key()
135
+ with open(out_file_path, 'wb') as f:
136
+ f.write(key)
137
+
138
+ @staticmethod
139
+ def start(
140
+ config: DSNodeConfig,
141
+ disconnect_callback: Optional[Callable] = None,
142
+ update_callback: Optional[Callable] = None
143
+ ) -> 'DSNodeServer':
144
+ n = DSNodeServer(config, disconnect_callback, update_callback)
145
+ n.thread = threading.Thread(target=n.serve_forever, daemon=True, args=(config.port, ))
146
+ n.thread.start()
147
+
148
+ if n.config.bootstrap_nodes is not None and len(n.config.bootstrap_nodes) > 0:
149
+ for bs in n.config.bootstrap_nodes:
150
+ try:
151
+ n.node.bootstrap(bs)
152
+ break # Throws exception if connection is not made
153
+ except Exception as e:
154
+ print(e)
155
+
156
+ return n
@@ -3,9 +3,8 @@ import sys
3
3
  import time
4
4
  import random
5
5
  import shutil
6
- import logging
7
6
  import unittest
8
- import socket
7
+ import requests
9
8
  from typing import List, Dict
10
9
 
11
10
  sys.path.append(os.path.join(os.path.dirname(__file__), './src'))
@@ -26,7 +25,7 @@ key_file = "src/distributed_state_network/test.key"
26
25
  if not os.path.exists(key_file):
27
26
  DSNodeServer.generate_key(key_file)
28
27
 
29
- def spawn_node(node_id: str, bootstrap_nodes: List[Dict] = [], sock: socket.socket = None):
28
+ def spawn_node(node_id: str, bootstrap_nodes: List[Dict] = []):
30
29
  global current_port
31
30
  current_port += 1
32
31
  n = DSNodeServer.start(DSNodeConfig.from_dict({
@@ -34,7 +33,7 @@ def spawn_node(node_id: str, bootstrap_nodes: List[Dict] = [], sock: socket.sock
34
33
  "port": current_port,
35
34
  "aes_key_file": key_file,
36
35
  "bootstrap_nodes": bootstrap_nodes
37
- }), sock=sock)
36
+ }))
38
37
  global nodes
39
38
  nodes.append(n)
40
39
  return n
@@ -140,10 +139,10 @@ class TestNode(unittest.TestCase):
140
139
  self.assertEqual(None, bootstrap.node.read_data("connector", "foo"))
141
140
 
142
141
  connector.node.update_data("foo", "bar")
143
- time.sleep(0.5) # Give time for UDP packet to arrive
142
+ time.sleep(0.5) # Give time for HTTP request to complete
144
143
  self.assertEqual("bar", bootstrap.node.read_data("connector", "foo"))
145
144
  bootstrap.node.update_data("bar", "baz")
146
- time.sleep(0.5) # Give time for UDP packet to arrive
145
+ time.sleep(0.5) # Give time for HTTP request to complete
147
146
  self.assertEqual("baz", connector.node.read_data("bootstrap", "bar"))
148
147
 
149
148
  def test_bad_aes_key(self):
@@ -154,38 +153,39 @@ class TestNode(unittest.TestCase):
154
153
  print(e)
155
154
 
156
155
  def test_authorization(self):
157
- """Test that unauthorized UDP packets are rejected"""
156
+ """Test that unauthorized HTTP requests are rejected"""
158
157
  n = spawn_node("node")
159
158
 
160
- # Create a UDP socket to send unauthorized data
161
- test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
162
- test_sock.settimeout(2)
159
+ # Give Flask server time to start
160
+ time.sleep(0.5)
163
161
 
164
- # Send unencrypted data - should be rejected
165
- test_sock.sendto(b'TEST', ('127.0.0.1', n.config.port))
166
-
167
- # Try to receive response (should not get one for bad data)
162
+ # Send unencrypted data - should be rejected with 500 error
168
163
  try:
169
- data, addr = test_sock.recvfrom(1024)
170
- # If we get here, check that response indicates error
171
- print(f"Received response: {data}")
172
- except socket.timeout:
173
- # This is expected - no valid response for bad data
174
- pass
164
+ response = requests.post(
165
+ f'http://127.0.0.1:{n.config.port}/ping',
166
+ data=b'TEST',
167
+ timeout=2
168
+ )
169
+ # Should get error status code
170
+ self.assertNotEqual(response.status_code, 200)
171
+ print(f"Received status code for bad data: {response.status_code}")
172
+ except Exception as e:
173
+ # Connection errors are also acceptable
174
+ print(f"Request failed as expected: {e}")
175
175
 
176
176
  # Send properly encrypted data
177
177
  encrypted_data = n.node.encrypt_data(bytes([4]) + b'TEST') # MSG_PING = 4
178
- test_sock.sendto(encrypted_data, ('127.0.0.1', n.config.port))
178
+ response = requests.post(
179
+ f'http://127.0.0.1:{n.config.port}/ping',
180
+ data=encrypted_data,
181
+ headers={'Content-Type': 'application/octet-stream'},
182
+ timeout=2
183
+ )
179
184
 
180
- # Should get a response for valid encrypted ping
181
- try:
182
- data, addr = test_sock.recvfrom(1024)
183
- decrypted = n.node.decrypt_data(data)
184
- self.assertEqual(decrypted[0], 4) # Should be MSG_PING response
185
- except socket.timeout:
186
- self.fail("Should receive response for valid encrypted ping")
187
-
188
- test_sock.close()
185
+ # Should get successful response for valid encrypted ping
186
+ self.assertEqual(response.status_code, 200)
187
+ decrypted = n.node.decrypt_data(response.content)
188
+ self.assertEqual(decrypted[0], 4) # Should be MSG_PING response
189
189
 
190
190
  def test_version_matching(self):
191
191
  bootstrap = spawn_node("bootstrap")
@@ -205,7 +205,7 @@ class TestNode(unittest.TestCase):
205
205
  connector = spawn_node("connector", [bootstrap.node.my_con().to_json()])
206
206
  try:
207
207
  # Try to send malformed data
208
- connector.node.send_udp_request(bootstrap.node.my_con(), 1, b'MALFORMED_DATA')
208
+ connector.node.send_http_request(bootstrap.node.my_con(), 1, b'MALFORMED_DATA')
209
209
  self.fail("Should throw error for malformed data")
210
210
  except Exception as e:
211
211
  print(e)
@@ -352,20 +352,26 @@ class TestNode(unittest.TestCase):
352
352
  connector = spawn_node("connector", [bootstrap.node.my_con().to_json()])
353
353
  self.assertIn('connector', bootstrap.node.peers())
354
354
 
355
- def test_custom_socket(self):
356
- """Test that a custom socket can be provided"""
357
- # Create a custom UDP socket
358
- custom_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
359
- custom_sock.bind(("0.0.0.0", 9000))
360
-
361
- # Start node with custom socket
362
- n = spawn_node("custom-socket-node", sock=custom_sock)
355
+ def test_http_endpoints(self):
356
+ """Test that HTTP endpoints are accessible"""
357
+ n = spawn_node("http-test-node")
363
358
 
364
- # Verify the node is using the custom socket
365
- self.assertEqual(n.socket, custom_sock)
359
+ # Test that all endpoints exist
360
+ endpoints = ['/hello', '/peers', '/update', '/ping']
366
361
 
367
- # Clean up
368
- n.stop()
362
+ for endpoint in endpoints:
363
+ # Send a request to each endpoint (will fail due to encryption, but should return 400/500, not 404)
364
+ try:
365
+ response = requests.post(
366
+ f'http://127.0.0.1:{n.config.port}{endpoint}',
367
+ data=b'test',
368
+ timeout=2
369
+ )
370
+ # Should not be 404 (Not Found)
371
+ self.assertNotEqual(response.status_code, 404, f"Endpoint {endpoint} not found")
372
+ print(f"Endpoint {endpoint} exists (status: {response.status_code})")
373
+ except Exception as e:
374
+ print(f"Endpoint {endpoint} test failed: {e}")
369
375
 
370
376
  if __name__ == "__main__":
371
377
  unittest.main()
@@ -33,7 +33,7 @@ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional
33
33
  else:
34
34
  args["bootstrap_nodes"] = []
35
35
 
36
- return DSNodeServer.start(DSNodeConfig(**args), None, disconnect_cb, update_cb)
36
+ return DSNodeServer.start(DSNodeConfig(**args), disconnect_cb, update_cb)
37
37
 
38
38
  class ConnectionsTest(unittest.TestCase):
39
39
  def test_one(self):
@@ -1,58 +0,0 @@
1
- ## DSNodeServer
2
-
3
- HTTPS server wrapper for DSNode that handles incoming network requests.
4
-
5
- ```python
6
- from distributed_state_network import DSNodeServer
7
- ```
8
-
9
- ### Class Definition
10
- ```python
11
- class DSNodeServer(HTTPServer):
12
- config: DSNodeConfig
13
- node: DSNode
14
- ```
15
-
16
- ### Constructor
17
-
18
- **Parameters:**
19
- - `config` (`DSNodeConfig`): Node configuration
20
- - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
21
- - `update_callback` (`Optional[Callable]`): Callback for state update events
22
-
23
- ### Static Methods
24
-
25
- ### `start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> DSNodeServer`
26
- Creates and starts a new DSNodeServer instance with SSL configuration.
27
- ```python
28
- server = DSNodeServer.start(config)
29
- ```
30
-
31
- **Parameters:**
32
- - `config` (`DSNodeConfig`): Node configuration
33
- - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events (no parameters)
34
- - `update_callback` (`Optional[Callable]`): Callback for state update events (no parameters)
35
-
36
- **Returns:**
37
- - `DSNodeServer`: Running server instance
38
-
39
- ### `generate_key(out_file_path: str) -> None`
40
- Generates a new AES key file for network encryption.
41
-
42
- **Parameters:**
43
- - `out_file_path` (`str`): Path where the key file will be saved
44
-
45
- **Example:**
46
- ```python
47
- DSNodeServer.generate_key("/path/to/network.key")
48
- ```
49
-
50
- ### Instance Methods
51
-
52
- ### `stop() -> None`
53
- Gracefully shuts down the server and cleans up resources.
54
-
55
- **Example:**
56
- ```python
57
- server.stop()
58
- ```
@@ -1,65 +0,0 @@
1
- ## Network Protocol
2
-
3
- ### Transport Layer
4
- The network now uses **UDP (User Datagram Protocol)** for all communication instead of HTTP/HTTPS. This provides:
5
- - Lower latency for peer-to-peer communication
6
- - Reduced overhead compared to HTTP
7
- - Simpler connection management
8
- - More suitable for real-time distributed systems
9
-
10
- ### Message Types
11
- All UDP packets use a message type byte prefix to identify the packet type:
12
-
13
- - **Type 1 (HELLO)**: Exchange node information and credentials
14
- - **Type 2 (PEERS)**: Request/share peer list
15
- - **Type 3 (UPDATE)**: Send/receive state updates
16
- - **Type 4 (PING)**: Connectivity check
17
-
18
- ### Packet Structure
19
- Each UDP packet follows this structure:
20
- 1. **AES Encrypted Payload** containing:
21
- - Message type (1 byte)
22
- - Message payload (variable length)
23
-
24
- ### Security
25
- - All communication is encrypted using AES with a shared key
26
- - Messages are signed using ECDSA for authentication
27
- - UDP packets are encrypted end-to-end
28
- - HTTPS/TLS is not supported with UDP (certificates are no longer used)
29
-
30
- ### State Synchronization
31
- - Nodes maintain a copy of all peers' states
32
- - Updates are broadcast to all connected peers
33
- - Timestamps prevent older updates from overwriting newer ones
34
-
35
- ### Socket Management
36
- - You can provide a custom UDP socket when starting a server
37
- - If no socket is provided, one will be created and bound automatically
38
- - Socket timeout is set to 2 seconds for requests
39
- - Maximum UDP packet size is 65507 bytes
40
-
41
- ## Important Notes
42
-
43
- 1. **Shared AES Key**: All nodes in the network must use the same AES key file
44
- 2. **Unique Node IDs**: Each node must have a unique node_id
45
- 3. **Port Availability**: Ensure the specified UDP port is available before starting
46
- 4. **Bootstrap Nodes**: At least one bootstrap node is required to join an existing network
47
- 5. **Network Tick**: The network performs maintenance checks every 3 seconds
48
- 6. **Credential Management**: ECDSA keys are automatically generated and stored in `credentials/` directory
49
- 7. **UDP Reliability**: The protocol implements retry logic (up to 3 attempts) for failed requests
50
- 8. **HTTPS Not Supported**: The `https` configuration option is ignored when using UDP
51
-
52
- ## Error Handling
53
-
54
- Common error codes (embedded in exception messages):
55
- - **401**: Not Authorized (signature verification failed)
56
- - **406**: Not Acceptable (invalid data, stale update, or version mismatch)
57
- - **505**: Version not supported
58
-
59
- ## Migration from HTTP
60
-
61
- If you're migrating from the HTTP-based version:
62
- 1. Remove any HTTPS certificate dependencies
63
- 2. Update firewall rules to allow UDP traffic on your ports
64
- 3. The API remains largely the same, but transport is now UDP
65
- 4. The `https` flag in configuration is now ignored
@@ -1,144 +0,0 @@
1
- import socket
2
- import threading
3
- import logging
4
- from typing import Callable, Optional
5
- from distributed_state_network.dsnode import DSNode
6
- from distributed_state_network.objects.config import DSNodeConfig
7
- from distributed_state_network.util.aes import generate_aes_key
8
- from distributed_state_network.util import stop_thread
9
-
10
- VERSION = "0.2.1"
11
- logging.basicConfig(level=logging.INFO)
12
-
13
- # Message type constants
14
- MSG_HELLO = 1
15
- MSG_PEERS = 2
16
- MSG_UPDATE = 3
17
- MSG_PING = 4
18
-
19
- class DSNodeServer:
20
- def __init__(
21
- self,
22
- config: DSNodeConfig,
23
- sock: Optional[socket.socket] = None,
24
- disconnect_callback: Optional[Callable] = None,
25
- update_callback: Optional[Callable] = None
26
- ):
27
- self.config = config
28
- self.running = True
29
-
30
- # Use provided socket or create new one
31
- if sock is not None:
32
- self.socket = sock
33
- else:
34
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
35
- self.socket.bind(("0.0.0.0", config.port))
36
-
37
- # Create DSNode with the socket
38
- self.node = DSNode(config, VERSION, self.socket, disconnect_callback, update_callback)
39
- self.node.logger.info(f'Started DSNode on UDP port {config.port}')
40
-
41
- def stop(self):
42
- self.node.shutting_down = True
43
- self.running = False
44
- try:
45
- self.socket.close()
46
- except:
47
- pass
48
- stop_thread(self.thread)
49
-
50
- def handle_packet(self, data: bytes, addr: tuple):
51
- """Handle incoming UDP packet - either a request or a response"""
52
- try:
53
- # Decrypt the data
54
- decrypted = self.node.decrypt_data(data)
55
-
56
- if len(decrypted) == 0:
57
- return
58
-
59
- # First byte is message type
60
- msg_type = decrypted[0]
61
- body = decrypted[1:]
62
-
63
- # Check if this is a response to a pending request
64
- request_id = (addr[0], addr[1], msg_type)
65
- with self.node.response_lock:
66
- is_response = request_id in self.node.pending_responses
67
-
68
- if is_response:
69
- # This is a response to our request - route to response handler
70
- self.node.handle_response(data, addr)
71
- else:
72
- # This is a new request - handle it
73
- response = None
74
-
75
- if msg_type == MSG_HELLO:
76
- # Pass the detected IP address to handle_hello
77
- response = self.node.handle_hello(body, addr[0])
78
-
79
- elif msg_type == MSG_PEERS:
80
- response = self.node.handle_peers(body)
81
-
82
- elif msg_type == MSG_UPDATE:
83
- response = self.node.handle_update(body)
84
-
85
- elif msg_type == MSG_PING:
86
- response = b''
87
-
88
- # Send response if handler returned data
89
- if response is not None:
90
- # Prepend message type to response
91
- response_with_type = bytes([msg_type]) + response
92
- encrypted_response = self.node.encrypt_data(response_with_type)
93
- self.socket.sendto(encrypted_response, addr)
94
-
95
- except Exception as e:
96
- if len(e.args) >= 2:
97
- self.node.logger.error(f"Error handling packet from {addr}: {e.args[0]} {e.args[1]}")
98
- else:
99
- self.node.logger.error(f"Error handling packet from {addr}: {e}")
100
-
101
- def serve_forever(self):
102
- """Main UDP server loop"""
103
- while self.running:
104
- try:
105
- # Receive UDP packet (max 65507 bytes for UDP)
106
- data, addr = self.socket.recvfrom(65507)
107
- if self.running:
108
- # Handle packet in separate thread to avoid blocking
109
- threading.Thread(target=self.handle_packet, args=(data, addr), daemon=True).start()
110
- except OSError:
111
- # Socket was closed
112
- if not self.running:
113
- break
114
- except Exception as e:
115
- self.node.logger.error(f"Error in server loop: {e}")
116
- if not self.running:
117
- break
118
-
119
- @staticmethod
120
- def generate_key(out_file_path: str):
121
- key = generate_aes_key()
122
- with open(out_file_path, 'wb') as f:
123
- f.write(key)
124
-
125
- @staticmethod
126
- def start(
127
- config: DSNodeConfig,
128
- sock: Optional[socket.socket] = None,
129
- disconnect_callback: Optional[Callable] = None,
130
- update_callback: Optional[Callable] = None
131
- ) -> 'DSNodeServer':
132
- n = DSNodeServer(config, sock, disconnect_callback, update_callback)
133
- n.thread = threading.Thread(target=n.serve_forever, daemon=True)
134
- n.thread.start()
135
-
136
- if n.config.bootstrap_nodes is not None and len(n.config.bootstrap_nodes) > 0:
137
- for bs in n.config.bootstrap_nodes:
138
- try:
139
- n.node.bootstrap(bs)
140
- break # Throws exception if connection is not made
141
- except Exception as e:
142
- print(e)
143
-
144
- return n