fognode 0.2.2__tar.gz → 0.2.3__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.
- {fognode-0.2.2 → fognode-0.2.3}/PKG-INFO +43 -39
- {fognode-0.2.2 → fognode-0.2.3}/README.md +42 -38
- fognode-0.2.3/examples/headless_server.py +21 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/__init__.py +15 -2
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/app.py +87 -98
- fognode-0.2.3/src/fognode/core/events.py +56 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/server.py +10 -9
- fognode-0.2.2/examples/headless_server.py +0 -14
- {fognode-0.2.2 → fognode-0.2.3}/.github/CODEOWNERS +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/PULL_REQUEST_TEMPLATE/optimization.md +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/dependabot.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/build.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/pr-summary.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/pr.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/.gitignore +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/AGENTS.md +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/CHANGELOG.md +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/CONTRIBUTING.md +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/LICENSE +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/pyproject.toml +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/__main__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/auth/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/auth/handshake.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/aesgcm.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/blake2.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/blowfish.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/chacha20.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/ed25519.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/fernet.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/hkdf.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/hmac.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/pbkdf2.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/rsa.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/scrypt.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/sha3.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/x25519.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/cli/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/cli/entrypoint.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/client.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/probe.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/cert.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/channel.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/kdf.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/kx.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/password.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/primitives.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/decorators.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/exceptions.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/base.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/command.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/text.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/handlers/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/handlers/handler.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/constants.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/exceptions.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/protocol.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/ipwords.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/net.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/ratelimit.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/wire/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/src/fognode/wire/framing.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/conftest.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/test_crypto.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/test_framing.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/test_ipwords.py +0 -0
- {fognode-0.2.2 → fognode-0.2.3}/tests/test_ratelimit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fognode
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: fognode - headless secure encrypted data transmission. TLS + X25519 + AESGCM + PBKDF2.
|
|
5
5
|
Project-URL: Repository, https://github.com/reekeer/fognode
|
|
6
6
|
Author: reekeer
|
|
@@ -63,82 +63,86 @@ Stack: TLS 1.2+ · X25519 · AESGCM-256 · HMAC-SHA256 · PBKDF2 · HKDF
|
|
|
63
63
|
pip install fognode
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
## Quick start
|
|
66
|
+
## Quick start
|
|
67
67
|
|
|
68
68
|
### Server
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
|
-
from fognode import
|
|
71
|
+
from fognode import Server, MessageEvent
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
host="0.0.0.0",
|
|
75
|
-
port=9443,
|
|
76
|
-
user="alice",
|
|
77
|
-
password="secret",
|
|
78
|
-
cipher=Cipher.AESGCM,
|
|
79
|
-
)
|
|
73
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
80
74
|
|
|
81
|
-
@
|
|
75
|
+
@server.on_event(MessageEvent)
|
|
82
76
|
async def echo(ctx):
|
|
83
|
-
|
|
77
|
+
if ctx.event.text:
|
|
78
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
84
79
|
|
|
85
|
-
@
|
|
86
|
-
async def ping(ctx):
|
|
87
|
-
await ctx.answer("pong")
|
|
88
|
-
|
|
89
|
-
@app.on_connect()
|
|
80
|
+
@server.on_event(ConnectEvent)
|
|
90
81
|
async def on_connect(ctx):
|
|
91
|
-
print(
|
|
82
|
+
print("+ peer connected")
|
|
83
|
+
|
|
84
|
+
@server.on_event(DisconnectEvent)
|
|
85
|
+
async def on_disconnect(ctx):
|
|
86
|
+
print("- peer disconnected")
|
|
92
87
|
|
|
93
88
|
if __name__ == "__main__":
|
|
94
|
-
|
|
89
|
+
server.run()
|
|
95
90
|
```
|
|
96
91
|
|
|
97
92
|
### Client
|
|
98
93
|
|
|
99
94
|
```python
|
|
100
|
-
from fognode import
|
|
95
|
+
from fognode import Client, MessageEvent, ClosedEvent
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
connect_string="alice@oak-pine-stone-field:9443",
|
|
104
|
-
password="secret",
|
|
105
|
-
cipher=Cipher.AESGCM,
|
|
106
|
-
)
|
|
97
|
+
client = Client(connect_string="oak-pine-stone-field:9443", password="secret")
|
|
107
98
|
|
|
108
|
-
@
|
|
99
|
+
@client.on_event(MessageEvent)
|
|
109
100
|
async def on_message(ctx):
|
|
110
|
-
print(f"
|
|
101
|
+
print(f"msg: {ctx.event.text}")
|
|
102
|
+
|
|
103
|
+
@client.on_event(ClosedEvent)
|
|
104
|
+
async def on_closed(ctx):
|
|
105
|
+
print("connection closed")
|
|
111
106
|
|
|
112
107
|
if __name__ == "__main__":
|
|
113
|
-
|
|
108
|
+
client.connect()
|
|
114
109
|
```
|
|
115
110
|
|
|
111
|
+
## Events
|
|
112
|
+
|
|
113
|
+
| Event | Server | Client | Description |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| `StartEvent` | ✅ | ✅ | Server/client started |
|
|
116
|
+
| `ConnectEvent` | ✅ | ✅ | Peer connected |
|
|
117
|
+
| `DisconnectEvent` | ✅ | ✅ | Peer disconnected |
|
|
118
|
+
| `MessageEvent` | ✅ | ✅ | Message received |
|
|
119
|
+
| `ClosedEvent` | ❌ | ✅ | Connection closed |
|
|
120
|
+
| `ErrorEvent` | ❌ | ✅ | Error occurred |
|
|
121
|
+
|
|
116
122
|
## Classic API
|
|
117
123
|
|
|
118
124
|
```python
|
|
119
125
|
from fognode import start_server, client_connect
|
|
120
126
|
|
|
121
|
-
ip, code, fp = start_server("0.0.0.0", 9443, "
|
|
122
|
-
print(f"Connect:
|
|
127
|
+
ip, code, fp = start_server("0.0.0.0", 9443, "secret")
|
|
128
|
+
print(f"Connect: {code}:9443")
|
|
123
129
|
```
|
|
124
130
|
|
|
125
131
|
## Structure
|
|
126
132
|
|
|
127
133
|
```
|
|
128
134
|
src/fognode/
|
|
129
|
-
├── app.py #
|
|
130
|
-
├──
|
|
131
|
-
├──
|
|
132
|
-
├──
|
|
133
|
-
├──
|
|
134
|
-
|
|
135
|
-
├── handlers/ # HandlerObject
|
|
136
|
-
├── types/ # exceptions, constants, protocol
|
|
135
|
+
├── app.py # Server, Client, Context
|
|
136
|
+
├── core/
|
|
137
|
+
│ ├── events.py # Event classes
|
|
138
|
+
│ ├── server.py # start_server()
|
|
139
|
+
│ ├── client.py # client_connect()
|
|
140
|
+
│ └── probe.py # probe_server()
|
|
137
141
|
├── crypto/ # primitives, kdf, cert, channel
|
|
138
142
|
├── ciphers/ # aesgcm, chacha20, x25519, hkdf, pbkdf2, hmac
|
|
139
143
|
├── wire/ # framing
|
|
140
144
|
├── auth/ # handshake
|
|
141
|
-
├──
|
|
145
|
+
├── types/ # exceptions, constants, protocol
|
|
142
146
|
├── decorators.py # retry, rate_limited, timed
|
|
143
147
|
├── exceptions.py # errors
|
|
144
148
|
├── utils/ # ipwords, ratelimit, net
|
|
@@ -25,82 +25,86 @@ Stack: TLS 1.2+ · X25519 · AESGCM-256 · HMAC-SHA256 · PBKDF2 · HKDF
|
|
|
25
25
|
pip install fognode
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
## Quick start
|
|
28
|
+
## Quick start
|
|
29
29
|
|
|
30
30
|
### Server
|
|
31
31
|
|
|
32
32
|
```python
|
|
33
|
-
from fognode import
|
|
33
|
+
from fognode import Server, MessageEvent
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
host="0.0.0.0",
|
|
37
|
-
port=9443,
|
|
38
|
-
user="alice",
|
|
39
|
-
password="secret",
|
|
40
|
-
cipher=Cipher.AESGCM,
|
|
41
|
-
)
|
|
35
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
42
36
|
|
|
43
|
-
@
|
|
37
|
+
@server.on_event(MessageEvent)
|
|
44
38
|
async def echo(ctx):
|
|
45
|
-
|
|
39
|
+
if ctx.event.text:
|
|
40
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
46
41
|
|
|
47
|
-
@
|
|
48
|
-
async def ping(ctx):
|
|
49
|
-
await ctx.answer("pong")
|
|
50
|
-
|
|
51
|
-
@app.on_connect()
|
|
42
|
+
@server.on_event(ConnectEvent)
|
|
52
43
|
async def on_connect(ctx):
|
|
53
|
-
print(
|
|
44
|
+
print("+ peer connected")
|
|
45
|
+
|
|
46
|
+
@server.on_event(DisconnectEvent)
|
|
47
|
+
async def on_disconnect(ctx):
|
|
48
|
+
print("- peer disconnected")
|
|
54
49
|
|
|
55
50
|
if __name__ == "__main__":
|
|
56
|
-
|
|
51
|
+
server.run()
|
|
57
52
|
```
|
|
58
53
|
|
|
59
54
|
### Client
|
|
60
55
|
|
|
61
56
|
```python
|
|
62
|
-
from fognode import
|
|
57
|
+
from fognode import Client, MessageEvent, ClosedEvent
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
connect_string="alice@oak-pine-stone-field:9443",
|
|
66
|
-
password="secret",
|
|
67
|
-
cipher=Cipher.AESGCM,
|
|
68
|
-
)
|
|
59
|
+
client = Client(connect_string="oak-pine-stone-field:9443", password="secret")
|
|
69
60
|
|
|
70
|
-
@
|
|
61
|
+
@client.on_event(MessageEvent)
|
|
71
62
|
async def on_message(ctx):
|
|
72
|
-
print(f"
|
|
63
|
+
print(f"msg: {ctx.event.text}")
|
|
64
|
+
|
|
65
|
+
@client.on_event(ClosedEvent)
|
|
66
|
+
async def on_closed(ctx):
|
|
67
|
+
print("connection closed")
|
|
73
68
|
|
|
74
69
|
if __name__ == "__main__":
|
|
75
|
-
|
|
70
|
+
client.connect()
|
|
76
71
|
```
|
|
77
72
|
|
|
73
|
+
## Events
|
|
74
|
+
|
|
75
|
+
| Event | Server | Client | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `StartEvent` | ✅ | ✅ | Server/client started |
|
|
78
|
+
| `ConnectEvent` | ✅ | ✅ | Peer connected |
|
|
79
|
+
| `DisconnectEvent` | ✅ | ✅ | Peer disconnected |
|
|
80
|
+
| `MessageEvent` | ✅ | ✅ | Message received |
|
|
81
|
+
| `ClosedEvent` | ❌ | ✅ | Connection closed |
|
|
82
|
+
| `ErrorEvent` | ❌ | ✅ | Error occurred |
|
|
83
|
+
|
|
78
84
|
## Classic API
|
|
79
85
|
|
|
80
86
|
```python
|
|
81
87
|
from fognode import start_server, client_connect
|
|
82
88
|
|
|
83
|
-
ip, code, fp = start_server("0.0.0.0", 9443, "
|
|
84
|
-
print(f"Connect:
|
|
89
|
+
ip, code, fp = start_server("0.0.0.0", 9443, "secret")
|
|
90
|
+
print(f"Connect: {code}:9443")
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
## Structure
|
|
88
94
|
|
|
89
95
|
```
|
|
90
96
|
src/fognode/
|
|
91
|
-
├── app.py #
|
|
92
|
-
├──
|
|
93
|
-
├──
|
|
94
|
-
├──
|
|
95
|
-
├──
|
|
96
|
-
|
|
97
|
-
├── handlers/ # HandlerObject
|
|
98
|
-
├── types/ # exceptions, constants, protocol
|
|
97
|
+
├── app.py # Server, Client, Context
|
|
98
|
+
├── core/
|
|
99
|
+
│ ├── events.py # Event classes
|
|
100
|
+
│ ├── server.py # start_server()
|
|
101
|
+
│ ├── client.py # client_connect()
|
|
102
|
+
│ └── probe.py # probe_server()
|
|
99
103
|
├── crypto/ # primitives, kdf, cert, channel
|
|
100
104
|
├── ciphers/ # aesgcm, chacha20, x25519, hkdf, pbkdf2, hmac
|
|
101
105
|
├── wire/ # framing
|
|
102
106
|
├── auth/ # handshake
|
|
103
|
-
├──
|
|
107
|
+
├── types/ # exceptions, constants, protocol
|
|
104
108
|
├── decorators.py # retry, rate_limited, timed
|
|
105
109
|
├── exceptions.py # errors
|
|
106
110
|
├── utils/ # ipwords, ratelimit, net
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fognode import Server, ConnectEvent, DisconnectEvent, MessageEvent
|
|
4
|
+
|
|
5
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
6
|
+
|
|
7
|
+
@server.on_event(ConnectEvent)
|
|
8
|
+
async def on_connect(ctx):
|
|
9
|
+
print("+ peer connected")
|
|
10
|
+
|
|
11
|
+
@server.on_event(DisconnectEvent)
|
|
12
|
+
async def on_disconnect(ctx):
|
|
13
|
+
print("- peer disconnected")
|
|
14
|
+
|
|
15
|
+
@server.on_event(MessageEvent)
|
|
16
|
+
async def on_message(ctx):
|
|
17
|
+
if ctx.event.text:
|
|
18
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
server.run()
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from fognode import ciphers, decorators, exceptions, filters, types
|
|
4
|
-
from fognode.app import Client, Context,
|
|
4
|
+
from fognode.app import Client, Context, Server
|
|
5
5
|
from fognode.core.client import client_connect
|
|
6
|
+
from fognode.core.events import (
|
|
7
|
+
BaseEvent,
|
|
8
|
+
ClosedEvent,
|
|
9
|
+
ConnectEvent,
|
|
10
|
+
DisconnectEvent,
|
|
11
|
+
ErrorEvent,
|
|
12
|
+
MessageEvent,
|
|
13
|
+
StartEvent,
|
|
14
|
+
)
|
|
6
15
|
from fognode.core.server import start_server
|
|
7
16
|
from fognode.crypto.channel import SecureChannel
|
|
8
17
|
from fognode.types import (
|
|
@@ -57,18 +66,22 @@ __all__ = [
|
|
|
57
66
|
"CodeName",
|
|
58
67
|
"ConnectString",
|
|
59
68
|
"ConnectionInfo",
|
|
69
|
+
"ConnectEvent",
|
|
60
70
|
"Context",
|
|
61
71
|
"DEFAULT_HOST",
|
|
62
72
|
"DEFAULT_PORT",
|
|
73
|
+
"DisconnectEvent",
|
|
74
|
+
"ErrorEvent",
|
|
63
75
|
"Fingerprint",
|
|
64
76
|
"FrameError",
|
|
65
77
|
"HandshakeError",
|
|
66
78
|
"InfoMsg",
|
|
67
79
|
"IPAddress",
|
|
68
80
|
"MAX_MESSAGE_SIZE",
|
|
69
|
-
"
|
|
81
|
+
"MessageEvent",
|
|
70
82
|
"MessageHandler",
|
|
71
83
|
"NONCE_LENGTH",
|
|
84
|
+
"StartEvent",
|
|
72
85
|
"OnConnect",
|
|
73
86
|
"OnDisconnect",
|
|
74
87
|
"OnMessage",
|
|
@@ -2,10 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import threading
|
|
5
|
-
from dataclasses import dataclass
|
|
6
5
|
from typing import TYPE_CHECKING, Any, Callable
|
|
7
6
|
|
|
8
7
|
from fognode.core.client import client_connect
|
|
8
|
+
from fognode.core.events import (
|
|
9
|
+
BaseEvent,
|
|
10
|
+
ClosedEvent,
|
|
11
|
+
ConnectEvent,
|
|
12
|
+
DisconnectEvent,
|
|
13
|
+
ErrorEvent,
|
|
14
|
+
MessageEvent,
|
|
15
|
+
StartEvent,
|
|
16
|
+
)
|
|
9
17
|
from fognode.core.server import start_server
|
|
10
18
|
from fognode.crypto.channel import SecureChannel
|
|
11
19
|
from fognode.handlers import HandlerObject
|
|
@@ -15,28 +23,16 @@ if TYPE_CHECKING:
|
|
|
15
23
|
from fognode.app import Client, Server
|
|
16
24
|
|
|
17
25
|
|
|
18
|
-
@dataclass(slots=True, frozen=True)
|
|
19
|
-
class Message:
|
|
20
|
-
type: str
|
|
21
|
-
text: str
|
|
22
|
-
ts: float
|
|
23
|
-
raw: dict[str, Any]
|
|
24
|
-
|
|
25
|
-
@classmethod
|
|
26
|
-
def from_dict(cls, data: dict[str, Any]) -> Message:
|
|
27
|
-
return cls(
|
|
28
|
-
type=data.get("type", ""),
|
|
29
|
-
text=data.get("text", ""),
|
|
30
|
-
ts=data.get("ts", 0.0),
|
|
31
|
-
raw=data,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass(slots=True)
|
|
36
26
|
class Context:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
app: Server | Client,
|
|
30
|
+
channel: SecureChannel | None,
|
|
31
|
+
event: BaseEvent | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.app = app
|
|
34
|
+
self.channel = channel
|
|
35
|
+
self.event = event
|
|
40
36
|
|
|
41
37
|
async def answer(self, text: str) -> None:
|
|
42
38
|
if self.channel is None:
|
|
@@ -59,34 +55,25 @@ class Server:
|
|
|
59
55
|
self.host = host
|
|
60
56
|
self.port = port
|
|
61
57
|
self.password = password
|
|
62
|
-
self._handlers: dict[
|
|
63
|
-
"connect": [],
|
|
64
|
-
"disconnect": [],
|
|
65
|
-
"message": [],
|
|
66
|
-
}
|
|
58
|
+
self._handlers: dict[type[BaseEvent], list[HandlerObject]] = {}
|
|
67
59
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
68
60
|
self._channel: SecureChannel | None = None
|
|
69
61
|
|
|
70
|
-
def
|
|
62
|
+
def on_event(self, event_class: type[BaseEvent]) -> Callable[..., Any]:
|
|
71
63
|
def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
72
|
-
self._handlers[
|
|
64
|
+
self._handlers.setdefault(event_class, []).append(HandlerObject(callback))
|
|
73
65
|
return callback
|
|
74
66
|
|
|
75
67
|
return decorator
|
|
76
68
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
self._handlers["disconnect"].append(HandlerObject(callback))
|
|
80
|
-
return callback
|
|
69
|
+
def on_connect(self) -> Callable[..., Any]:
|
|
70
|
+
return self.on_event(ConnectEvent)
|
|
81
71
|
|
|
82
|
-
|
|
72
|
+
def on_disconnect(self) -> Callable[..., Any]:
|
|
73
|
+
return self.on_event(DisconnectEvent)
|
|
83
74
|
|
|
84
75
|
def on_message(self) -> Callable[..., Any]:
|
|
85
|
-
|
|
86
|
-
self._handlers["message"].append(HandlerObject(callback))
|
|
87
|
-
return callback
|
|
88
|
-
|
|
89
|
-
return decorator
|
|
76
|
+
return self.on_event(MessageEvent)
|
|
90
77
|
|
|
91
78
|
def run(self) -> None:
|
|
92
79
|
if not self.password:
|
|
@@ -97,11 +84,16 @@ class Server:
|
|
|
97
84
|
|
|
98
85
|
def _on_connect() -> None:
|
|
99
86
|
if self._loop is not None:
|
|
100
|
-
self._loop.call_soon_threadsafe(self.
|
|
87
|
+
self._loop.call_soon_threadsafe(self._process_event, ConnectEvent())
|
|
101
88
|
|
|
102
89
|
def _on_disconnect() -> None:
|
|
103
90
|
if self._loop is not None:
|
|
104
|
-
self._loop.call_soon_threadsafe(self.
|
|
91
|
+
self._loop.call_soon_threadsafe(self._process_event, DisconnectEvent())
|
|
92
|
+
|
|
93
|
+
def _on_msg(msg: dict[str, Any]) -> None:
|
|
94
|
+
if self._loop is not None:
|
|
95
|
+
event = MessageEvent.from_dict(msg)
|
|
96
|
+
self._loop.call_soon_threadsafe(self._process_event, event)
|
|
105
97
|
|
|
106
98
|
start_server(
|
|
107
99
|
self.host,
|
|
@@ -109,8 +101,11 @@ class Server:
|
|
|
109
101
|
self.password,
|
|
110
102
|
on_connect=_on_connect,
|
|
111
103
|
on_disconnect=_on_disconnect,
|
|
104
|
+
on_message=_on_msg,
|
|
112
105
|
)
|
|
113
106
|
|
|
107
|
+
self._process_event(StartEvent())
|
|
108
|
+
|
|
114
109
|
try:
|
|
115
110
|
self._loop.run_forever()
|
|
116
111
|
except KeyboardInterrupt:
|
|
@@ -118,27 +113,18 @@ class Server:
|
|
|
118
113
|
finally:
|
|
119
114
|
self._loop.close()
|
|
120
115
|
|
|
121
|
-
def
|
|
122
|
-
ctx = Context(self, self._channel)
|
|
123
|
-
for handler in self._handlers[
|
|
124
|
-
asyncio.create_task(
|
|
125
|
-
|
|
126
|
-
def _process_disconnect(self) -> None:
|
|
127
|
-
ctx = Context(self, self._channel)
|
|
128
|
-
for handler in self._handlers["disconnect"]:
|
|
129
|
-
asyncio.create_task(handler.call(ctx))
|
|
130
|
-
|
|
131
|
-
def _process_message(self, msg: dict[str, Any]) -> None:
|
|
132
|
-
message = Message.from_dict(msg)
|
|
133
|
-
ctx = Context(self, self._channel, message)
|
|
134
|
-
for handler in self._handlers["message"]:
|
|
135
|
-
asyncio.create_task(self._run_handler(handler, msg, ctx))
|
|
116
|
+
def _process_event(self, event: BaseEvent) -> None:
|
|
117
|
+
ctx = Context(self, self._channel, event)
|
|
118
|
+
for handler in self._handlers.get(type(event), []):
|
|
119
|
+
asyncio.create_task(self._run_handler(handler, event, ctx))
|
|
136
120
|
|
|
137
121
|
async def _run_handler(
|
|
138
|
-
self, handler: HandlerObject,
|
|
122
|
+
self, handler: HandlerObject, event: BaseEvent, ctx: Context
|
|
139
123
|
) -> None:
|
|
140
|
-
if
|
|
141
|
-
await handler.
|
|
124
|
+
if hasattr(event, "data") and isinstance(event.data, dict):
|
|
125
|
+
if not await handler.check(event.data):
|
|
126
|
+
return
|
|
127
|
+
await handler.call(ctx)
|
|
142
128
|
|
|
143
129
|
|
|
144
130
|
class Client:
|
|
@@ -149,34 +135,31 @@ class Client:
|
|
|
149
135
|
) -> None:
|
|
150
136
|
self.connect_string = connect_string
|
|
151
137
|
self.password = password
|
|
152
|
-
self._handlers: dict[
|
|
153
|
-
"connect": [],
|
|
154
|
-
"disconnect": [],
|
|
155
|
-
"message": [],
|
|
156
|
-
}
|
|
138
|
+
self._handlers: dict[type[BaseEvent], list[HandlerObject]] = {}
|
|
157
139
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
158
140
|
self._channel: SecureChannel | None = None
|
|
159
141
|
|
|
160
|
-
def
|
|
142
|
+
def on_event(self, event_class: type[BaseEvent]) -> Callable[..., Any]:
|
|
161
143
|
def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
162
|
-
self._handlers[
|
|
144
|
+
self._handlers.setdefault(event_class, []).append(HandlerObject(callback))
|
|
163
145
|
return callback
|
|
164
146
|
|
|
165
147
|
return decorator
|
|
166
148
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
self._handlers["disconnect"].append(HandlerObject(callback))
|
|
170
|
-
return callback
|
|
149
|
+
def on_connect(self) -> Callable[..., Any]:
|
|
150
|
+
return self.on_event(ConnectEvent)
|
|
171
151
|
|
|
172
|
-
|
|
152
|
+
def on_disconnect(self) -> Callable[..., Any]:
|
|
153
|
+
return self.on_event(DisconnectEvent)
|
|
173
154
|
|
|
174
155
|
def on_message(self) -> Callable[..., Any]:
|
|
175
|
-
|
|
176
|
-
self._handlers["message"].append(HandlerObject(callback))
|
|
177
|
-
return callback
|
|
156
|
+
return self.on_event(MessageEvent)
|
|
178
157
|
|
|
179
|
-
|
|
158
|
+
def on_closed(self) -> Callable[..., Any]:
|
|
159
|
+
return self.on_event(ClosedEvent)
|
|
160
|
+
|
|
161
|
+
def on_error(self) -> Callable[..., Any]:
|
|
162
|
+
return self.on_event(ErrorEvent)
|
|
180
163
|
|
|
181
164
|
def connect(self) -> None:
|
|
182
165
|
if not self.connect_string or not self.password:
|
|
@@ -185,22 +168,37 @@ class Client:
|
|
|
185
168
|
self._loop = asyncio.new_event_loop()
|
|
186
169
|
asyncio.set_event_loop(self._loop)
|
|
187
170
|
|
|
188
|
-
|
|
171
|
+
try:
|
|
172
|
+
ch, _fp = client_connect(self.connect_string, self.password)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
self._process_event(ErrorEvent(exception=exc))
|
|
175
|
+
return
|
|
176
|
+
|
|
189
177
|
self._channel = ch
|
|
190
178
|
|
|
191
179
|
welcome = ch.recv()
|
|
192
180
|
if welcome.get("type") == "welcome":
|
|
193
|
-
self.
|
|
181
|
+
self._process_event(StartEvent(self._channel))
|
|
182
|
+
self._process_event(ConnectEvent(self._channel))
|
|
194
183
|
|
|
195
184
|
def _recv() -> None:
|
|
196
185
|
while True:
|
|
197
186
|
try:
|
|
198
187
|
msg = ch.recv()
|
|
199
188
|
if self._loop is not None:
|
|
200
|
-
|
|
201
|
-
|
|
189
|
+
event = MessageEvent.from_dict(msg, ch)
|
|
190
|
+
self._loop.call_soon_threadsafe(self._process_event, event)
|
|
191
|
+
except Exception as exc:
|
|
202
192
|
if self._loop is not None:
|
|
203
|
-
self._loop.call_soon_threadsafe(
|
|
193
|
+
self._loop.call_soon_threadsafe(
|
|
194
|
+
self._process_event, ErrorEvent(ch, exception=exc)
|
|
195
|
+
)
|
|
196
|
+
self._loop.call_soon_threadsafe(
|
|
197
|
+
self._process_event, ClosedEvent(ch)
|
|
198
|
+
)
|
|
199
|
+
self._loop.call_soon_threadsafe(
|
|
200
|
+
self._process_event, DisconnectEvent(ch)
|
|
201
|
+
)
|
|
204
202
|
break
|
|
205
203
|
|
|
206
204
|
threading.Thread(target=_recv, daemon=True).start()
|
|
@@ -217,24 +215,15 @@ class Client:
|
|
|
217
215
|
raise RuntimeError("not connected")
|
|
218
216
|
self._channel.send(data)
|
|
219
217
|
|
|
220
|
-
def
|
|
221
|
-
ctx = Context(self, self._channel)
|
|
222
|
-
for handler in self._handlers[
|
|
223
|
-
asyncio.create_task(
|
|
224
|
-
|
|
225
|
-
def _process_disconnect(self) -> None:
|
|
226
|
-
ctx = Context(self, self._channel)
|
|
227
|
-
for handler in self._handlers["disconnect"]:
|
|
228
|
-
asyncio.create_task(handler.call(ctx))
|
|
229
|
-
|
|
230
|
-
def _process_raw_msg(self, msg: dict[str, Any]) -> None:
|
|
231
|
-
message = Message.from_dict(msg)
|
|
232
|
-
ctx = Context(self, self._channel, message)
|
|
233
|
-
for handler in self._handlers["message"]:
|
|
234
|
-
asyncio.create_task(self._run_handler(handler, msg, ctx))
|
|
218
|
+
def _process_event(self, event: BaseEvent) -> None:
|
|
219
|
+
ctx = Context(self, self._channel, event)
|
|
220
|
+
for handler in self._handlers.get(type(event), []):
|
|
221
|
+
asyncio.create_task(self._run_handler(handler, event, ctx))
|
|
235
222
|
|
|
236
223
|
async def _run_handler(
|
|
237
|
-
self, handler: HandlerObject,
|
|
224
|
+
self, handler: HandlerObject, event: BaseEvent, ctx: Context
|
|
238
225
|
) -> None:
|
|
239
|
-
if
|
|
240
|
-
await handler.
|
|
226
|
+
if hasattr(event, "data") and isinstance(event.data, dict):
|
|
227
|
+
if not await handler.check(event.data):
|
|
228
|
+
return
|
|
229
|
+
await handler.call(ctx)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fognode.crypto.channel import SecureChannel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class BaseEvent:
|
|
11
|
+
channel: SecureChannel | None = None
|
|
12
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class StartEvent(BaseEvent):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ConnectEvent(BaseEvent):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DisconnectEvent(BaseEvent):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MessageEvent(BaseEvent):
|
|
32
|
+
type: str = ""
|
|
33
|
+
text: str = ""
|
|
34
|
+
ts: float = 0.0
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(
|
|
38
|
+
cls, data: dict[str, Any], channel: SecureChannel | None = None
|
|
39
|
+
) -> MessageEvent:
|
|
40
|
+
return cls(
|
|
41
|
+
channel=channel,
|
|
42
|
+
data=data,
|
|
43
|
+
type=data.get("type", ""),
|
|
44
|
+
text=data.get("text", ""),
|
|
45
|
+
ts=data.get("ts", 0.0),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ClosedEvent(BaseEvent):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ErrorEvent(BaseEvent):
|
|
56
|
+
exception: Exception | None = None
|
|
@@ -13,7 +13,7 @@ from fognode.crypto.cert import cert_fingerprint, cert_paths, generate_cert, ssl
|
|
|
13
13
|
from fognode.crypto.channel import SecureChannel
|
|
14
14
|
from fognode.crypto.password import store_password
|
|
15
15
|
from fognode.types.exceptions import AuthError, SecurityError
|
|
16
|
-
from fognode.types.protocol import CodeName, IPAddress, OnConnect, OnDisconnect, Port
|
|
16
|
+
from fognode.types.protocol import CodeName, IPAddress, OnConnect, OnDisconnect, OnMessage, Port
|
|
17
17
|
from fognode.utils.ipwords import ip_to_name
|
|
18
18
|
from fognode.utils.net import local_ip
|
|
19
19
|
from fognode.utils.ratelimit import RateLimiter
|
|
@@ -26,15 +26,12 @@ def _session_loop(
|
|
|
26
26
|
ip: IPAddress,
|
|
27
27
|
on_connect: OnConnect | None,
|
|
28
28
|
on_disconnect: OnDisconnect | None,
|
|
29
|
+
on_message: OnMessage | None,
|
|
29
30
|
) -> None:
|
|
30
31
|
if on_connect:
|
|
31
32
|
on_connect()
|
|
32
33
|
|
|
33
|
-
ch.send(
|
|
34
|
-
{
|
|
35
|
-
"type": "welcome",
|
|
36
|
-
}
|
|
37
|
-
)
|
|
34
|
+
ch.send({"type": "welcome"})
|
|
38
35
|
|
|
39
36
|
try:
|
|
40
37
|
while True:
|
|
@@ -43,6 +40,8 @@ def _session_loop(
|
|
|
43
40
|
|
|
44
41
|
if mtype == "cmd":
|
|
45
42
|
_handle_cmd(ch, msg)
|
|
43
|
+
elif on_message:
|
|
44
|
+
on_message(msg)
|
|
46
45
|
except (ConnectionError, OSError, SecurityError):
|
|
47
46
|
pass
|
|
48
47
|
finally:
|
|
@@ -51,7 +50,7 @@ def _session_loop(
|
|
|
51
50
|
on_disconnect()
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
def _handle_cmd(ch: SecureChannel, msg: dict) -> None:
|
|
53
|
+
def _handle_cmd(ch: SecureChannel, msg: dict) -> None:
|
|
55
54
|
cmd = msg.get("cmd", "")
|
|
56
55
|
if cmd == "info":
|
|
57
56
|
ch.send(
|
|
@@ -71,6 +70,7 @@ def _handle_client(
|
|
|
71
70
|
ctx: ssl.SSLContext,
|
|
72
71
|
on_connect: OnConnect | None,
|
|
73
72
|
on_disconnect: OnDisconnect | None,
|
|
73
|
+
on_message: OnMessage | None,
|
|
74
74
|
) -> None:
|
|
75
75
|
if not _rl.check(client_ip):
|
|
76
76
|
raw.close()
|
|
@@ -79,7 +79,7 @@ def _handle_client(
|
|
|
79
79
|
with ctx.wrap_socket(raw, server_side=True) as tls:
|
|
80
80
|
tls.settimeout(30)
|
|
81
81
|
ch = server_handshake(tls, client_ip)
|
|
82
|
-
_session_loop(ch, client_ip, on_connect, on_disconnect)
|
|
82
|
+
_session_loop(ch, client_ip, on_connect, on_disconnect, on_message)
|
|
83
83
|
except ssl.SSLError:
|
|
84
84
|
_rl.fail(client_ip)
|
|
85
85
|
except AuthError:
|
|
@@ -97,6 +97,7 @@ def start_server(
|
|
|
97
97
|
password: str,
|
|
98
98
|
on_connect: OnConnect | None = None,
|
|
99
99
|
on_disconnect: OnDisconnect | None = None,
|
|
100
|
+
on_message: OnMessage | None = None,
|
|
100
101
|
) -> tuple[IPAddress, CodeName, str]:
|
|
101
102
|
local = local_ip()
|
|
102
103
|
display_ip = local if host == "0.0.0.0" else host
|
|
@@ -133,7 +134,7 @@ def start_server(
|
|
|
133
134
|
break
|
|
134
135
|
threading.Thread(
|
|
135
136
|
target=_handle_client,
|
|
136
|
-
args=(conn, addr[0], ctx, on_connect, on_disconnect),
|
|
137
|
+
args=(conn, addr[0], ctx, on_connect, on_disconnect, on_message),
|
|
137
138
|
daemon=True,
|
|
138
139
|
).start()
|
|
139
140
|
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from fognode import start_server
|
|
4
|
-
|
|
5
|
-
ip, code, fp = start_server("0.0.0.0", 9443, "alice", "secret")
|
|
6
|
-
print(f"Server running at {ip} ({code}) port 9443")
|
|
7
|
-
print(f"Fingerprint: {fp}")
|
|
8
|
-
|
|
9
|
-
import signal
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
signal.pause()
|
|
13
|
-
except (KeyboardInterrupt, AttributeError):
|
|
14
|
-
pass
|
|
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
|
|
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
|
|
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
|
|
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
|