distributed-state-network 0.3.0__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.
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/PKG-INFO +2 -1
- distributed_state_network-0.4.0/documentation/protocol.md +114 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/pyproject.toml +5 -4
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/dsnode.py +64 -75
- distributed_state_network-0.4.0/src/distributed_state_network/handler.py +156 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/test.py +49 -43
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/tests/connections.py +1 -1
- distributed_state_network-0.3.0/documentation/protocol.md +0 -66
- distributed_state_network-0.3.0/src/distributed_state_network/handler.py +0 -145
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/.github/workflows/publish.yml +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/.gitignore +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/LICENSE +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/README.md +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/coverage.sh +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node-config.md +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node-server.md +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node.md +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/usage.md +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/__init__.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/create_key.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/config.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/endpoint.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/hello_packet.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/msg_types.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/peers_packet.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/signed_packet.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/objects/state_packet.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/__init__.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/aes.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/byte_helper.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/ecdsa.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/https.py +0 -0
- {distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/src/distributed_state_network/util/key_manager.py +0 -0
- {distributed_state_network-0.3.0 → 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.
|
|
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,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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
71
|
-
"""Set
|
|
72
|
-
self.
|
|
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,84 +105,68 @@ class DSNode:
|
|
|
102
105
|
if self.disconnect_cb is not None:
|
|
103
106
|
self.disconnect_cb()
|
|
104
107
|
|
|
105
|
-
def
|
|
106
|
-
"""Send
|
|
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
|
-
# Prepend message type
|
|
112
|
-
data_with_type = bytes([msg_type
|
|
111
|
+
# Prepend message type to payload
|
|
112
|
+
data_with_type = bytes([msg_type]) + payload
|
|
113
113
|
encrypted_data = self.encrypt_data(data_with_type)
|
|
114
114
|
|
|
115
|
-
#
|
|
116
|
-
|
|
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
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
#
|
|
124
|
-
|
|
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
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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.
|
|
151
|
+
return self.send_http_request(endpoint, msg_type, payload, retries + 1)
|
|
147
152
|
else:
|
|
148
|
-
raise Exception(f"
|
|
149
|
-
except
|
|
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.
|
|
157
|
+
return self.send_http_request(endpoint, msg_type, payload, retries + 1)
|
|
153
158
|
else:
|
|
154
|
-
raise Exception(f"
|
|
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) < 2:
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
# First byte is message type, second byte is response bit
|
|
166
|
-
msg_type = decrypted[0]
|
|
167
|
-
is_response = decrypted[1]
|
|
168
|
-
body = decrypted[2:]
|
|
169
|
-
|
|
170
|
-
# Find matching pending request
|
|
171
|
-
request_id = (addr[0], addr[1], msg_type)
|
|
172
|
-
|
|
173
|
-
with self.response_lock:
|
|
174
|
-
if request_id in self.pending_responses:
|
|
175
|
-
self.pending_responses[request_id]['data'] = body
|
|
176
|
-
self.pending_responses[request_id]['event'].set()
|
|
159
|
+
raise Exception(f"HTTP request to {endpoint.to_string()} failed: {e}")
|
|
177
160
|
except Exception as e:
|
|
178
|
-
|
|
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}")
|
|
179
166
|
|
|
180
167
|
def send_request_to_node(self, node_id: str, msg_type: int, payload: bytes) -> bytes:
|
|
181
168
|
con = self.connection_from_node(node_id)
|
|
182
|
-
return self.
|
|
169
|
+
return self.send_http_request(con, msg_type, payload)
|
|
183
170
|
|
|
184
171
|
def encrypt_data(self, data: bytes) -> bytes:
|
|
185
172
|
return aes_encrypt(self.get_aes_key(), data)
|
|
@@ -227,7 +214,7 @@ class DSNode:
|
|
|
227
214
|
self.logger.info(f"HELLO => {con.to_string()}")
|
|
228
215
|
|
|
229
216
|
payload = self.my_hello_packet().to_bytes()
|
|
230
|
-
content = self.
|
|
217
|
+
content = self.send_http_request(con, MSG_HELLO, payload)
|
|
231
218
|
|
|
232
219
|
# Get the response packet
|
|
233
220
|
pkt = HelloPacket.from_bytes(content)
|
|
@@ -257,7 +244,7 @@ class DSNode:
|
|
|
257
244
|
|
|
258
245
|
return pkt.node_id
|
|
259
246
|
|
|
260
|
-
def handle_hello(self, data: bytes, detected_address: str = None) -> bytes:
|
|
247
|
+
def handle_hello(self, data: bytes, detected_address: str = None) -> bytes | None:
|
|
261
248
|
pkt = HelloPacket.from_bytes(data)
|
|
262
249
|
self.logger.info(f"Received HELLO from {pkt.node_id}")
|
|
263
250
|
if pkt.version != self.version:
|
|
@@ -269,6 +256,8 @@ class DSNode:
|
|
|
269
256
|
|
|
270
257
|
if pkt.node_id not in self.address_book:
|
|
271
258
|
self.address_book[pkt.node_id] = pkt.connection
|
|
259
|
+
else:
|
|
260
|
+
return None
|
|
272
261
|
|
|
273
262
|
if pkt.node_id not in self.node_states:
|
|
274
263
|
self.node_states[pkt.node_id] = StatePacket(pkt.node_id, 0, b'', { })
|
|
@@ -285,7 +274,7 @@ class DSNode:
|
|
|
285
274
|
self.my_con(),
|
|
286
275
|
self.cred_manager.my_public(),
|
|
287
276
|
None,
|
|
288
|
-
None # No certificate for
|
|
277
|
+
None # No certificate for HTTP
|
|
289
278
|
)
|
|
290
279
|
pkt.sign(self.cred_manager.my_private())
|
|
291
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
|
|
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] = []
|
|
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
|
-
})
|
|
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
|
|
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
|
|
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
|
|
156
|
+
"""Test that unauthorized HTTP requests are rejected"""
|
|
158
157
|
n = spawn_node("node")
|
|
159
158
|
|
|
160
|
-
#
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
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
|
|
356
|
-
"""Test that
|
|
357
|
-
|
|
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
|
-
#
|
|
365
|
-
|
|
359
|
+
# Test that all endpoints exist
|
|
360
|
+
endpoints = ['/hello', '/peers', '/update', '/ping']
|
|
366
361
|
|
|
367
|
-
|
|
368
|
-
|
|
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),
|
|
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,66 +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
|
-
- Response bit (1 byte): 0 for requests, 1 for responses
|
|
23
|
-
- Message payload (variable length)
|
|
24
|
-
|
|
25
|
-
### Security
|
|
26
|
-
- All communication is encrypted using AES with a shared key
|
|
27
|
-
- Messages are signed using ECDSA for authentication
|
|
28
|
-
- UDP packets are encrypted end-to-end
|
|
29
|
-
- HTTPS/TLS is not supported with UDP (certificates are no longer used)
|
|
30
|
-
|
|
31
|
-
### State Synchronization
|
|
32
|
-
- Nodes maintain a copy of all peers' states
|
|
33
|
-
- Updates are broadcast to all connected peers
|
|
34
|
-
- Timestamps prevent older updates from overwriting newer ones
|
|
35
|
-
|
|
36
|
-
### Socket Management
|
|
37
|
-
- You can provide a custom UDP socket when starting a server
|
|
38
|
-
- If no socket is provided, one will be created and bound automatically
|
|
39
|
-
- Socket timeout is set to 2 seconds for requests
|
|
40
|
-
- Maximum UDP packet size is 65507 bytes
|
|
41
|
-
|
|
42
|
-
## Important Notes
|
|
43
|
-
|
|
44
|
-
1. **Shared AES Key**: All nodes in the network must use the same AES key file
|
|
45
|
-
2. **Unique Node IDs**: Each node must have a unique node_id
|
|
46
|
-
3. **Port Availability**: Ensure the specified UDP port is available before starting
|
|
47
|
-
4. **Bootstrap Nodes**: At least one bootstrap node is required to join an existing network
|
|
48
|
-
5. **Network Tick**: The network performs maintenance checks every 3 seconds
|
|
49
|
-
6. **Credential Management**: ECDSA keys are automatically generated and stored in `credentials/` directory
|
|
50
|
-
7. **UDP Reliability**: The protocol implements retry logic (up to 3 attempts) for failed requests
|
|
51
|
-
8. **HTTPS Not Supported**: The `https` configuration option is ignored when using UDP
|
|
52
|
-
|
|
53
|
-
## Error Handling
|
|
54
|
-
|
|
55
|
-
Common error codes (embedded in exception messages):
|
|
56
|
-
- **401**: Not Authorized (signature verification failed)
|
|
57
|
-
- **406**: Not Acceptable (invalid data, stale update, or version mismatch)
|
|
58
|
-
- **505**: Version not supported
|
|
59
|
-
|
|
60
|
-
## Migration from HTTP
|
|
61
|
-
|
|
62
|
-
If you're migrating from the HTTP-based version:
|
|
63
|
-
1. Remove any HTTPS certificate dependencies
|
|
64
|
-
2. Update firewall rules to allow UDP traffic on your ports
|
|
65
|
-
3. The API remains largely the same, but transport is now UDP
|
|
66
|
-
4. The `https` flag in configuration is now ignored
|
|
@@ -1,145 +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.3.0"
|
|
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) < 2:
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
# First byte is message type, second byte is response bit
|
|
60
|
-
msg_type = decrypted[0]
|
|
61
|
-
is_response = decrypted[1]
|
|
62
|
-
body = decrypted[2:]
|
|
63
|
-
|
|
64
|
-
# Check if this is a response (either by bit flag or by pending request)
|
|
65
|
-
request_id = (addr[0], addr[1], msg_type)
|
|
66
|
-
with self.node.response_lock:
|
|
67
|
-
has_pending_request = request_id in self.node.pending_responses
|
|
68
|
-
|
|
69
|
-
if is_response == 1 or has_pending_request:
|
|
70
|
-
# This is a response to our request - route to response handler
|
|
71
|
-
self.node.handle_response(data, addr)
|
|
72
|
-
else:
|
|
73
|
-
# This is a new request - handle it
|
|
74
|
-
response = None
|
|
75
|
-
|
|
76
|
-
if msg_type == MSG_HELLO:
|
|
77
|
-
# Pass the detected IP address to handle_hello
|
|
78
|
-
response = self.node.handle_hello(body, addr[0])
|
|
79
|
-
|
|
80
|
-
elif msg_type == MSG_PEERS:
|
|
81
|
-
response = self.node.handle_peers(body)
|
|
82
|
-
|
|
83
|
-
elif msg_type == MSG_UPDATE:
|
|
84
|
-
response = self.node.handle_update(body)
|
|
85
|
-
|
|
86
|
-
elif msg_type == MSG_PING:
|
|
87
|
-
response = b''
|
|
88
|
-
|
|
89
|
-
# Send response if handler returned data
|
|
90
|
-
if response is not None:
|
|
91
|
-
# Prepend message type and response bit (1 for response) to response
|
|
92
|
-
response_with_type = bytes([msg_type, 1]) + response
|
|
93
|
-
encrypted_response = self.node.encrypt_data(response_with_type)
|
|
94
|
-
self.socket.sendto(encrypted_response, addr)
|
|
95
|
-
|
|
96
|
-
except Exception as e:
|
|
97
|
-
if len(e.args) >= 2:
|
|
98
|
-
self.node.logger.error(f"Error handling packet from {addr}: {e.args[0]} {e.args[1]}")
|
|
99
|
-
else:
|
|
100
|
-
self.node.logger.error(f"Error handling packet from {addr}: {e}")
|
|
101
|
-
|
|
102
|
-
def serve_forever(self):
|
|
103
|
-
"""Main UDP server loop"""
|
|
104
|
-
while self.running:
|
|
105
|
-
try:
|
|
106
|
-
# Receive UDP packet (max 65507 bytes for UDP)
|
|
107
|
-
data, addr = self.socket.recvfrom(65507)
|
|
108
|
-
if self.running:
|
|
109
|
-
# Handle packet in separate thread to avoid blocking
|
|
110
|
-
threading.Thread(target=self.handle_packet, args=(data, addr), daemon=True).start()
|
|
111
|
-
except OSError:
|
|
112
|
-
# Socket was closed
|
|
113
|
-
if not self.running:
|
|
114
|
-
break
|
|
115
|
-
except Exception as e:
|
|
116
|
-
self.node.logger.error(f"Error in server loop: {e}")
|
|
117
|
-
if not self.running:
|
|
118
|
-
break
|
|
119
|
-
|
|
120
|
-
@staticmethod
|
|
121
|
-
def generate_key(out_file_path: str):
|
|
122
|
-
key = generate_aes_key()
|
|
123
|
-
with open(out_file_path, 'wb') as f:
|
|
124
|
-
f.write(key)
|
|
125
|
-
|
|
126
|
-
@staticmethod
|
|
127
|
-
def start(
|
|
128
|
-
config: DSNodeConfig,
|
|
129
|
-
sock: Optional[socket.socket] = None,
|
|
130
|
-
disconnect_callback: Optional[Callable] = None,
|
|
131
|
-
update_callback: Optional[Callable] = None
|
|
132
|
-
) -> 'DSNodeServer':
|
|
133
|
-
n = DSNodeServer(config, sock, disconnect_callback, update_callback)
|
|
134
|
-
n.thread = threading.Thread(target=n.serve_forever, daemon=True)
|
|
135
|
-
n.thread.start()
|
|
136
|
-
|
|
137
|
-
if n.config.bootstrap_nodes is not None and len(n.config.bootstrap_nodes) > 0:
|
|
138
|
-
for bs in n.config.bootstrap_nodes:
|
|
139
|
-
try:
|
|
140
|
-
n.node.bootstrap(bs)
|
|
141
|
-
break # Throws exception if connection is not made
|
|
142
|
-
except Exception as e:
|
|
143
|
-
print(e)
|
|
144
|
-
|
|
145
|
-
return n
|
{distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node-config.md
RENAMED
|
File without changes
|
{distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node-server.md
RENAMED
|
File without changes
|
{distributed_state_network-0.3.0 → distributed_state_network-0.4.0}/documentation/ds-node.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|