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.
Files changed (74) hide show
  1. {fognode-0.2.2 → fognode-0.2.3}/PKG-INFO +43 -39
  2. {fognode-0.2.2 → fognode-0.2.3}/README.md +42 -38
  3. fognode-0.2.3/examples/headless_server.py +21 -0
  4. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/__init__.py +15 -2
  5. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/app.py +87 -98
  6. fognode-0.2.3/src/fognode/core/events.py +56 -0
  7. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/server.py +10 -9
  8. fognode-0.2.2/examples/headless_server.py +0 -14
  9. {fognode-0.2.2 → fognode-0.2.3}/.github/CODEOWNERS +0 -0
  10. {fognode-0.2.2 → fognode-0.2.3}/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +0 -0
  11. {fognode-0.2.2 → fognode-0.2.3}/.github/PULL_REQUEST_TEMPLATE/optimization.md +0 -0
  12. {fognode-0.2.2 → fognode-0.2.3}/.github/dependabot.yml +0 -0
  13. {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/build.yml +0 -0
  14. {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/pr-summary.yml +0 -0
  15. {fognode-0.2.2 → fognode-0.2.3}/.github/workflows/pr.yml +0 -0
  16. {fognode-0.2.2 → fognode-0.2.3}/.gitignore +0 -0
  17. {fognode-0.2.2 → fognode-0.2.3}/AGENTS.md +0 -0
  18. {fognode-0.2.2 → fognode-0.2.3}/CHANGELOG.md +0 -0
  19. {fognode-0.2.2 → fognode-0.2.3}/CONTRIBUTING.md +0 -0
  20. {fognode-0.2.2 → fognode-0.2.3}/LICENSE +0 -0
  21. {fognode-0.2.2 → fognode-0.2.3}/pyproject.toml +0 -0
  22. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/__main__.py +0 -0
  23. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/auth/__init__.py +0 -0
  24. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/auth/handshake.py +0 -0
  25. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/__init__.py +0 -0
  26. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/aesgcm.py +0 -0
  27. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/blake2.py +0 -0
  28. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/blowfish.py +0 -0
  29. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/chacha20.py +0 -0
  30. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/ed25519.py +0 -0
  31. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/fernet.py +0 -0
  32. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/hkdf.py +0 -0
  33. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/hmac.py +0 -0
  34. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/pbkdf2.py +0 -0
  35. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/rsa.py +0 -0
  36. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/scrypt.py +0 -0
  37. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/sha3.py +0 -0
  38. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/ciphers/x25519.py +0 -0
  39. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/cli/__init__.py +0 -0
  40. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/cli/entrypoint.py +0 -0
  41. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/__init__.py +0 -0
  42. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/client.py +0 -0
  43. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/core/probe.py +0 -0
  44. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/__init__.py +0 -0
  45. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/cert.py +0 -0
  46. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/channel.py +0 -0
  47. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/kdf.py +0 -0
  48. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/kx.py +0 -0
  49. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/password.py +0 -0
  50. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/crypto/primitives.py +0 -0
  51. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/decorators.py +0 -0
  52. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/exceptions.py +0 -0
  53. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/__init__.py +0 -0
  54. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/base.py +0 -0
  55. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/command.py +0 -0
  56. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/filters/text.py +0 -0
  57. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/handlers/__init__.py +0 -0
  58. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/handlers/handler.py +0 -0
  59. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/__init__.py +0 -0
  60. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/constants.py +0 -0
  61. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/exceptions.py +0 -0
  62. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/types/protocol.py +0 -0
  63. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/__init__.py +0 -0
  64. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/ipwords.py +0 -0
  65. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/net.py +0 -0
  66. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/utils/ratelimit.py +0 -0
  67. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/wire/__init__.py +0 -0
  68. {fognode-0.2.2 → fognode-0.2.3}/src/fognode/wire/framing.py +0 -0
  69. {fognode-0.2.2 → fognode-0.2.3}/tests/__init__.py +0 -0
  70. {fognode-0.2.2 → fognode-0.2.3}/tests/conftest.py +0 -0
  71. {fognode-0.2.2 → fognode-0.2.3}/tests/test_crypto.py +0 -0
  72. {fognode-0.2.2 → fognode-0.2.3}/tests/test_framing.py +0 -0
  73. {fognode-0.2.2 → fognode-0.2.3}/tests/test_ipwords.py +0 -0
  74. {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.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 (aiogram-style)
66
+ ## Quick start
67
67
 
68
68
  ### Server
69
69
 
70
70
  ```python
71
- from fognode import App, Cipher
71
+ from fognode import Server, MessageEvent
72
72
 
73
- app = App(
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
- @app.on_message()
75
+ @server.on_event(MessageEvent)
82
76
  async def echo(ctx):
83
- await ctx.answer(f"echo: {ctx.message.text}")
77
+ if ctx.event.text:
78
+ await ctx.answer(f"echo: {ctx.event.text}")
84
79
 
85
- @app.on_command("ping")
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(f"+ {ctx.user}")
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
- app.run()
89
+ server.run()
95
90
  ```
96
91
 
97
92
  ### Client
98
93
 
99
94
  ```python
100
- from fognode import App, Cipher
95
+ from fognode import Client, MessageEvent, ClosedEvent
101
96
 
102
- app = App.client(
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
- @app.on_message()
99
+ @client.on_event(MessageEvent)
109
100
  async def on_message(ctx):
110
- print(f"{ctx.message.user}: {ctx.message.text}")
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
- app.run()
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, "alice", "secret")
122
- print(f"Connect: alice@{code}:9443")
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 # App class (aiogram-style)
130
- ├── cipher.py # Cipher enum
131
- ├── context.py # Context for handlers
132
- ├── message.py # Message dataclass
133
- ├── router.py # Router for handlers
134
- ├── filters/ # Command, Text filters
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
- ├── core/ # server, client, session, state
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 (aiogram-style)
28
+ ## Quick start
29
29
 
30
30
  ### Server
31
31
 
32
32
  ```python
33
- from fognode import App, Cipher
33
+ from fognode import Server, MessageEvent
34
34
 
35
- app = App(
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
- @app.on_message()
37
+ @server.on_event(MessageEvent)
44
38
  async def echo(ctx):
45
- await ctx.answer(f"echo: {ctx.message.text}")
39
+ if ctx.event.text:
40
+ await ctx.answer(f"echo: {ctx.event.text}")
46
41
 
47
- @app.on_command("ping")
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(f"+ {ctx.user}")
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
- app.run()
51
+ server.run()
57
52
  ```
58
53
 
59
54
  ### Client
60
55
 
61
56
  ```python
62
- from fognode import App, Cipher
57
+ from fognode import Client, MessageEvent, ClosedEvent
63
58
 
64
- app = App.client(
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
- @app.on_message()
61
+ @client.on_event(MessageEvent)
71
62
  async def on_message(ctx):
72
- print(f"{ctx.message.user}: {ctx.message.text}")
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
- app.run()
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, "alice", "secret")
84
- print(f"Connect: alice@{code}:9443")
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 # App class (aiogram-style)
92
- ├── cipher.py # Cipher enum
93
- ├── context.py # Context for handlers
94
- ├── message.py # Message dataclass
95
- ├── router.py # Router for handlers
96
- ├── filters/ # Command, Text filters
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
- ├── core/ # server, client, session, state
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, Message, Server
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
- "Message",
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
- app: Server | Client
38
- channel: SecureChannel | None
39
- message: Message | None = None
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[str, list[HandlerObject]] = {
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 on_connect(self) -> Callable[..., Any]:
62
+ def on_event(self, event_class: type[BaseEvent]) -> Callable[..., Any]:
71
63
  def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
72
- self._handlers["connect"].append(HandlerObject(callback))
64
+ self._handlers.setdefault(event_class, []).append(HandlerObject(callback))
73
65
  return callback
74
66
 
75
67
  return decorator
76
68
 
77
- def on_disconnect(self) -> Callable[..., Any]:
78
- def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
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
- return decorator
72
+ def on_disconnect(self) -> Callable[..., Any]:
73
+ return self.on_event(DisconnectEvent)
83
74
 
84
75
  def on_message(self) -> Callable[..., Any]:
85
- def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
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._process_connect)
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._process_disconnect)
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 _process_connect(self) -> None:
122
- ctx = Context(self, self._channel)
123
- for handler in self._handlers["connect"]:
124
- asyncio.create_task(handler.call(ctx))
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, data: dict[str, Any], ctx: Context
122
+ self, handler: HandlerObject, event: BaseEvent, ctx: Context
139
123
  ) -> None:
140
- if await handler.check(data):
141
- await handler.call(ctx)
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[str, list[HandlerObject]] = {
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 on_connect(self) -> Callable[..., Any]:
142
+ def on_event(self, event_class: type[BaseEvent]) -> Callable[..., Any]:
161
143
  def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
162
- self._handlers["connect"].append(HandlerObject(callback))
144
+ self._handlers.setdefault(event_class, []).append(HandlerObject(callback))
163
145
  return callback
164
146
 
165
147
  return decorator
166
148
 
167
- def on_disconnect(self) -> Callable[..., Any]:
168
- def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
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
- return decorator
152
+ def on_disconnect(self) -> Callable[..., Any]:
153
+ return self.on_event(DisconnectEvent)
173
154
 
174
155
  def on_message(self) -> Callable[..., Any]:
175
- def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
176
- self._handlers["message"].append(HandlerObject(callback))
177
- return callback
156
+ return self.on_event(MessageEvent)
178
157
 
179
- return decorator
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
- ch, _fp = client_connect(self.connect_string, self.password)
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._process_connect()
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
- self._loop.call_soon_threadsafe(self._process_raw_msg, msg)
201
- except Exception:
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(self._process_disconnect)
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 _process_connect(self) -> None:
221
- ctx = Context(self, self._channel)
222
- for handler in self._handlers["connect"]:
223
- asyncio.create_task(handler.call(ctx))
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, data: dict[str, Any], ctx: Context
224
+ self, handler: HandlerObject, event: BaseEvent, ctx: Context
238
225
  ) -> None:
239
- if await handler.check(data):
240
- await handler.call(ctx)
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: # type: ignore[type-arg]
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