traffic-twin 0.1.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.
Files changed (54) hide show
  1. traffic_twin-0.1.0/LICENSE +202 -0
  2. traffic_twin-0.1.0/PKG-INFO +293 -0
  3. traffic_twin-0.1.0/README.md +263 -0
  4. traffic_twin-0.1.0/pyproject.toml +44 -0
  5. traffic_twin-0.1.0/setup.cfg +4 -0
  6. traffic_twin-0.1.0/src/traffic_twin/__init__.py +15 -0
  7. traffic_twin-0.1.0/src/traffic_twin/__main__.py +40 -0
  8. traffic_twin-0.1.0/src/traffic_twin/_log.py +38 -0
  9. traffic_twin-0.1.0/src/traffic_twin/_version.py +2 -0
  10. traffic_twin-0.1.0/src/traffic_twin/agent.py +819 -0
  11. traffic_twin-0.1.0/src/traffic_twin/auto.py +26 -0
  12. traffic_twin-0.1.0/src/traffic_twin/autocapture.py +445 -0
  13. traffic_twin-0.1.0/src/traffic_twin/bodies.py +660 -0
  14. traffic_twin-0.1.0/src/traffic_twin/bootstrap/sitecustomize.py +33 -0
  15. traffic_twin-0.1.0/src/traffic_twin/capture/__init__.py +1 -0
  16. traffic_twin-0.1.0/src/traffic_twin/capture/asgi.py +233 -0
  17. traffic_twin-0.1.0/src/traffic_twin/capture/common.py +139 -0
  18. traffic_twin-0.1.0/src/traffic_twin/capture/outbound.py +867 -0
  19. traffic_twin-0.1.0/src/traffic_twin/capture/routes.py +80 -0
  20. traffic_twin-0.1.0/src/traffic_twin/capture/wsgi.py +376 -0
  21. traffic_twin-0.1.0/src/traffic_twin/conformance.py +79 -0
  22. traffic_twin-0.1.0/src/traffic_twin/context.py +128 -0
  23. traffic_twin-0.1.0/src/traffic_twin/ed25519.py +147 -0
  24. traffic_twin-0.1.0/src/traffic_twin/event.py +95 -0
  25. traffic_twin-0.1.0/src/traffic_twin/exporter.py +269 -0
  26. traffic_twin-0.1.0/src/traffic_twin/fleet.py +350 -0
  27. traffic_twin-0.1.0/src/traffic_twin/formats.py +138 -0
  28. traffic_twin-0.1.0/src/traffic_twin/gqlshape.py +395 -0
  29. traffic_twin-0.1.0/src/traffic_twin/hostpat.py +127 -0
  30. traffic_twin-0.1.0/src/traffic_twin/jsonlex.py +366 -0
  31. traffic_twin-0.1.0/src/traffic_twin/policy.py +937 -0
  32. traffic_twin-0.1.0/src/traffic_twin/py.typed +0 -0
  33. traffic_twin-0.1.0/src/traffic_twin/runtime.py +158 -0
  34. traffic_twin-0.1.0/src/traffic_twin/sanitizer.py +1402 -0
  35. traffic_twin-0.1.0/src/traffic_twin/store.py +191 -0
  36. traffic_twin-0.1.0/src/traffic_twin/textutil.py +60 -0
  37. traffic_twin-0.1.0/src/traffic_twin/vectors.py +131 -0
  38. traffic_twin-0.1.0/src/traffic_twin.egg-info/PKG-INFO +293 -0
  39. traffic_twin-0.1.0/src/traffic_twin.egg-info/SOURCES.txt +52 -0
  40. traffic_twin-0.1.0/src/traffic_twin.egg-info/dependency_links.txt +1 -0
  41. traffic_twin-0.1.0/src/traffic_twin.egg-info/top_level.txt +1 -0
  42. traffic_twin-0.1.0/tests/test_asgi.py +171 -0
  43. traffic_twin-0.1.0/tests/test_autocapture.py +302 -0
  44. traffic_twin-0.1.0/tests/test_bench.py +33 -0
  45. traffic_twin-0.1.0/tests/test_bodies.py +215 -0
  46. traffic_twin-0.1.0/tests/test_fleet.py +446 -0
  47. traffic_twin-0.1.0/tests/test_fork.py +56 -0
  48. traffic_twin-0.1.0/tests/test_hosts.py +209 -0
  49. traffic_twin-0.1.0/tests/test_install.py +164 -0
  50. traffic_twin-0.1.0/tests/test_otel.py +74 -0
  51. traffic_twin-0.1.0/tests/test_outbound.py +353 -0
  52. traffic_twin-0.1.0/tests/test_safety.py +79 -0
  53. traffic_twin-0.1.0/tests/test_units.py +206 -0
  54. traffic_twin-0.1.0/tests/test_wsgi.py +201 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: traffic-twin
