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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capyy
3
- Version: 0.3.0
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.strip()}")
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
- def get(data, path, default=_NOTHING):
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 → {jsonlib.dumps(message['request'], ensure_ascii=False)}")
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.0
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.0"
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