distributed-state-network 0.6.7__tar.gz → 1.0.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 (52) hide show
  1. distributed_state_network-1.0.0/.gitignore +3 -0
  2. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/LICENSE +20 -20
  3. distributed_state_network-1.0.0/PKG-INFO +58 -0
  4. distributed_state_network-1.0.0/README.md +36 -0
  5. distributed_state_network-1.0.0/pyproject.toml +37 -0
  6. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/__init__.py +2 -1
  7. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/dsnode.py +134 -57
  8. distributed_state_network-1.0.0/src/distributed_state_network/handler.py +266 -0
  9. distributed_state_network-1.0.0/src/distributed_state_network/network_protocol.py +35 -0
  10. distributed_state_network-1.0.0/src/distributed_state_network/objects/config.py +102 -0
  11. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/data_packet.py +2 -3
  12. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/endpoint.py +8 -3
  13. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/hello_packet.py +18 -11
  14. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/peers_packet.py +6 -4
  15. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/signed_packet.py +9 -6
  16. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/state_packet.py +4 -4
  17. distributed_state_network-1.0.0/src/distributed_state_network/util/aes.py +54 -0
  18. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/byte_helper.py +5 -5
  19. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/ecdsa.py +7 -4
  20. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/key_manager.py +16 -8
  21. distributed_state_network-1.0.0/tests/base.py +63 -0
  22. distributed_state_network-1.0.0/tests/test_aes.py +56 -0
  23. distributed_state_network-1.0.0/tests/test_callbacks.py +58 -0
  24. distributed_state_network-1.0.0/tests/test_configuration.py +65 -0
  25. distributed_state_network-1.0.0/tests/test_connectivity.py +76 -0
  26. distributed_state_network-1.0.0/tests/test_disconnect.py +80 -0
  27. distributed_state_network-1.0.0/tests/test_error_handling.py +104 -0
  28. distributed_state_network-1.0.0/tests/test_http_endpoints.py +34 -0
  29. distributed_state_network-1.0.0/tests/test_security.py +201 -0
  30. distributed_state_network-1.0.0/tests/test_state_data.py +67 -0
  31. distributed_state_network-1.0.0/tests/test_wrapper_methods.py +111 -0
  32. distributed_state_network-0.6.7/.github/workflows/publish.yml +0 -35
  33. distributed_state_network-0.6.7/.gitignore +0 -12
  34. distributed_state_network-0.6.7/PKG-INFO +0 -117
  35. distributed_state_network-0.6.7/README.md +0 -91
  36. distributed_state_network-0.6.7/coverage.sh +0 -3
  37. distributed_state_network-0.6.7/documentation/ds-node-config.md +0 -59
  38. distributed_state_network-0.6.7/documentation/ds-node-server.md +0 -190
  39. distributed_state_network-0.6.7/documentation/ds-node.md +0 -65
  40. distributed_state_network-0.6.7/documentation/protocol.md +0 -114
  41. distributed_state_network-0.6.7/documentation/usage.md +0 -153
  42. distributed_state_network-0.6.7/pyproject.toml +0 -38
  43. distributed_state_network-0.6.7/src/distributed_state_network/create_key.py +0 -4
  44. distributed_state_network-0.6.7/src/distributed_state_network/handler.py +0 -195
  45. distributed_state_network-0.6.7/src/distributed_state_network/objects/config.py +0 -24
  46. distributed_state_network-0.6.7/src/distributed_state_network/util/aes.py +0 -45
  47. distributed_state_network-0.6.7/src/distributed_state_network/util/https.py +0 -51
  48. distributed_state_network-0.6.7/technical.md +0 -94
  49. distributed_state_network-0.6.7/tests/connections.py +0 -117
  50. distributed_state_network-0.6.7/tests/test.py +0 -557
  51. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/msg_types.py +0 -0
  52. {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/__init__.py +0 -0
@@ -0,0 +1,3 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Colton Clemmer
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Colton Clemmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: distributed-state-network
3
+ Version: 1.0.0
4
+ Summary: Peer-to-peer encrypted state-sharing network over HTTP
5
+ Project-URL: Homepage, https://github.com/erinclemmer/language-pipes
6
+ Project-URL: Issues, https://github.com/erinclemmer/language-pipes/issues
7
+ Author-email: Erin Clemmer <erin.c.clemmer@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: distributed,encryption,networking,peer-to-peer,state
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Topic :: System :: Networking
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: cryptography
19
+ Requires-Dist: ecdsa
20
+ Requires-Dist: requests
21
+ Description-Content-Type: text/markdown
22
+
23
+ # distributed-state-network
24
+
25
+ A peer-to-peer network of nodes that share replicated state over HTTP, with
26
+ per-message AES encryption and ECDSA-signed packets. Each node gossips its
27
+ state to peers; the network self-organizes from a set of bootstrap nodes.
28
+
29
+ This is the networking layer used by
30
+ [language-pipes](https://github.com/erinclemmer/language-pipes), but has no
31
+ dependency on it and can be used on its own.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install distributed-state-network
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from distributed_state_network import DSNodeServer, DSNodeConfig
43
+
44
+ key = DSNodeServer.generate_key()
45
+ server = DSNodeServer.start(DSNodeConfig(
46
+ node_id="node-a",
47
+ network_ip="127.0.0.1",
48
+ port=8000,
49
+ aes_key=key,
50
+ ))
51
+ ```
52
+
53
+ See `src/distributed_state_network/` for the packet types, key management, and
54
+ node handler implementation.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,36 @@
1
+ # distributed-state-network
2
+
3
+ A peer-to-peer network of nodes that share replicated state over HTTP, with
4
+ per-message AES encryption and ECDSA-signed packets. Each node gossips its
5
+ state to peers; the network self-organizes from a set of bootstrap nodes.
6
+
7
+ This is the networking layer used by
8
+ [language-pipes](https://github.com/erinclemmer/language-pipes), but has no
9
+ dependency on it and can be used on its own.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install distributed-state-network
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from distributed_state_network import DSNodeServer, DSNodeConfig
21
+
22
+ key = DSNodeServer.generate_key()
23
+ server = DSNodeServer.start(DSNodeConfig(
24
+ node_id="node-a",
25
+ network_ip="127.0.0.1",
26
+ port=8000,
27
+ aes_key=key,
28
+ ))
29
+ ```
30
+
31
+ See `src/distributed_state_network/` for the packet types, key management, and
32
+ node handler implementation.
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "distributed-state-network"
7
+ version = "1.0.0"
8
+ authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
+ description = "Peer-to-peer encrypted state-sharing network over HTTP"
10
+ keywords = ["distributed", "networking", "peer-to-peer", "state", "encryption"]
11
+ license = "MIT"
12
+ readme = "README.md"
13
+ license-files = ["LICENSE"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3.10",
16
+ "Development Status :: 4 - Beta",
17
+ "Operating System :: OS Independent",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Topic :: System :: Networking",
20
+ "Typing :: Typed"
21
+ ]
22
+ requires-python = ">=3.10"
23
+ dependencies = [
24
+ "requests",
25
+ "ecdsa",
26
+ "cryptography"
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/erinclemmer/language-pipes"
31
+ Issues = "https://github.com/erinclemmer/language-pipes/issues"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/distributed_state_network"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
@@ -1,4 +1,5 @@
1
1
  from distributed_state_network.handler import DSNodeServer
2
2
  from distributed_state_network.dsnode import DSNode
3
3
  from distributed_state_network.objects.config import DSNodeConfig
4
- from distributed_state_network.objects.endpoint import Endpoint
4
+ from distributed_state_network.objects.endpoint import Endpoint
5
+ from distributed_state_network.network_protocol import StateNetworkNode
@@ -1,10 +1,9 @@
1
- import os
2
1
  import time
3
2
  import random
4
3
  import logging
5
4
  import threading
6
5
  import requests
7
- from typing import Dict, List, Optional, Callable
6
+ from typing import Dict, List, Optional, Callable, Tuple
8
7
 
9
8
  from distributed_state_network.objects.endpoint import Endpoint
10
9
  from distributed_state_network.objects.hello_packet import HelloPacket
@@ -15,11 +14,13 @@ from distributed_state_network.objects.config import DSNodeConfig
15
14
 
16
15
  from distributed_state_network.util import get_dict_hash
17
16
  from distributed_state_network.util.key_manager import CredentialManager
18
- from distributed_state_network.util.aes import aes_encrypt, aes_decrypt
17
+ from distributed_state_network.util.aes import aes_encrypt, aes_decrypt, AES_KEY_LENGTH
19
18
 
20
19
  TICK_INTERVAL = 3
21
20
  HTTP_TIMEOUT = 2 # seconds
22
21
 
22
+ MIN_TRANSFER_BYTES_PER_SEC = 1024 * 1024 # 1 MB/s floor
23
+
23
24
  # Message type constants (must match handler.py)
24
25
  MSG_HELLO = 1
25
26
  MSG_PEERS = 2
@@ -41,18 +42,24 @@ class DSNode:
41
42
  config: DSNodeConfig
42
43
  address_book: Dict[str, Endpoint]
43
44
  node_states: Dict[str, StatePacket]
44
- shutting_down: bool = False
45
+ shutting_down: bool
46
+ logs: List[Tuple[float, str]]
47
+ create_alert: Callable[[str], None]
45
48
 
46
49
  def __init__(
47
50
  self,
48
51
  config: DSNodeConfig,
49
52
  version: str,
53
+ create_alert: Callable[[str], None],
50
54
  disconnect_callback: Optional[Callable] = None,
51
55
  update_callback: Optional[Callable] = None,
52
56
  receive_callback: Optional[Callable] = None
53
57
  ):
54
58
  self.config = config
55
59
  self.version = version
60
+ self.shutting_down = False
61
+ self.create_alert = create_alert
62
+ self.logs = []
56
63
 
57
64
  self.cred_manager = CredentialManager(config.credential_dir, config.node_id)
58
65
  self.cred_manager.generate_keys()
@@ -65,31 +72,83 @@ class DSNode:
65
72
  self.config.node_id: Endpoint(self.config.network_ip, config.port)
66
73
  }
67
74
 
68
- self.logger = logging.getLogger("DSN: " + config.node_id)
75
+ self.logger = logging.getLogger(__name__)
69
76
  self.disconnect_cb = disconnect_callback
70
77
  self.update_cb = update_callback
71
78
  self.receive_cb = receive_callback
79
+
80
+ # Validate configured AES key eagerly so bad/legacy key formats fail fast.
81
+ self.get_aes_key()
82
+ self.add_log(f"Starting network server on port {config.port}")
72
83
 
73
84
  threading.Thread(target=self.network_tick, daemon=True).start()
74
85
 
75
- def get_aes_key(self) -> bytes:
76
- return bytes.fromhex(self.config.aes_key)
86
+ def add_log(self, msg: str, level: str = "INFO"):
87
+ if level.upper() == "INFO":
88
+ self.logger.info(msg)
89
+ elif level.upper() == "ERROR":
90
+ self.logger.error(msg)
91
+ elif level.upper() == "DEBUG":
92
+ self.logger.debug(msg)
93
+ elif level.upper() == "WARNING":
94
+ self.logger.warning(msg)
95
+ else:
96
+ self.logger.info(msg)
97
+
98
+ self.logs.append((time.time(), msg))
99
+
100
+ def get_aes_key(self) -> Optional[bytes]:
101
+ if self.config.aes_key is None:
102
+ return None
103
+ key = bytes.fromhex(self.config.aes_key)
104
+ if len(key) != AES_KEY_LENGTH:
105
+ raise ValueError(
106
+ f"Invalid AES key length ({len(key)} bytes). Expected {AES_KEY_LENGTH} bytes."
107
+ )
108
+ return key
109
+
110
+ def _is_ip_whitelisted(self, ip: Optional[str]) -> bool:
111
+ if len(self.config.whitelist_ips) == 0:
112
+ return True
113
+ if ip is None:
114
+ return False
115
+ return ip in self.config.whitelist_ips
116
+
117
+ def ensure_ip_allowed(self, ip: Optional[str]):
118
+ if not self._is_ip_whitelisted(ip):
119
+ raise Exception(401, f"IP address '{ip}' is not in whitelist")
120
+
121
+ def _is_node_id_whitelisted(self, node_id: Optional[str]) -> bool:
122
+ if len(self.config.whitelist_node_ids) == 0:
123
+ return True
124
+ if node_id is None:
125
+ return False
126
+ if node_id == self.config.node_id:
127
+ return True
128
+ return node_id in self.config.whitelist_node_ids
129
+
130
+ def ensure_node_id_allowed(self, node_id: Optional[str]):
131
+ if not self._is_node_id_whitelisted(node_id):
132
+ raise Exception(401, f"{node_id} not in {self.config.node_id}'s whitelist")
133
+
134
+ def ensure_endpoint_allowed(self, endpoint: Endpoint):
135
+ self.ensure_ip_allowed(endpoint.address)
77
136
 
78
137
  def write_address_book(self, node_id: str, conn: Endpoint):
79
- if conn.address == "127.0.0.1" and conn.port == self.config.port:
80
- raise Exception("Trying to write address book for self")
81
- self.logger.info(f"Address set: {node_id} -> {conn.address}:{conn.port}")
138
+ if node_id != self.config.node_id:
139
+ self.ensure_node_id_allowed(node_id)
140
+ self.ensure_endpoint_allowed(conn)
82
141
  self.address_book[node_id] = conn
83
142
 
84
143
  def network_tick(self):
85
- time.sleep(TICK_INTERVAL)
86
- if self.shutting_down:
87
- self.logger.info("Shutting down node")
88
- return
89
- self.test_connections()
90
- self.gossip()
91
- threading.Thread(target=self.network_tick, daemon=True).start()
92
-
144
+ while True:
145
+ time.sleep(TICK_INTERVAL)
146
+ if self.shutting_down:
147
+ self.add_log("Shutting down node", "INFO")
148
+ return
149
+ self.test_connections()
150
+ self.gossip()
151
+
93
152
  def gossip(self):
94
153
  if len(self.address_book.keys()) == 0:
95
154
  return
@@ -105,7 +164,6 @@ class DSNode:
105
164
  del self.node_states[node_id]
106
165
  if node_id in self.address_book:
107
166
  del self.address_book[node_id]
108
- self.logger.info(f"PING failed for {node_id}, disconnecting...")
109
167
 
110
168
  for node_id in self.node_states.copy().keys():
111
169
  if node_id not in self.node_states or node_id == self.config.node_id:
@@ -115,14 +173,16 @@ class DSNode:
115
173
  return
116
174
  self.send_ping(node_id)
117
175
  except Exception:
118
- self.logger.error(f"PING Failure for {node_id}")
119
176
  if node_id in self.node_states: # double check if something has changed since the ping request started
120
177
  remove(node_id)
178
+ msg = f"{node_id} has disconnected"
179
+ self.add_log(msg)
121
180
  if self.disconnect_cb is not None:
122
181
  self.disconnect_cb()
123
182
 
124
183
  def send_http_request(self, endpoint: Endpoint, msg_type: int, payload: bytes, retries: int = 0) -> bytes:
125
184
  """Send HTTP request and wait for response"""
185
+ self.ensure_endpoint_allowed(endpoint)
126
186
  try:
127
187
  # Prepend message type to payload
128
188
  data = bytes([msg_type]) + payload
@@ -133,12 +193,15 @@ class DSNode:
133
193
  path = MSG_TYPE_TO_PATH.get(msg_type, '/unknown')
134
194
  url = f"http://{endpoint.address}:{endpoint.port}{path}"
135
195
 
196
+ # Scale timeout to transfer size
197
+ timeout = max(HTTP_TIMEOUT, len(data) / MIN_TRANSFER_BYTES_PER_SEC)
198
+
136
199
  # Send HTTP POST request
137
200
  response = requests.post(
138
201
  url,
139
202
  data=data,
140
203
  headers={'Content-Type': 'application/octet-stream'},
141
- timeout=HTTP_TIMEOUT
204
+ timeout=timeout
142
205
  )
143
206
 
144
207
  # Check response status
@@ -146,7 +209,7 @@ class DSNode:
146
209
  # No content - valid for some responses like successful HELLO with no data
147
210
  return b''
148
211
  elif response.status_code != 200:
149
- raise Exception(f"HTTP error: {response.status_code}")
212
+ raise Exception(response.status_code, response.content.decode())
150
213
 
151
214
  response_data = response.content
152
215
  # Decrypt the response
@@ -170,28 +233,29 @@ class DSNode:
170
233
  return self.send_http_request(endpoint, msg_type, payload, retries + 1)
171
234
  else:
172
235
  raise Exception(f"HTTP request to {endpoint.to_string()} timed out")
173
- except requests.exceptions.RequestException as e:
236
+ except requests.exceptions.RequestException:
174
237
  if retries < 2:
175
238
  time.sleep(0.5)
176
239
  return self.send_http_request(endpoint, msg_type, payload, retries + 1)
177
240
  else:
178
- raise Exception(f"HTTP request to {endpoint.to_string()} failed: {e}")
179
- except Exception as e:
180
- if retries < 2:
181
- time.sleep(0.5)
182
- return self.send_http_request(endpoint, msg_type, payload, retries + 1)
183
- else:
184
- raise Exception(f"HTTP request to {endpoint.to_string()} failed: {e}")
241
+ raise Exception(f"HTTP request to {endpoint.to_string()} failed")
185
242
 
186
243
  def send_request_to_node(self, node_id: str, msg_type: int, payload: bytes) -> bytes:
244
+ self.ensure_node_id_allowed(node_id)
187
245
  con = self.connection_from_node(node_id)
188
246
  return self.send_http_request(con, msg_type, payload)
189
247
 
190
248
  def encrypt_data(self, data: bytes) -> bytes:
191
- return aes_encrypt(self.get_aes_key(), data)
249
+ key = self.get_aes_key()
250
+ if key is None:
251
+ return data
252
+ return aes_encrypt(key, data)
192
253
 
193
254
  def decrypt_data(self, data: bytes) -> bytes:
194
- return aes_decrypt(self.get_aes_key(), data)
255
+ key = self.get_aes_key()
256
+ if key is None:
257
+ return data
258
+ return aes_decrypt(key, data)
195
259
 
196
260
  def request_peers(self, node_id: str):
197
261
  pkt = PeersPacket(self.config.node_id, None, { })
@@ -215,6 +279,7 @@ class DSNode:
215
279
 
216
280
  def handle_peers(self, data: bytes):
217
281
  pkt = PeersPacket.from_bytes(data)
282
+ self.ensure_node_id_allowed(pkt.node_id)
218
283
  if pkt.node_id not in self.address_book:
219
284
  raise Exception(401, f"Could not find {pkt.node_id} in address book") # Not Authorized
220
285
 
@@ -232,20 +297,30 @@ class DSNode:
232
297
  return pkt.to_bytes()
233
298
 
234
299
  def send_hello(self, con: Endpoint):
235
- self.logger.info(f"HELLO => {con.to_string()}")
236
-
237
300
  pkt = self.my_hello_packet()
238
301
  payload = pkt.to_bytes()
239
- content = self.send_http_request(con, MSG_HELLO, payload)
240
-
302
+ try:
303
+ content = self.send_http_request(con, MSG_HELLO, payload)
304
+ except Exception as e:
305
+ if e.args[0] == 505 or e.args[0] == 401:
306
+ msg = f"Network Error: {e.args[1]}"
307
+ self.add_log(msg)
308
+ elif "HTTP request to" in e.args[0]:
309
+ msg = f"Connection to {con.address}:{con.port} failed"
310
+ self.logs.append((time.time(), msg))
311
+ if len(e.args) > 1:
312
+ code, msg = e.args
313
+ if msg == "Node ID not in whitelist":
314
+ self.add_log(f"Error from {con.address}:{con.port}: Not in their whitelist")
315
+ raise e
316
+
241
317
  # Get the response packet
242
318
  pkt = HelloPacket.from_bytes(content)
243
- self.logger.info(f"Received HELLO from {pkt.node_id}")
244
-
319
+
245
320
  # Verify version compatibility
246
321
  if pkt.version != self.version:
247
- msg = f"HELLO => {pkt.node_id} (Version mismatch \"{pkt.version}\" != \"{self.version}\")"
248
- self.logger.error(msg)
322
+ msg = f"Network version mismatch \"{pkt.version}\" ({pkt.node_id}) != \"{self.version}\" ({self.config.node_id})"
323
+ self.add_log(msg, "ERROR")
249
324
  raise Exception(505) # Version not supported
250
325
 
251
326
  # Store the peer's public key
@@ -253,9 +328,8 @@ class DSNode:
253
328
 
254
329
  # If the server sent us our detected IP, update our address book
255
330
  if pkt.detected_address:
256
- self.logger.info(f"Server detected our IP as: {pkt.detected_address}")
257
331
  # Update our own connection in the address book with the detected IP
258
- self.write_address_book(self.config.node_id, pkt.detected_address, self.config.port)
332
+ self.write_address_book(self.config.node_id, Endpoint(pkt.detected_address, self.config.port))
259
333
 
260
334
  self.write_address_book(pkt.node_id, con)
261
335
 
@@ -268,15 +342,19 @@ class DSNode:
268
342
  self.node_states[node_id] = StatePacket(node_id, 0, b'', { })
269
343
 
270
344
  def handle_hello(self, data: bytes, detected_address: str) -> bytes:
345
+ self.ensure_ip_allowed(detected_address)
271
346
  pkt = HelloPacket.from_bytes(data)
272
- self.logger.info(f"Received HELLO from {pkt.node_id}")
347
+ self.ensure_node_id_allowed(pkt.node_id)
273
348
  if pkt.version != self.version:
274
- msg = f"HELLO => {pkt.node_id} (Version mismatch \"{pkt.version}\" != \"{self.version}\")"
275
- self.logger.error(msg)
276
- raise Exception(505) # Version not supported
349
+ msg = f"Network version mismatch \"{pkt.version}\" ({pkt.node_id}) != \"{self.version}\" ({self.config.node_id})"
350
+ self.add_log(msg, "ERROR")
351
+ self.create_alert(msg)
352
+ raise Exception(505, msg) # Version not supported
277
353
 
278
354
  self.cred_manager.ensure_public(pkt.node_id, pkt.ecdsa_public_key)
279
355
  self.write_address_book(pkt.node_id, Endpoint(detected_address, pkt.connection.port))
356
+ if pkt.node_id != self.config.node_id:
357
+ self.add_log(f"{pkt.node_id} has connnected")
280
358
 
281
359
  if pkt.node_id not in self.node_states:
282
360
  self.init_state(pkt.node_id)
@@ -292,7 +370,7 @@ class DSNode:
292
370
  self.config.node_id,
293
371
  self.my_con(),
294
372
  self.cred_manager.my_public(),
295
- None,
373
+ b'',
296
374
  None # No certificate for HTTP
297
375
  )
298
376
  pkt.sign(self.cred_manager.my_private())
@@ -300,31 +378,30 @@ class DSNode:
300
378
 
301
379
  def send_ping(self, node_id: str):
302
380
  try:
303
- conn = self.connection_from_node(node_id)
304
- self.logger.debug(f"PING => {node_id} ({conn.address}:{conn.port})")
305
381
  self.send_request_to_node(node_id, MSG_PING, b' ')
306
382
  except Exception as e:
307
383
  raise Exception(f'PING => {node_id}: {e}')
308
384
 
309
385
  def send_update(self, node_id: str):
310
- self.logger.debug(f"UPDATE => {node_id}")
311
- content = self.send_request_to_node(node_id, MSG_UPDATE, self.my_state().to_bytes())
386
+ try:
387
+ content = self.send_request_to_node(node_id, MSG_UPDATE, self.my_state().to_bytes())
388
+ except Exception:
389
+ return b''
312
390
  return content
313
391
 
314
392
  def handle_update(self, data: bytes):
315
393
  pkt = StatePacket.from_bytes(data)
394
+ self.ensure_node_id_allowed(pkt.node_id)
316
395
 
317
396
  if not self.update_state(pkt):
318
397
  return b''
319
398
 
320
- self.logger.info(f"Received UPDATE from {pkt.node_id}")
321
-
322
399
  if self.update_cb is not None:
323
400
  try:
324
401
  self.update_cb()
325
402
  except Exception as e:
326
- self.logger.error("Update Error Captured:")
327
- self.logger.error(str(e))
403
+ self.add_log("Update Error Captured:", "ERROR")
404
+ self.add_log(str(e), "ERROR")
328
405
 
329
406
  return self.my_state().to_bytes()
330
407
 
@@ -395,7 +472,7 @@ class DSNode:
395
472
 
396
473
  def receive_data(self, data: bytes):
397
474
  pkt = DataPacket.from_bytes(data)
398
- self.logger.info(f"Received DATA from {pkt.node_id} ({len(pkt.data)} bytes)")
475
+ self.ensure_node_id_allowed(pkt.node_id)
399
476
 
400
477
  # Ensure sender is known
401
478
  if pkt.node_id not in self.address_book:
@@ -407,7 +484,7 @@ class DSNode:
407
484
 
408
485
  if self.receive_cb is not None:
409
486
  try:
410
- self.receive_cb(pkt.data)
487
+ self.receive_cb(pkt.node_id, pkt.data)
411
488
  except Exception as e:
412
489
  print(e)
413
490