lantalk 2.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.
- lantalk-2.0.0/LICENCE +21 -0
- lantalk-2.0.0/PKG-INFO +300 -0
- lantalk-2.0.0/README.md +270 -0
- lantalk-2.0.0/lantalk/__init__.py +2 -0
- lantalk-2.0.0/lantalk/cli.py +54 -0
- lantalk-2.0.0/lantalk/client.py +269 -0
- lantalk-2.0.0/lantalk/protocol.py +72 -0
- lantalk-2.0.0/lantalk/server.py +522 -0
- lantalk-2.0.0/lantalk.egg-info/PKG-INFO +300 -0
- lantalk-2.0.0/lantalk.egg-info/SOURCES.txt +15 -0
- lantalk-2.0.0/lantalk.egg-info/dependency_links.txt +1 -0
- lantalk-2.0.0/lantalk.egg-info/entry_points.txt +2 -0
- lantalk-2.0.0/lantalk.egg-info/requires.txt +4 -0
- lantalk-2.0.0/lantalk.egg-info/top_level.txt +1 -0
- lantalk-2.0.0/pyproject.toml +52 -0
- lantalk-2.0.0/setup.cfg +4 -0
- lantalk-2.0.0/tests/test_lantalk.py +323 -0
lantalk-2.0.0/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ezra Destaw
|
|
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
|
+
SOFTWARE.
|
lantalk-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lantalk
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Terminal-based LAN chat — async, discoverable, secure
|
|
5
|
+
Author-email: Ezra Destaw <ezradestaw@email.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Ezradestaw/lantalk
|
|
7
|
+
Project-URL: Repository, https://github.com/Ezradestaw/lantalk
|
|
8
|
+
Project-URL: Issues, https://github.com/Ezradestaw/lantalk/issues
|
|
9
|
+
Keywords: chat,lan,wifi,terminal,networking,broadcast,asyncio
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Chat
|
|
22
|
+
Classifier: Topic :: Internet
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENCE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# LanTalk
|
|
32
|
+
|
|
33
|
+
> Terminal-based LAN chat — pure asyncio, auto-discoverable, zero runtime dependencies.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
_ _____ _ _
|
|
37
|
+
| | |_ _| | | |
|
|
38
|
+
| | __ _ _ __ | | __ _| | | __
|
|
39
|
+
| | / _` | '_ \ | |/ _` | | |/ /
|
|
40
|
+
| |___| (_| | | | || | (_| | | <
|
|
41
|
+
|______\__,_|_| |_\_/ \__,_|_|_|\_\
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/lantalk/)
|
|
45
|
+
[](https://pypi.org/project/lantalk/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What is LanTalk?
|
|
51
|
+
|
|
52
|
+
LanTalk lets people on the **same Wi-Fi or LAN** chat instantly — no accounts, no internet, no servers in the cloud. One person runs `lantalk` and starts a server; everyone else runs `lantalk` and joins. That's it.
|
|
53
|
+
|
|
54
|
+
- Works on school networks, home networks, offices, hackathons
|
|
55
|
+
- Pure Python stdlib — nothing to `pip install` beyond the package itself
|
|
56
|
+
- Clients auto-discover the server via UDP broadcast (no IP typing required)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
| | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| 🔍 **Auto-discovery** | UDP beacon finds servers on your LAN automatically |
|
|
65
|
+
| 🔐 **Password auth** | Optional server password with SHA-256 hashing |
|
|
66
|
+
| 👤 **Username dedup** | Two people can't use the same name simultaneously |
|
|
67
|
+
| 💬 **Private messages** | `/pm <user> <message>` — end-to-end on the wire |
|
|
68
|
+
| 🛡 **Rate limiting** | Spammers get warned then kicked (10 msg / 5 s) |
|
|
69
|
+
| ⏱ **Idle timeouts** | Dead sockets cleaned up after 5 minutes of silence |
|
|
70
|
+
| 🚫 **Persistent bans** | Banned IPs survive server restarts via `bans.json` |
|
|
71
|
+
| 🔇 **Full validation** | Every message checked for type and field constraints |
|
|
72
|
+
| 📝 **Structured logging** | Console + `lantalk.log` file, levels DEBUG/INFO/WARNING |
|
|
73
|
+
| ⚡ **Pure asyncio** | Single event loop — no threads, no race conditions |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install lantalk
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Python 3.10+ required. No third-party dependencies.
|
|
84
|
+
|
|
85
|
+
### From source
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/yourusername/lantalk
|
|
89
|
+
cd lantalk
|
|
90
|
+
pip install -e ".[dev]"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
### 1 — Start a server (one person does this)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
$ lantalk
|
|
101
|
+
|
|
102
|
+
[1] Start a Server
|
|
103
|
+
[2] Join as Client
|
|
104
|
+
|
|
105
|
+
Choose (1 or 2): 1
|
|
106
|
+
Port (default 5050):
|
|
107
|
+
Server display name: Alice
|
|
108
|
+
Set password (leave blank for none): secret
|
|
109
|
+
|
|
110
|
+
══════════════════════════════════════════════════════
|
|
111
|
+
LanTalk Server v2.0.0 — Alice
|
|
112
|
+
Listening on 192.168.1.5:5050
|
|
113
|
+
Discovery beacon active (UDP 5051)
|
|
114
|
+
Password protection: ON
|
|
115
|
+
══════════════════════════════════════════════════════
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2 — Join as a client (everyone else)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
$ lantalk
|
|
122
|
+
|
|
123
|
+
[1] Start a Server
|
|
124
|
+
[2] Join as Client
|
|
125
|
+
|
|
126
|
+
Choose (1 or 2): 2
|
|
127
|
+
Scanning for LanTalk servers…
|
|
128
|
+
Found 1 server(s):
|
|
129
|
+
|
|
130
|
+
[1] 192.168.1.5:5050
|
|
131
|
+
[m] Enter manually
|
|
132
|
+
|
|
133
|
+
Choose server: 1
|
|
134
|
+
Display name: Bob
|
|
135
|
+
Password: secret
|
|
136
|
+
|
|
137
|
+
══════════════════════════════════════════════════════
|
|
138
|
+
LanTalk Client v2.0.0 — Bob
|
|
139
|
+
Connected to 192.168.1.5:5050
|
|
140
|
+
Commands: /users /pm <user> <msg> /quit
|
|
141
|
+
══════════════════════════════════════════════════════
|
|
142
|
+
[Bob | lantalk] > Hey everyone!
|
|
143
|
+
[14:22] Alice: Welcome Bob!
|
|
144
|
+
[Bob | lantalk] > /pm Alice just between us 👋
|
|
145
|
+
[Bob | lantalk] > /users
|
|
146
|
+
[14:23] *** Online: Alice, Bob, Carol ***
|
|
147
|
+
[Bob | lantalk] >
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
### Client commands
|
|
155
|
+
|
|
156
|
+
| Command | Description |
|
|
157
|
+
|---------|-------------|
|
|
158
|
+
| `/users` | List all connected users |
|
|
159
|
+
| `/pm <user> <message>` | Send a private message |
|
|
160
|
+
| `/quit` | Disconnect and exit |
|
|
161
|
+
|
|
162
|
+
### Server console commands
|
|
163
|
+
|
|
164
|
+
| Command | Description |
|
|
165
|
+
|---------|-------------|
|
|
166
|
+
| `/users` | List connected users |
|
|
167
|
+
| `/stats` | Show connection count and ban list size |
|
|
168
|
+
| `/kick <user>` | Disconnect a user |
|
|
169
|
+
| `/ban <user>` | Ban a user's IP (persisted to `bans.json`) |
|
|
170
|
+
| `/bans` | Show all banned IPs |
|
|
171
|
+
| `/unban <ip>` | Remove an IP from the ban list |
|
|
172
|
+
| `/help` | Show this list |
|
|
173
|
+
| `/quit` | Shut down the server |
|
|
174
|
+
| *(anything else)* | Broadcast as a server message to all clients |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Architecture
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
182
|
+
│ LAN (e.g. 192.168.1.0/24) │
|
|
183
|
+
│ │
|
|
184
|
+
│ ┌────────────────────────────────────┐ │
|
|
185
|
+
│ │ LanTalkServer (pure asyncio) │ │
|
|
186
|
+
│ │ │◄─── TCP :5050 ───┐ │
|
|
187
|
+
│ │ _discovery_beacon() ─────────────┼──── UDP bcast │ │
|
|
188
|
+
│ │ asyncio coroutine, no threads │ every 2 s │ │
|
|
189
|
+
│ │ │ │ │
|
|
190
|
+
│ │ per-client _RateLimiter │ │ │
|
|
191
|
+
│ │ _validate() on every message │ │ │
|
|
192
|
+
│ │ asyncio.wait_for() timeouts │ │ │
|
|
193
|
+
│ │ bans.json (persistent) │ │ │
|
|
194
|
+
│ │ lantalk.log (structured) │ │ │
|
|
195
|
+
│ └────────────────────────────────────┘ │ │
|
|
196
|
+
│ │ │
|
|
197
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┴─┐ │
|
|
198
|
+
│ │ Client A │ │ Client B │ │ Client C │ │
|
|
199
|
+
│ │ UDP discover │ │ UDP discover │ │ manual IP entry │ │
|
|
200
|
+
│ │ asyncio │ │ asyncio │ │ asyncio │ │
|
|
201
|
+
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
|
|
202
|
+
└──────────────────────────────────────────────────────────────┘
|
|
203
|
+
|
|
204
|
+
Message flow (post-auth):
|
|
205
|
+
Client ──JSON──► Server ──broadcast JSON──► all other Clients
|
|
206
|
+
└──(PM)──────────► one Client
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Concurrency model
|
|
210
|
+
|
|
211
|
+
Everything runs in **one asyncio event loop** — no `threading.Thread`, no locks between threads, no shared mutable state across OS threads. The only `asyncio.Lock` is used to guard the `_clients` dict between concurrent coroutines.
|
|
212
|
+
|
|
213
|
+
### JSON wire protocol
|
|
214
|
+
|
|
215
|
+
Every message is a single UTF-8 JSON line terminated with `\n`.
|
|
216
|
+
|
|
217
|
+
```jsonc
|
|
218
|
+
// Server → Client: auth challenge
|
|
219
|
+
{"type": "auth_request", "ts": "14:22", "needs_password": true}
|
|
220
|
+
|
|
221
|
+
// Client → Server: credentials
|
|
222
|
+
{"username": "Bob", "password": "secret"}
|
|
223
|
+
|
|
224
|
+
// Server → Client: success / failure
|
|
225
|
+
{"type": "auth_ok", "ts": "…", "username": "Bob", "server_name": "Alice"}
|
|
226
|
+
{"type": "auth_fail", "ts": "…", "reason": "Username already taken."}
|
|
227
|
+
|
|
228
|
+
// Client → Server: chat message
|
|
229
|
+
{"type": "message", "text": "Hello!"}
|
|
230
|
+
|
|
231
|
+
// Server → all clients: broadcast
|
|
232
|
+
{"type": "message", "ts": "14:22", "user": "Bob", "text": "Hello!"}
|
|
233
|
+
|
|
234
|
+
// Server → one client: private message
|
|
235
|
+
{"type": "pm", "ts": "14:22", "from_user": "Alice", "text": "hey"}
|
|
236
|
+
|
|
237
|
+
// Server → clients: system event
|
|
238
|
+
{"type": "system", "ts": "14:22", "text": "Bob joined from 192.168.1.10."}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Security notes
|
|
244
|
+
|
|
245
|
+
- Passwords are **never stored or sent in plaintext**. The server stores a SHA-256 hash; clients send the raw password only during the auth handshake (upgrade to TLS in v3 is planned).
|
|
246
|
+
- Banned IPs are written to `bans.json` in the server's working directory and reloaded on every start.
|
|
247
|
+
- Rate limiting: each client is capped at **10 messages per 5 seconds**. Exceeding this sends a warning; repeat offences kick the user.
|
|
248
|
+
- Usernames are limited to 24 alphanumeric/underscore/hyphen characters.
|
|
249
|
+
- All incoming messages are schema-validated before processing; malformed JSON is silently dropped.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Logging
|
|
254
|
+
|
|
255
|
+
LanTalk writes structured logs to both the terminal and `lantalk.log`:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
[14:22:01] INFO Server started on 192.168.1.5:5050
|
|
259
|
+
[14:22:15] INFO Bob joined from 192.168.1.10
|
|
260
|
+
[14:23:44] WARNING Carol hit rate limit
|
|
261
|
+
[14:25:01] INFO Kicked Dave
|
|
262
|
+
[14:25:03] INFO Banned Dave (192.168.1.22)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Set `LANTALK_LOG_LEVEL=DEBUG` to see per-message traces.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Running tests
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
pip install -e ".[dev]"
|
|
273
|
+
pytest -v
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Changelog
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
2.0.0 — pure asyncio (no threads), validation, rate limiting,
|
|
282
|
+
timeouts, persistent bans, structured logging, v2 prompt
|
|
283
|
+
1.1.0 — asyncio rewrite, UDP discovery, JSON protocol, auth, commands, pytest
|
|
284
|
+
1.0.0 — initial: threading, basic TCP, plain-text protocol
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Roadmap
|
|
290
|
+
|
|
291
|
+
- [ ] TLS encryption (v3)
|
|
292
|
+
- [ ] File transfer (`/send file.pdf`)
|
|
293
|
+
- [ ] Web UI mode (`http://localhost:5050`)
|
|
294
|
+
- [ ] Plugin system (`@lantalk.plugin`)
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT © Ezra
|
lantalk-2.0.0/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# LanTalk
|
|
2
|
+
|
|
3
|
+
> Terminal-based LAN chat — pure asyncio, auto-discoverable, zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
_ _____ _ _
|
|
7
|
+
| | |_ _| | | |
|
|
8
|
+
| | __ _ _ __ | | __ _| | | __
|
|
9
|
+
| | / _` | '_ \ | |/ _` | | |/ /
|
|
10
|
+
| |___| (_| | | | || | (_| | | <
|
|
11
|
+
|______\__,_|_| |_\_/ \__,_|_|_|\_\
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
[](https://pypi.org/project/lantalk/)
|
|
15
|
+
[](https://pypi.org/project/lantalk/)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What is LanTalk?
|
|
21
|
+
|
|
22
|
+
LanTalk lets people on the **same Wi-Fi or LAN** chat instantly — no accounts, no internet, no servers in the cloud. One person runs `lantalk` and starts a server; everyone else runs `lantalk` and joins. That's it.
|
|
23
|
+
|
|
24
|
+
- Works on school networks, home networks, offices, hackathons
|
|
25
|
+
- Pure Python stdlib — nothing to `pip install` beyond the package itself
|
|
26
|
+
- Clients auto-discover the server via UDP broadcast (no IP typing required)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| 🔍 **Auto-discovery** | UDP beacon finds servers on your LAN automatically |
|
|
35
|
+
| 🔐 **Password auth** | Optional server password with SHA-256 hashing |
|
|
36
|
+
| 👤 **Username dedup** | Two people can't use the same name simultaneously |
|
|
37
|
+
| 💬 **Private messages** | `/pm <user> <message>` — end-to-end on the wire |
|
|
38
|
+
| 🛡 **Rate limiting** | Spammers get warned then kicked (10 msg / 5 s) |
|
|
39
|
+
| ⏱ **Idle timeouts** | Dead sockets cleaned up after 5 minutes of silence |
|
|
40
|
+
| 🚫 **Persistent bans** | Banned IPs survive server restarts via `bans.json` |
|
|
41
|
+
| 🔇 **Full validation** | Every message checked for type and field constraints |
|
|
42
|
+
| 📝 **Structured logging** | Console + `lantalk.log` file, levels DEBUG/INFO/WARNING |
|
|
43
|
+
| ⚡ **Pure asyncio** | Single event loop — no threads, no race conditions |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install lantalk
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.10+ required. No third-party dependencies.
|
|
54
|
+
|
|
55
|
+
### From source
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/yourusername/lantalk
|
|
59
|
+
cd lantalk
|
|
60
|
+
pip install -e ".[dev]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
### 1 — Start a server (one person does this)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ lantalk
|
|
71
|
+
|
|
72
|
+
[1] Start a Server
|
|
73
|
+
[2] Join as Client
|
|
74
|
+
|
|
75
|
+
Choose (1 or 2): 1
|
|
76
|
+
Port (default 5050):
|
|
77
|
+
Server display name: Alice
|
|
78
|
+
Set password (leave blank for none): secret
|
|
79
|
+
|
|
80
|
+
══════════════════════════════════════════════════════
|
|
81
|
+
LanTalk Server v2.0.0 — Alice
|
|
82
|
+
Listening on 192.168.1.5:5050
|
|
83
|
+
Discovery beacon active (UDP 5051)
|
|
84
|
+
Password protection: ON
|
|
85
|
+
══════════════════════════════════════════════════════
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2 — Join as a client (everyone else)
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
$ lantalk
|
|
92
|
+
|
|
93
|
+
[1] Start a Server
|
|
94
|
+
[2] Join as Client
|
|
95
|
+
|
|
96
|
+
Choose (1 or 2): 2
|
|
97
|
+
Scanning for LanTalk servers…
|
|
98
|
+
Found 1 server(s):
|
|
99
|
+
|
|
100
|
+
[1] 192.168.1.5:5050
|
|
101
|
+
[m] Enter manually
|
|
102
|
+
|
|
103
|
+
Choose server: 1
|
|
104
|
+
Display name: Bob
|
|
105
|
+
Password: secret
|
|
106
|
+
|
|
107
|
+
══════════════════════════════════════════════════════
|
|
108
|
+
LanTalk Client v2.0.0 — Bob
|
|
109
|
+
Connected to 192.168.1.5:5050
|
|
110
|
+
Commands: /users /pm <user> <msg> /quit
|
|
111
|
+
══════════════════════════════════════════════════════
|
|
112
|
+
[Bob | lantalk] > Hey everyone!
|
|
113
|
+
[14:22] Alice: Welcome Bob!
|
|
114
|
+
[Bob | lantalk] > /pm Alice just between us 👋
|
|
115
|
+
[Bob | lantalk] > /users
|
|
116
|
+
[14:23] *** Online: Alice, Bob, Carol ***
|
|
117
|
+
[Bob | lantalk] >
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Commands
|
|
123
|
+
|
|
124
|
+
### Client commands
|
|
125
|
+
|
|
126
|
+
| Command | Description |
|
|
127
|
+
|---------|-------------|
|
|
128
|
+
| `/users` | List all connected users |
|
|
129
|
+
| `/pm <user> <message>` | Send a private message |
|
|
130
|
+
| `/quit` | Disconnect and exit |
|
|
131
|
+
|
|
132
|
+
### Server console commands
|
|
133
|
+
|
|
134
|
+
| Command | Description |
|
|
135
|
+
|---------|-------------|
|
|
136
|
+
| `/users` | List connected users |
|
|
137
|
+
| `/stats` | Show connection count and ban list size |
|
|
138
|
+
| `/kick <user>` | Disconnect a user |
|
|
139
|
+
| `/ban <user>` | Ban a user's IP (persisted to `bans.json`) |
|
|
140
|
+
| `/bans` | Show all banned IPs |
|
|
141
|
+
| `/unban <ip>` | Remove an IP from the ban list |
|
|
142
|
+
| `/help` | Show this list |
|
|
143
|
+
| `/quit` | Shut down the server |
|
|
144
|
+
| *(anything else)* | Broadcast as a server message to all clients |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Architecture
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
152
|
+
│ LAN (e.g. 192.168.1.0/24) │
|
|
153
|
+
│ │
|
|
154
|
+
│ ┌────────────────────────────────────┐ │
|
|
155
|
+
│ │ LanTalkServer (pure asyncio) │ │
|
|
156
|
+
│ │ │◄─── TCP :5050 ───┐ │
|
|
157
|
+
│ │ _discovery_beacon() ─────────────┼──── UDP bcast │ │
|
|
158
|
+
│ │ asyncio coroutine, no threads │ every 2 s │ │
|
|
159
|
+
│ │ │ │ │
|
|
160
|
+
│ │ per-client _RateLimiter │ │ │
|
|
161
|
+
│ │ _validate() on every message │ │ │
|
|
162
|
+
│ │ asyncio.wait_for() timeouts │ │ │
|
|
163
|
+
│ │ bans.json (persistent) │ │ │
|
|
164
|
+
│ │ lantalk.log (structured) │ │ │
|
|
165
|
+
│ └────────────────────────────────────┘ │ │
|
|
166
|
+
│ │ │
|
|
167
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┴─┐ │
|
|
168
|
+
│ │ Client A │ │ Client B │ │ Client C │ │
|
|
169
|
+
│ │ UDP discover │ │ UDP discover │ │ manual IP entry │ │
|
|
170
|
+
│ │ asyncio │ │ asyncio │ │ asyncio │ │
|
|
171
|
+
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
|
|
172
|
+
└──────────────────────────────────────────────────────────────┘
|
|
173
|
+
|
|
174
|
+
Message flow (post-auth):
|
|
175
|
+
Client ──JSON──► Server ──broadcast JSON──► all other Clients
|
|
176
|
+
└──(PM)──────────► one Client
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Concurrency model
|
|
180
|
+
|
|
181
|
+
Everything runs in **one asyncio event loop** — no `threading.Thread`, no locks between threads, no shared mutable state across OS threads. The only `asyncio.Lock` is used to guard the `_clients` dict between concurrent coroutines.
|
|
182
|
+
|
|
183
|
+
### JSON wire protocol
|
|
184
|
+
|
|
185
|
+
Every message is a single UTF-8 JSON line terminated with `\n`.
|
|
186
|
+
|
|
187
|
+
```jsonc
|
|
188
|
+
// Server → Client: auth challenge
|
|
189
|
+
{"type": "auth_request", "ts": "14:22", "needs_password": true}
|
|
190
|
+
|
|
191
|
+
// Client → Server: credentials
|
|
192
|
+
{"username": "Bob", "password": "secret"}
|
|
193
|
+
|
|
194
|
+
// Server → Client: success / failure
|
|
195
|
+
{"type": "auth_ok", "ts": "…", "username": "Bob", "server_name": "Alice"}
|
|
196
|
+
{"type": "auth_fail", "ts": "…", "reason": "Username already taken."}
|
|
197
|
+
|
|
198
|
+
// Client → Server: chat message
|
|
199
|
+
{"type": "message", "text": "Hello!"}
|
|
200
|
+
|
|
201
|
+
// Server → all clients: broadcast
|
|
202
|
+
{"type": "message", "ts": "14:22", "user": "Bob", "text": "Hello!"}
|
|
203
|
+
|
|
204
|
+
// Server → one client: private message
|
|
205
|
+
{"type": "pm", "ts": "14:22", "from_user": "Alice", "text": "hey"}
|
|
206
|
+
|
|
207
|
+
// Server → clients: system event
|
|
208
|
+
{"type": "system", "ts": "14:22", "text": "Bob joined from 192.168.1.10."}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Security notes
|
|
214
|
+
|
|
215
|
+
- Passwords are **never stored or sent in plaintext**. The server stores a SHA-256 hash; clients send the raw password only during the auth handshake (upgrade to TLS in v3 is planned).
|
|
216
|
+
- Banned IPs are written to `bans.json` in the server's working directory and reloaded on every start.
|
|
217
|
+
- Rate limiting: each client is capped at **10 messages per 5 seconds**. Exceeding this sends a warning; repeat offences kick the user.
|
|
218
|
+
- Usernames are limited to 24 alphanumeric/underscore/hyphen characters.
|
|
219
|
+
- All incoming messages are schema-validated before processing; malformed JSON is silently dropped.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Logging
|
|
224
|
+
|
|
225
|
+
LanTalk writes structured logs to both the terminal and `lantalk.log`:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
[14:22:01] INFO Server started on 192.168.1.5:5050
|
|
229
|
+
[14:22:15] INFO Bob joined from 192.168.1.10
|
|
230
|
+
[14:23:44] WARNING Carol hit rate limit
|
|
231
|
+
[14:25:01] INFO Kicked Dave
|
|
232
|
+
[14:25:03] INFO Banned Dave (192.168.1.22)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Set `LANTALK_LOG_LEVEL=DEBUG` to see per-message traces.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Running tests
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pip install -e ".[dev]"
|
|
243
|
+
pytest -v
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Changelog
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
2.0.0 — pure asyncio (no threads), validation, rate limiting,
|
|
252
|
+
timeouts, persistent bans, structured logging, v2 prompt
|
|
253
|
+
1.1.0 — asyncio rewrite, UDP discovery, JSON protocol, auth, commands, pytest
|
|
254
|
+
1.0.0 — initial: threading, basic TCP, plain-text protocol
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Roadmap
|
|
260
|
+
|
|
261
|
+
- [ ] TLS encryption (v3)
|
|
262
|
+
- [ ] File transfer (`/send file.pdf`)
|
|
263
|
+
- [ ] Web UI mode (`http://localhost:5050`)
|
|
264
|
+
- [ ] Plugin system (`@lantalk.plugin`)
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT © Ezra
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
lantalk.cli
|
|
3
|
+
-----------
|
|
4
|
+
Entry point for the `lantalk` command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from lantalk.server import run_server, DEFAULT_PORT as SERVER_PORT
|
|
9
|
+
from lantalk.client import run_client, DEFAULT_PORT as CLIENT_PORT
|
|
10
|
+
|
|
11
|
+
VERSION = "2.0.0"
|
|
12
|
+
|
|
13
|
+
BANNER = r"""
|
|
14
|
+
_ _____ _ _
|
|
15
|
+
| | |_ _| | | |
|
|
16
|
+
| | __ _ _ __ | | __ _| | | __
|
|
17
|
+
| | / _` | '_ \ | |/ _` | | |/ /
|
|
18
|
+
| |___| (_| | | | || | (_| | | <
|
|
19
|
+
|______\__,_|_| |_\_/ \__,_|_|_|\_\
|
|
20
|
+
|
|
21
|
+
LAN chat — instant, private, no internet needed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _color(text: str, code: str) -> str:
|
|
26
|
+
return f"\033[{code}m{text}\033[0m"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
print(_color(BANNER, "1;35"))
|
|
31
|
+
print(_color(f" v{VERSION} • pure asyncio • zero runtime dependencies\n", "35"))
|
|
32
|
+
|
|
33
|
+
print(" [1] Start a Server")
|
|
34
|
+
print(" [2] Join as Client")
|
|
35
|
+
print()
|
|
36
|
+
|
|
37
|
+
while True:
|
|
38
|
+
choice = input("Choose (1 or 2): ").strip()
|
|
39
|
+
if choice in ("1", "2"):
|
|
40
|
+
break
|
|
41
|
+
print(_color("Please enter 1 or 2.", "31"))
|
|
42
|
+
|
|
43
|
+
print()
|
|
44
|
+
|
|
45
|
+
if choice == "1":
|
|
46
|
+
port_in = input(f"Port (default {SERVER_PORT}): ").strip()
|
|
47
|
+
port = int(port_in) if port_in.isdigit() else SERVER_PORT
|
|
48
|
+
run_server(port=port)
|
|
49
|
+
else:
|
|
50
|
+
run_client(default_port=CLIENT_PORT)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|