capyy 0.3.0__tar.gz → 0.3.2__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.
- {capyy-0.3.0/capyy.egg-info → capyy-0.3.2}/PKG-INFO +3 -1
- {capyy-0.3.0 → capyy-0.3.2}/README.md +2 -0
- {capyy-0.3.0 → capyy-0.3.2}/capy/__init__.py +32 -11
- {capyy-0.3.0 → capyy-0.3.2}/capy/network.py +3 -2
- {capyy-0.3.0 → capyy-0.3.2}/capy/wifi.py +2 -1
- {capyy-0.3.0 → capyy-0.3.2/capyy.egg-info}/PKG-INFO +3 -1
- {capyy-0.3.0 → capyy-0.3.2}/pyproject.toml +1 -1
- {capyy-0.3.0 → capyy-0.3.2}/tests/test_capy.py +6 -0
- {capyy-0.3.0 → capyy-0.3.2}/LICENSE +0 -0
- {capyy-0.3.0 → capyy-0.3.2}/capy/_tls.py +0 -0
- {capyy-0.3.0 → capyy-0.3.2}/capyy.egg-info/SOURCES.txt +0 -0
- {capyy-0.3.0 → capyy-0.3.2}/capyy.egg-info/dependency_links.txt +0 -0
- {capyy-0.3.0 → capyy-0.3.2}/capyy.egg-info/top_level.txt +0 -0
- {capyy-0.3.0 → capyy-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capyy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet
|
|
5
5
|
Author: Izac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,6 +81,8 @@ Everything is encrypted by default, with nothing to configure.
|
|
|
81
81
|
and prints its **fingerprint**.
|
|
82
82
|
- Like SSH, a client **remembers a server's fingerprint** the first time (`~/.capy/known_hosts.json`)
|
|
83
83
|
and raises `capy.SecurityError` if it ever changes.
|
|
84
|
+
- The server logs every request, but **never secrets**: parameters whose name contains
|
|
85
|
+
`password`, `pwd`, `mdp`, `secret` or `token` are shown as `"***"`.
|
|
84
86
|
- Use `tls=False` on **both** sides to talk in clear (faster on localhost).
|
|
85
87
|
|
|
86
88
|
## `capy.wifi` — find each other on the same network
|
|
@@ -59,6 +59,8 @@ Everything is encrypted by default, with nothing to configure.
|
|
|
59
59
|
and prints its **fingerprint**.
|
|
60
60
|
- Like SSH, a client **remembers a server's fingerprint** the first time (`~/.capy/known_hosts.json`)
|
|
61
61
|
and raises `capy.SecurityError` if it ever changes.
|
|
62
|
+
- The server logs every request, but **never secrets**: parameters whose name contains
|
|
63
|
+
`password`, `pwd`, `mdp`, `secret` or `token` are shown as `"***"`.
|
|
62
64
|
- Use `tls=False` on **both** sides to talk in clear (faster on localhost).
|
|
63
65
|
|
|
64
66
|
## `capy.wifi` — find each other on the same network
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import json as jsonlib # "json" est pris par capy.json()
|
|
4
4
|
import socket
|
|
5
5
|
import threading
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
from . import _tls
|
|
8
9
|
from ._tls import SecurityError # noqa: F401 (capy.SecurityError)
|
|
@@ -53,7 +54,7 @@ class Server:
|
|
|
53
54
|
conn.sendall((jsonlib.dumps(self.handle(line), ensure_ascii=False) + "\n").encode())
|
|
54
55
|
except (OSError, ValueError):
|
|
55
56
|
return
|
|
56
|
-
print(f"{addr[0]} → {line
|
|
57
|
+
print(f"{addr[0]} → {_hide(line)}")
|
|
57
58
|
|
|
58
59
|
# port 0 -> le système choisit un port libre
|
|
59
60
|
def run(self, port=None):
|
|
@@ -75,8 +76,27 @@ class Server:
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
# Renvoyer du JSON écrit en texte depuis une action
|
|
79
|
+
# Paramètres jamais affichés dans le journal du serveur (mots de passe, tokens…)
|
|
80
|
+
SECRETS = ("password", "passwd", "pwd", "mdp", "motdepasse", "secret", "token")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Une requête -> texte pour le journal, avec les secrets remplacés par "***"
|
|
84
|
+
# Ex : {"action": "login", "password": "1234"} -> {"action": "login", "password": "***"}
|
|
85
|
+
def _hide(line):
|
|
86
|
+
try:
|
|
87
|
+
request = jsonlib.loads(line) if isinstance(line, str) else dict(line)
|
|
88
|
+
except (ValueError, TypeError):
|
|
89
|
+
return str(line).strip()
|
|
90
|
+
if not isinstance(request, dict):
|
|
91
|
+
return str(line).strip()
|
|
92
|
+
for key in request:
|
|
93
|
+
if any(word in key.lower() for word in SECRETS):
|
|
94
|
+
request[key] = "***"
|
|
95
|
+
return jsonlib.dumps(request, ensure_ascii=False)
|
|
96
|
+
|
|
97
|
+
|
|
78
98
|
# Ex : return capy.json('{"text": "Hello"}')
|
|
79
|
-
def json(text):
|
|
99
|
+
def json(text) -> Any:
|
|
80
100
|
try:
|
|
81
101
|
return jsonlib.loads(text)
|
|
82
102
|
except ValueError as e:
|
|
@@ -94,7 +114,8 @@ _NOTHING = object() # Pour savoir si default a été donné (None est une vraie
|
|
|
94
114
|
# Ex : get('{"text": "Hello"}', "text") -> "Hello"
|
|
95
115
|
# get(reponse, "users.0.name") -> "Izac" (0 = 1er élément d'une liste)
|
|
96
116
|
# get(reponse, "age", default=0) -> 0 si "age" n'existe pas (au lieu d'une erreur)
|
|
97
|
-
|
|
117
|
+
# -> Any : la valeur peut être de n'importe quel type (sinon Pylance croit que c'est un object)
|
|
118
|
+
def get(data, path, default=_NOTHING) -> Any:
|
|
98
119
|
value = json(data) if isinstance(data, str) else data
|
|
99
120
|
for key in str(path).split("."):
|
|
100
121
|
if isinstance(value, dict) and key in value:
|
|
@@ -131,7 +152,7 @@ def _error(name, data, target):
|
|
|
131
152
|
# Du JSON (dict, liste...) -> texte JSON sur une ligne (un texte reste tel quel)
|
|
132
153
|
# Ex : to_str({"text": "Hello"}) -> '{"text": "Hello"}'
|
|
133
154
|
# to_str('{"text": "Hello"}', "text") -> "Hello"
|
|
134
|
-
def to_str(data, key=None):
|
|
155
|
+
def to_str(data, key=None) -> str:
|
|
135
156
|
if key is not None:
|
|
136
157
|
data = get(data, key)
|
|
137
158
|
if isinstance(data, str):
|
|
@@ -144,7 +165,7 @@ def to_str(data, key=None):
|
|
|
144
165
|
|
|
145
166
|
# -> entier. Accepte 42, 42.0, "42", " 42 "
|
|
146
167
|
# Ex : to_int("42") -> 42 / to_int("abc") -> ValueError
|
|
147
|
-
def to_int(data, key=None):
|
|
168
|
+
def to_int(data, key=None) -> int:
|
|
148
169
|
if key is not None:
|
|
149
170
|
data = get(data, key)
|
|
150
171
|
if isinstance(data, bool): # True est un int pour Python, mais pas pour nous
|
|
@@ -163,7 +184,7 @@ def to_int(data, key=None):
|
|
|
163
184
|
|
|
164
185
|
# -> nombre à virgule. Accepte 4.5, 4, "4.5"
|
|
165
186
|
# Ex : to_float("4.5") -> 4.5
|
|
166
|
-
def to_float(data, key=None):
|
|
187
|
+
def to_float(data, key=None) -> float:
|
|
167
188
|
if key is not None:
|
|
168
189
|
data = get(data, key)
|
|
169
190
|
if not isinstance(data, bool) and isinstance(data, (int, float, str)):
|
|
@@ -176,7 +197,7 @@ def to_float(data, key=None):
|
|
|
176
197
|
|
|
177
198
|
# -> True / False. Accepte true/false, 1/0, "true"/"false", "oui"/"non", "1"/"0"
|
|
178
199
|
# Ex : to_bool("oui") -> True
|
|
179
|
-
def to_bool(data, key=None):
|
|
200
|
+
def to_bool(data, key=None) -> bool:
|
|
180
201
|
if key is not None:
|
|
181
202
|
data = get(data, key)
|
|
182
203
|
if isinstance(data, bool):
|
|
@@ -194,7 +215,7 @@ def to_bool(data, key=None):
|
|
|
194
215
|
|
|
195
216
|
# -> liste. Accepte une liste, un tuple, ou du texte JSON '[1, 2]'
|
|
196
217
|
# Ex : to_list("[1, 2]") -> [1, 2]
|
|
197
|
-
def to_list(data, key=None):
|
|
218
|
+
def to_list(data, key=None) -> list:
|
|
198
219
|
if key is not None:
|
|
199
220
|
data = get(data, key)
|
|
200
221
|
if isinstance(data, (list, tuple)):
|
|
@@ -211,7 +232,7 @@ def to_list(data, key=None):
|
|
|
211
232
|
|
|
212
233
|
# -> dict. Accepte un dict, ou du texte JSON '{"a": 1}'
|
|
213
234
|
# Ex : to_dict('{"a": 1}') -> {"a": 1}
|
|
214
|
-
def to_dict(data, key=None):
|
|
235
|
+
def to_dict(data, key=None) -> dict:
|
|
215
236
|
if key is not None:
|
|
216
237
|
data = get(data, key)
|
|
217
238
|
if isinstance(data, dict):
|
|
@@ -229,7 +250,7 @@ def to_dict(data, key=None):
|
|
|
229
250
|
# Client : capy.call("127.0.0.1", 4000, "get", id=2) -> dict
|
|
230
251
|
# Lève ConnectionError si le serveur ne répond pas, SecurityError si ce n'est pas le bon serveur
|
|
231
252
|
# tls=False : pour un serveur pas chiffré (Server(..., tls=False))
|
|
232
|
-
def call(host, port, action, /, tls=True, **params):
|
|
253
|
+
def call(host, port, action, /, tls=True, **params) -> Any:
|
|
233
254
|
return _call(host, port, action, params, tls, label=f"{host}:{port}")
|
|
234
255
|
|
|
235
256
|
|
|
@@ -270,6 +291,6 @@ def _call(host, port, action, params, tls, label, who="capy"):
|
|
|
270
291
|
|
|
271
292
|
# Empreinte du certificat des serveurs capy de cette machine (à donner aux clients qui veulent vérifier)
|
|
272
293
|
# Ex : capy.fingerprint() -> "9dfa7b98…"
|
|
273
|
-
def fingerprint():
|
|
294
|
+
def fingerprint() -> str:
|
|
274
295
|
_tls.server_context("server") # Crée le certificat s'il n'existe pas encore
|
|
275
296
|
return _tls.my_fingerprint("server")
|
|
@@ -20,6 +20,7 @@ import socket
|
|
|
20
20
|
import sys
|
|
21
21
|
import threading
|
|
22
22
|
import time
|
|
23
|
+
from typing import Any
|
|
23
24
|
|
|
24
25
|
import capy
|
|
25
26
|
from capy import _tls
|
|
@@ -366,7 +367,7 @@ class Server(capy.Server):
|
|
|
366
367
|
channel.send({"id": message.get("id"), "response": response})
|
|
367
368
|
except ConnectionError:
|
|
368
369
|
return
|
|
369
|
-
_log(f"réseau → {
|
|
370
|
+
_log(f"réseau → {capy._hide(message['request'])}")
|
|
370
371
|
|
|
371
372
|
|
|
372
373
|
# ============================================================
|
|
@@ -476,7 +477,7 @@ def _call_once(host, port, message):
|
|
|
476
477
|
# Appeler une action d'un serveur par son nom, via le relais (comme capy.call)
|
|
477
478
|
# La connexion chiffrée reste ouverte : seul le 1er appel paie la connexion TLS
|
|
478
479
|
# Ex : capy.network.call("maison", "test") -> {"text": "Hello"}
|
|
479
|
-
def call(name, action, /, **params):
|
|
480
|
+
def call(name, action, /, **params) -> Any:
|
|
480
481
|
host, port = _address(None)
|
|
481
482
|
message = {"capy": "call", "to": name, "key": _config["key"], "request": {"action": action, **params}}
|
|
482
483
|
reply = None
|
|
@@ -6,6 +6,7 @@ import json as jsonlib
|
|
|
6
6
|
import socket
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
import capy
|
|
11
12
|
|
|
@@ -87,7 +88,7 @@ def find(name, timeout=1):
|
|
|
87
88
|
# Appeler une action d'un serveur par son nom (comme capy.call, sans IP ni port)
|
|
88
89
|
# tls=False : pour un serveur pas chiffré (Server(..., tls=False))
|
|
89
90
|
# Ex : capy.wifi.call("maison", "test") -> {"text": "Hello"}
|
|
90
|
-
def call(name, action, /, tls=True, **params):
|
|
91
|
+
def call(name, action, /, tls=True, **params) -> Any:
|
|
91
92
|
for attempt in range(2):
|
|
92
93
|
host, port = find(name)
|
|
93
94
|
if _secure.get(name, True) != tls:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capyy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet
|
|
5
5
|
Author: Izac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,6 +81,8 @@ Everything is encrypted by default, with nothing to configure.
|
|
|
81
81
|
and prints its **fingerprint**.
|
|
82
82
|
- Like SSH, a client **remembers a server's fingerprint** the first time (`~/.capy/known_hosts.json`)
|
|
83
83
|
and raises `capy.SecurityError` if it ever changes.
|
|
84
|
+
- The server logs every request, but **never secrets**: parameters whose name contains
|
|
85
|
+
`password`, `pwd`, `mdp`, `secret` or `token` are shown as `"***"`.
|
|
84
86
|
- Use `tls=False` on **both** sides to talk in clear (faster on localhost).
|
|
85
87
|
|
|
86
88
|
## `capy.wifi` — find each other on the same network
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "capyy"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -68,6 +68,12 @@ class TestHandle(unittest.TestCase):
|
|
|
68
68
|
self.assertEqual(self.api.handle('{"action": "divide", "a": 1, "b": 0}'), {"erreur": "division by zero"})
|
|
69
69
|
self.assertIn("erreur", self.api.handle('{"action": "divide"}'))
|
|
70
70
|
|
|
71
|
+
def test_hide_secrets(self):
|
|
72
|
+
line = '{"action": "login", "name": "Zoe", "password": "1234", "user_token": "abc"}'
|
|
73
|
+
self.assertEqual(capy._hide(line), '{"action": "login", "name": "Zoe", "password": "***", "user_token": "***"}')
|
|
74
|
+
self.assertEqual(capy._hide({"mdp": "x"}), '{"mdp": "***"}')
|
|
75
|
+
self.assertEqual(capy._hide("pas du json\n"), "pas du json")
|
|
76
|
+
|
|
71
77
|
|
|
72
78
|
class TestValues(unittest.TestCase):
|
|
73
79
|
data = {"city": "Paris", "temp": "18.5", "rain": "non", "days": [{"name": "lundi"}, {"name": "mardi"}]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|