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.
- distributed_state_network-1.0.0/.gitignore +3 -0
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/LICENSE +20 -20
- distributed_state_network-1.0.0/PKG-INFO +58 -0
- distributed_state_network-1.0.0/README.md +36 -0
- distributed_state_network-1.0.0/pyproject.toml +37 -0
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/__init__.py +2 -1
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/dsnode.py +134 -57
- distributed_state_network-1.0.0/src/distributed_state_network/handler.py +266 -0
- distributed_state_network-1.0.0/src/distributed_state_network/network_protocol.py +35 -0
- distributed_state_network-1.0.0/src/distributed_state_network/objects/config.py +102 -0
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/data_packet.py +2 -3
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/endpoint.py +8 -3
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/hello_packet.py +18 -11
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/peers_packet.py +6 -4
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/signed_packet.py +9 -6
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/state_packet.py +4 -4
- distributed_state_network-1.0.0/src/distributed_state_network/util/aes.py +54 -0
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/byte_helper.py +5 -5
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/ecdsa.py +7 -4
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/key_manager.py +16 -8
- distributed_state_network-1.0.0/tests/base.py +63 -0
- distributed_state_network-1.0.0/tests/test_aes.py +56 -0
- distributed_state_network-1.0.0/tests/test_callbacks.py +58 -0
- distributed_state_network-1.0.0/tests/test_configuration.py +65 -0
- distributed_state_network-1.0.0/tests/test_connectivity.py +76 -0
- distributed_state_network-1.0.0/tests/test_disconnect.py +80 -0
- distributed_state_network-1.0.0/tests/test_error_handling.py +104 -0
- distributed_state_network-1.0.0/tests/test_http_endpoints.py +34 -0
- distributed_state_network-1.0.0/tests/test_security.py +201 -0
- distributed_state_network-1.0.0/tests/test_state_data.py +67 -0
- distributed_state_network-1.0.0/tests/test_wrapper_methods.py +111 -0
- distributed_state_network-0.6.7/.github/workflows/publish.yml +0 -35
- distributed_state_network-0.6.7/.gitignore +0 -12
- distributed_state_network-0.6.7/PKG-INFO +0 -117
- distributed_state_network-0.6.7/README.md +0 -91
- distributed_state_network-0.6.7/coverage.sh +0 -3
- distributed_state_network-0.6.7/documentation/ds-node-config.md +0 -59
- distributed_state_network-0.6.7/documentation/ds-node-server.md +0 -190
- distributed_state_network-0.6.7/documentation/ds-node.md +0 -65
- distributed_state_network-0.6.7/documentation/protocol.md +0 -114
- distributed_state_network-0.6.7/documentation/usage.md +0 -153
- distributed_state_network-0.6.7/pyproject.toml +0 -38
- distributed_state_network-0.6.7/src/distributed_state_network/create_key.py +0 -4
- distributed_state_network-0.6.7/src/distributed_state_network/handler.py +0 -195
- distributed_state_network-0.6.7/src/distributed_state_network/objects/config.py +0 -24
- distributed_state_network-0.6.7/src/distributed_state_network/util/aes.py +0 -45
- distributed_state_network-0.6.7/src/distributed_state_network/util/https.py +0 -51
- distributed_state_network-0.6.7/technical.md +0 -94
- distributed_state_network-0.6.7/tests/connections.py +0 -117
- distributed_state_network-0.6.7/tests/test.py +0 -557
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/objects/msg_types.py +0 -0
- {distributed_state_network-0.6.7 → distributed_state_network-1.0.0}/src/distributed_state_network/util/__init__.py +0 -0
|
@@ -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
|
|
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(
|
|
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
|
|
76
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
self.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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=
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
319
|
+
|
|
245
320
|
# Verify version compatibility
|
|
246
321
|
if pkt.version != self.version:
|
|
247
|
-
msg = f"
|
|
248
|
-
self.
|
|
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.
|
|
347
|
+
self.ensure_node_id_allowed(pkt.node_id)
|
|
273
348
|
if pkt.version != self.version:
|
|
274
|
-
msg = f"
|
|
275
|
-
self.
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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.
|
|
327
|
-
self.
|
|
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.
|
|
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
|
|