3
+ Version: 0.1.0
4
+ Summary: traffic-twin capture SDK for Python: sanitizes HTTP exchanges in process and exports them to tt-core over mTLS.
5
+ Author: traffic-twin maintainers
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/ivseb/traffic-twin
8
+ Project-URL: Documentation, https://github.com/ivseb/traffic-twin/tree/main/sdk/python#readme
9
+ Project-URL: Repository, https://github.com/ivseb/traffic-twin
10
+ Project-URL: Issues, https://github.com/ivseb/traffic-twin/issues
11
+ Project-URL: Changelog, https://github.com/ivseb/traffic-twin/blob/main/CHANGELOG.md
12
+ Keywords: traffic-twin,http,capture,sanitizer,opentelemetry,mtls
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
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.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Dynamic: license-file
30
+
31
+ # traffic-twin (Python)
32
+
33
+ SDK di cattura di traffic-twin per Python ≥ 3.9. Osserva le chiamate HTTP in ingresso
34
+ (Flask, FastAPI/Starlette, Django, qualunque app WSGI o ASGI) e in uscita
35
+ (`requests`, `httpx`, `urllib`, `http.client`), le **sanifica nel processo** secondo la
36
+ policy firmata (specifica `docs/spec/sanitizer.md`, fino alla v1.19) e invia a `tt-core` solo forme,
37
+ token HMAC e valori ammessi dalla policy, come log OTLP/HTTP JSON su mTLS.
38
+
39
+ - **Zero dipendenze a runtime**: solo la libreria standard (`pyproject.toml` ha
40
+ `dependencies = []`). Né `opentelemetry-*` né librerie crittografiche: la verifica delle
41
+ firme Ed25519 (RFC 8032) è scritta in Python puro (`traffic_twin/ed25519.py`, provata con i
42
+ vettori della RFC).
43
+ - **Non modifica il traffico dell'app**: nessun `traceparent` iniettato; l'unica eccezione,
44
+ opzionale e decisa dalla policy, è il membro di baggage `tt.s` verso gli host di
45
+ `sampling.propagate_to` (mai verso un host esterno della policy). Il membro è messo **in testa**
46
+ al baggage (chi legge prende il primo `tt.s` valido): un `tt.s` falsificato da un client e
47
+ inoltrato dall'app non può mai prevalere.
48
+ - **Non rallenta l'app**: sulla strada della richiesta ci sono solo la decisione di
49
+ campionamento e la copia dei byte del corpo; sanificazione, serializzazione e invio
50
+ avvengono in thread in background con code limitate. Quando le code sono piene si scartano
51
+ gli eventi più vecchi (e si contano), mai le richieste.
52
+ - **Fail-closed**: senza policy valida, chiave HMAC o primo heartbeat non cattura nulla;
53
+ l'app continua a funzionare normalmente. Un errore interno del sanitizer scarta lo
54
+ scambio e incrementa `sanitizer_errors`.
55
+
56
+ ## Avvio senza codice
57
+
58
+ ```bash
59
+ pip install ./traffic_twin-x.y.z-py3-none-any.whl # dal kit; dal registro interno: pip install traffic-twin
60
+ export TT_ENROLL_URL=https://tt-core.internal:4318
61
+ export TT_ENROLL_TOKEN=tte_… # token di iscrizione (contiene l'impronta della CA)
62
+ export TT_HMAC_KEY_FILE=/run/secrets/tt-hmac-key
63
+ export TT_SERVICE_NAME=shop-api
64
+ python -m traffic_twin run -- uvicorn app:app # o gunicorn app:app, flask --app app run, python manage.py runserver, …
65
+ ```
66
+
67
+ Il comando cattura da solo **sia le chiamate in ingresso sia quelle in uscita**, senza toccare il codice
68
+ (`traffic_twin/autocapture.py`): la prima volta che un modulo noto viene importato, una piccola patch fa sì che
69
+ gli oggetti che crea catturino il traffico.
70
+
71
+ | Cosa | Come si avvolge |
72
+ |---|---|
73
+ | FastAPI / Starlette | `build_middleware_stack` restituisce lo stack avvolto nel middleware ASGI (una volta per app, mai se l'app ha già `install()` o il middleware) |
74
+ | Flask | `Flask.__init__` avvolge `wsgi_app` |
75
+ | Django | `get_wsgi_application()` / `get_asgi_application()` restituiscono l'oggetto avvolto |
76
+ | uvicorn (`uvicorn app:app`, `uvicorn.run(app)`) | l'app caricata, se è una semplice funzione/oggetto ASGI (o WSGI con `--interface wsgi`) che nessuna patch di framework copre; con `--factory` l'app che la factory crea è avvolta solo se è Starlette/FastAPI/Flask |
77
+ | gunicorn (WSGI o `-k uvicorn.workers.UvicornWorker`) | l'app che `gunicorn.util.import_app` restituisce, se è una semplice funzione/oggetto WSGI o ASGI |
78
+ | `requests`, `httpx`, `urllib` | in uscita, appena `requests`/`httpx` sono importati (`TT_OUTBOUND=auto`, il default di questo avvio); `TT_OUTBOUND=off` spegne la parte in uscita, una lista (`httpx`, `requests,urllib`, `http.client`) sceglie quali |
79
+
80
+ Un'app montata dentro un'altra già catturata (Starlette `Mount`, `DispatcherMiddleware`) non si cattura due volte.
81
+
82
+ **Quando non si riesce ad avvolgere nessuna app** (un server o un framework che non si conosce) l'SDK non resta in
83
+ silenzio: dopo 20 secondi scrive **una volta sola** su stderr `inbound not captured: ... add traffic_twin.install(app)`
84
+ e segnala a `tt-core` l'errore `inbound_not_captured` nell'heartbeat; la console mostra sulla sorgente il problema
85
+ **Ingresso non catturato** (e, senza ingresso, non c'è `identity.subject`). Rimedio: `app = traffic_twin.install(app)`
86
+ (sotto). Il controllo si ripete ogni 5 minuti finché nessuna app è avvolta.
87
+
88
+ `python -m traffic_twin run -- <comando>` mette `traffic_twin/bootstrap/sitecustomize.py` in
89
+ testa a `PYTHONPATH`: Python lo importa all'avvio, esso esegue prima l'eventuale
90
+ `sitecustomize` già presente nell'app e poi avvia l'agente. L'agente parte in background:
91
+ iscrizione, heartbeat e download della policy non ritardano l'avvio dell'app. Senza
92
+ `TT_ENROLL_URL` l'SDK resta inerte.
93
+
94
+ Alternative equivalenti: `import traffic_twin.auto` (una riga all'inizio dell'app) oppure
95
+ `traffic_twin.start()`.
96
+
97
+ | Variabile | Significato |
98
+ |---|---|
99
+ | `TT_ENROLL_URL` | URL del listener Fleet/intake di `tt-core` (sempre `https`) |
100
+ | `TT_ENROLL_TOKEN` | Token di iscrizione `tte_<segreto>.<impronta CA>`, monouso o di gruppo |
101
+ | `TT_HMAC_KEY` / `TT_HMAC_KEY_FILE` | Chiave HMAC locale, una o più righe `<kid>:<base64url>` (§7.1). Non viaggia mai verso `tt-core` |
102
+ | `TT_SERVICE_NAME` | Nome di servizio richiesto all'iscrizione (altrimenti `OTEL_SERVICE_NAME` in minuscolo). Il nome usato negli eventi è quello **assegnato** da `tt-core` |
103
+ | `TT_STATE_DIR` | Cartella dello stato locale (default `~/.traffic-twin/<id>`): certificato, chiave privata, CA, policy firmata, interruttore. Cartella `0700`, file `0600`. Se non è scrivibile, o è di un altro utente, si usa una cartella temporanea privata (l'iscrizione non sopravvive al riavvio); una cartella propria più larga (anche `~/.traffic-twin`) viene riportata a `0700`. I file di stato si leggono solo se regolari, dell'utente e non scrivibili da gruppo/altri (`state_file_untrusted`). L'interruttore di emergenza scarta anche i batch in retry e gli scambi in sanificazione (generazione) |
104
+ | `TT_OUTBOUND` | Hook in uscita **globali** da attivare, separati da virgola: `requests`, `httpx`, `urllib`, `http.client`, oppure `auto` (requests/httpx se già importati, più urllib). Con `python -m traffic_twin run` e `import traffic_twin.auto` il default è `auto` applicato **all'import** di `requests`/`httpx` (`off` lo spegne); con `traffic_twin.start()` / `install()` il default è nessuno (vedi sotto) |
105
+ | `TT_ENROLL_CA_FILE` | PEM della CA radice. Obbligatorio solo su Python 3.9, che non sa leggere la catena presentata dal server: l'impronta del token viene verificata su questo file |
106
+ | `TT_DISABLED` | `1` disattiva completamente l'SDK (nessun thread, nessun hook) |
107
+ | `TT_DEBUG` | `1` scrive su stderr solo codici di stato ed errore (mai valori) |
108
+ | `TT_HEARTBEAT_INTERVAL_MS`, `TT_POLICY_POLL_INTERVAL_MS`, `TT_EXPORT_FLUSH_MS`, `TT_INBOUND_GRACE_MS` | Solo per test: sostituiscono gli intervalli ricevuti da `tt-core` e l'attesa (20 s) prima dell'avviso `inbound not captured` |
109
+
110
+ ## Installazione nelle app
111
+
112
+ ```python
113
+ import traffic_twin
114
+
115
+ # Flask (modifica app.wsgi_app sul posto)
116
+ app = traffic_twin.install(app)
117
+
118
+ # FastAPI / Starlette (add_middleware, il più esterno)
119
+ app = traffic_twin.install(app)
120
+
121
+ # Django: si installa sull'oggetto WSGI/ASGI e si riassegna
122
+ application = traffic_twin.install(get_wsgi_application()) # oppure get_asgi_application()
123
+
124
+ # qualunque app WSGI o ASGI (rilevata dalla firma; kind="wsgi"|"asgi" per forzare)
125
+ application = traffic_twin.install(application)
126
+ ```
127
+
128
+ `install()` è idempotente: su un'app già avvolta (da un'altra `install()` o dal comando `run`) non fa nulla. Avvia l'agente dall'ambiente (o dai parametri: `install(app, enroll_url=…,
129
+ service_name=…, outbound="requests,httpx")`) e **non lancia mai eccezioni**: se non riesce a
130
+ installarsi emette un `RuntimeWarning` e lascia l'app com'è. Si possono usare anche i
131
+ middleware direttamente: `traffic_twin.capture.asgi.ASGIMiddleware`,
132
+ `traffic_twin.capture.wsgi.WSGIMiddleware`.
133
+
134
+ ### `framework_route` (§3.3, §3.4)
135
+
136
+ Il template della route si risolve **dopo** la risposta, nel thread di sanificazione, chiedendo
137
+ al router del framework quale route corrisponde alla richiesta (senza eseguire codice
138
+ dell'app), sempre completo dei prefissi di montaggio:
139
+
140
+ | Framework | Fonte | Note |
141
+ |---|---|---|
142
+ | Flask | `app.url_map.bind_to_environ(...).match(return_rule=True)` | Blueprint e `SCRIPT_NAME` inclusi; `<path:x>` non si normalizza |
143
+ | FastAPI / Starlette | `route.matches(scope)` sul router, anche dentro `Mount` | `{x:path}` non si normalizza |
144
+ | Django | `django.urls.resolve(...).route` | `re_path` non si normalizza: vale il template della policy |
145
+
146
+ Se la route non si può ricostruire non si passa `framework_route` e vale il punto 2 di §3.3.
147
+
148
+ ## Chiamate in uscita
149
+
150
+ Prima i **punti di estensione**, senza patch globali:
151
+
152
+ ```python
153
+ import requests, httpx, urllib.request
154
+ from traffic_twin.capture.outbound import make_requests_adapter, make_httpx_transports, make_urllib_handler
155
+
156
+ s = requests.Session(); s.mount("https://", make_requests_adapter()())
157
+ T, AT = make_httpx_transports()
158
+ c = httpx.Client(transport=T(httpx.HTTPTransport())) # sincrono
159
+ a = httpx.AsyncClient(transport=AT(httpx.AsyncHTTPTransport())) # asincrono
160
+ opener = urllib.request.build_opener(make_urllib_handler()())
161
+ ```
162
+
163
+ Per le app che costruiscono i client altrove, una patch è inevitabile: è **opt-in**
164
+ (`TT_OUTBOUND` o `outbound=`) e **reversibile** (ogni `install_*` di
165
+ `traffic_twin.capture.outbound` restituisce la funzione che ripristina gli originali):
166
+
167
+ | Valore | Cosa fa |
168
+ |---|---|
169
+ | `requests` | le nuove `requests.Session` ricevono l'adattatore (usa il punto di estensione) |
170
+ | `httpx` | avvolge `handle_request` / `handle_async_request` dei trasporti predefiniti |
171
+ | `urllib` | installa un opener predefinito con l'handler (`install_opener`, API ufficiale) |
172
+ | `http.client` | patch di `HTTPConnection` (`putrequest`, `putheader`, `endheaders`, `send`, `getresponse`): non esiste un punto di estensione a questo livello. Copre anche `urllib3`/`requests` e tutto ciò che sta sopra `http.client` |
173
+
174
+ Se due livelli vedono la stessa chiamata (adattatore `requests` e patch `http.client`) la
175
+ registra solo il più esterno (`contextvars`). Le chiamate fatte dall'SDK stesso (Fleet,
176
+ intake) non si catturano mai.
177
+
178
+ I corpi si copiano mentre l'app li legge (tee con limite `max_body_bytes + 1`, il resto si
179
+ conta): il corpo di una risposta che l'app non legge fino alla fine esce come `trunc:size`.
180
+ La codifica `gzip`/`deflate` la decodifica il sanitizer (§9.0), partendo dai byte ancora
181
+ codificati.
182
+
183
+ ### Host non dichiarati e scoperta automatica (spec v1.5)
184
+
185
+ Un host esterno che nessuna `base` della policy né `segments.hosts_allow` elenca non esce
186
+ più come `host:unlisted`: con `hosts.discovery: auto` (default, anche quando il campo
187
+ `hosts` manca) il `service` dell'evento è `host:` + **pattern** (sanitizer.md §3.5). Il
188
+ pattern è l'host in minuscolo, senza porta di default e senza un punto finale, in cui ogni
189
+ etichetta resta letterale solo se è `^[a-z][a-z0-9-]{0,62}$`, non ha sei cifre consecutive
190
+ né dodici esadecimali consecutivi, altrimenti diventa `{*}` (`t-3912345678.tenant.example` →
191
+ `host:{*}.tenant.example`). I letterali IP escono solo come `ip:private` / `ip:public`
192
+ (tabella chiusa di §3.6, parser IPv6 proprio in `traffic_twin/hostpat.py`: né `ipaddress` né
193
+ `is_private`), con la porta se non di default; un'etichetta vuota dà `host:invalid`. Gli
194
+ adattatori passano l'host così come lo vede il client (IPv6 tra parentesi, nessun IDN,
195
+ nessun punto tolto, nessun `str.lower()` Unicode).
196
+
197
+ `hosts.discovery: off` è la **modalità stretta**: gli host non dichiarati escono come
198
+ `host:unlisted`, byte per byte come prima della v1.5. Lo imposta la policy (`tt-core`:
199
+ `auto` → `off` chiude, `off` → `auto` apre, con le approvazioni normali); l'SDK non ha
200
+ opzioni locali. Il dominio registrabile e la cardinalità delle etichette sono controlli di
201
+ `tt-core` (zona B): l'SDK non li implementa.
202
+
203
+ ### Corpi XML e SOAP, multipart, documenti e corpi grandi (spec v1.19)
204
+
205
+ Con l'oggetto `bodies` della policy (sanitizer.md §22; assente = tutto spento, uscita
206
+ identica alle versioni precedenti) il sanitizer analizza anche:
207
+
208
+ - **XML e SOAP** (`application/xml`, `text/xml`, `*+xml`): lettore XML scritto a mano
209
+ (`traffic_twin/bodies.py`), sottoinsieme rigoroso di XML 1.0 + Namespaces, **senza**
210
+ `xml.etree`/`expat`: nessuna DTD, nessuna entità oltre alle cinque predefinite e ai
211
+ riferimenti numerici, iterativo e con al più 256 elementi annidati (`trunc:invalid-xml`
212
+ altrimenti). Escono anche `fmt` (`xml`/`soap11`/`soap12`), `xmlns` e, per la richiesta,
213
+ `soap_action`; le regole usano i passi `.@nome` e `.#text`.
214
+ - **`multipart/form-data`**: i campi come un form, le parti file sempre come
215
+ `drop:file:<tipo>:<classe>` (`trunc:invalid-multipart` se il corpo non è conforme).
216
+ - **Documenti base64** (`bodies.docs`): `drop:doc:<tipo>:<classe>` al posto della classe di
217
+ lunghezza.
218
+ - **Corpi grandi** (`bodies.max_large_body_bytes`): la copia del corpo conserva
219
+ `max(limits.max_body_bytes, bodies.max_large_body_bytes) + 1` byte (WSGI, ASGI e client
220
+ in uscita) e gli array si tagliano a `large_array` / `large_nested_array`.
221
+
222
+ L'azione `code` accetta anche la forma `{"action":"code","shape":"alnum"}` (§5.6).
223
+
224
+ ## Contesto, OpenTelemetry e campionamento
225
+
226
+ - Un `contextvars.ContextVar` lega ogni chiamata in uscita allo scambio in ingresso (stesso
227
+ `trace_id`, `parent_span_id` = `span_id` dell'evento in ingresso, decisione di campionamento
228
+ **ereditata**): vale anche per `asyncio.create_task` e per i thread dei pool che copiano il
229
+ contesto (Starlette/anyio). I thread avviati a mano dall'app non ereditano il contesto: lì
230
+ valgono le regole "job" di §12 (trace OpenTelemetry, poi caso).
231
+ - Si legge `traceparent`. Se `opentelemetry-api` è importabile se ne legge **solo** il contesto
232
+ dello span corrente (`trace.get_current_span().get_span_context()`): l'SDK **non** crea né
233
+ registra mai provider, propagatori o contesti, e non crea span. Il test
234
+ `tests/test_otel.py` esegue la stessa app con e senza l'SDK e confronta gli span esportati
235
+ (nome, tipo, attributi, struttura), il provider e il propagatore globali.
236
+ - Campionamento per soggetto (§11, §12): `HMAC(chiave, epoch ‖ "sample" ‖ soggetto)`;
237
+ baggage `tt.s` considerato solo con `trust_baggage` su servizi `internal`; l'override
238
+ dell'heartbeat può solo **abbassare** il tasso.
239
+
240
+ ## Fleet e stato locale
241
+
242
+ - **Iscrizione**: connessione TLS senza fiducia, ricerca nella catena presentata da
243
+ `tt-core` del certificato con l'impronta SHA-256 contenuta nel token, richiesta di iscrizione
244
+ con quel certificato come unica CA. La libreria standard non sa costruire una CSR PKCS#10:
245
+ si usa il percorso **senza CSR** dell'API (`tt-core` genera la coppia di chiavi, restituisce
246
+ `client_key` una sola volta sul canale appena fissato e non la salva). La chiave si scrive
247
+ subito con modo `0600` in `TT_STATE_DIR` (le API `ssl` di Python vogliono dei file per
248
+ `load_cert_chain`) e si riusa ai riavvii. Più processi (gunicorn con più worker) condividono
249
+ lo stato e si serializzano con un lock su file: un token monouso si consuma una volta sola.
250
+ - **Heartbeat prima di qualsiasi cattura**; interruttore di emergenza, revoca e
251
+ `min_issued_seq` sono salvati e restano validi dopo un riavvio.
252
+ - **Policy**: firma Ed25519 su `"tt.policy.body/v1" ‖ 0x00 ‖ corpo` con chiavi fissate e non
253
+ revocate (verifica in Python puro, circa 30 ms, una volta per policy), schema completo del
254
+ corpo, coerenza busta/corpo, gruppo, `not_after`, `issued_seq` crescente e ≥
255
+ `min_issued_seq`. Una policy rifiutata non sostituisce mai la precedente valida. Nuove
256
+ chiavi di firma solo se approvate (firma di endorsement) da una chiave già fissata.
257
+ - Server con `fork` (gunicorn `--preload`, uWSGI): i thread non sopravvivono al `fork`; il
258
+ processo figlio ricrea l'agente con `os.register_at_fork`.
259
+
260
+ ## Limiti noti
261
+
262
+ - **WSGI**: `PATH_INFO` e i nomi degli header arrivano già decodificati dal server. Il path
263
+ grezzo si prende da `RAW_URI`/`REQUEST_URI` (gunicorn, uWSGI, mod_wsgi); altrimenti il path
264
+ decodificato si ricodifica (un `%2F` originale non si distingue da `/`, con `wsgiref` e
265
+ Werkzeug). I nomi degli header si ricostruiscono da `HTTP_*` con `_` → `-`: un header che
266
+ contiene davvero un underscore compare con il trattino. Più header con lo stesso nome
267
+ arrivano già uniti dal server (con `, `, anche per `Cookie`).
268
+ - **Python 3.9**: manca l'accesso alla catena TLS presentata dal server; per l'iscrizione serve
269
+ `TT_ENROLL_CA_FILE`. Da 3.10 a 3.12 si usa l'API privata `_sslobj.get_unverified_chain()`,
270
+ da 3.13 quella pubblica.
271
+ - File serviti con `wsgi.file_wrapper` / `http.response.pathsend`: i byte non passano
272
+ dall'app, la risposta esce come `trunc:size`.
273
+ - Gli errori di rete di `urllib` senza la patch `http.client` si classificano `other`; con
274
+ l'adattatore `requests` o i trasporti `httpx` si distinguono `timeout`, `refused`, `dns`,
275
+ `tls`, `reset`.
276
+ - WebSocket e altri scope ASGI non `http` non si catturano.
277
+ - I `latency_ms` includono il tempo di streaming della risposta (fino alla chiusura).
278
+
279
+ ## Sviluppo
280
+
281
+ ```bash
282
+ cd sdk/python
283
+ make vectors # python -m traffic_twin.conformance : tutti i vettori di conformità (726 con la v1.19), byte per byte
284
+ make test # unittest: vettori, Ed25519 (RFC 8032), JSON, G1, WSGI, ASGI, uscita, Fleet, OTel, install
285
+ make bench # overhead (python bench/overhead.py)
286
+ pip install -r requirements-dev.txt # solo per i test opzionali (httpx, requests, Flask, …)
287
+ ```
288
+
289
+ I test usano solo `unittest`; quelli che richiedono una dipendenza di sviluppo
290
+ (`requirements-dev.txt`, versioni fissate) si saltano se manca. Un finto `tt-core` con
291
+ intake OTLP e API Fleet su mTLS (`tests/fakecore.py`) usa una PKI costruita in Python puro
292
+ (certificati Ed25519 in DER, `tests/pki.py`): non serve il binario `openssl`. Provato su
293
+ Python 3.9, 3.11, 3.12 e 3.13.