tng-sdk 2.7.3__py3-none-win_amd64.whl

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.
bin/scripts/tng.exe ADDED
Binary file
tng/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """TNG (Trusted Network Gateway) Python SDK.
2
+
3
+ Provides encrypted HTTP requests with remote attestation by managing
4
+ a TNG subprocess (http_proxy ingress) and using each HTTP library's
5
+ native proxy support to route traffic through the encrypted tunnel.
6
+
7
+ The user only provides security options; the target address is carried
8
+ in the request URL's Host header and read by the http_proxy ingress.
9
+
10
+ Usage:
11
+ from tng import Tng
12
+
13
+ # Simplest: disable remote attestation (for testing)
14
+ tng = Tng(no_ra=True)
15
+
16
+ # With verifier
17
+ tng = Tng(
18
+ verify={
19
+ "as_addr": "http://127.0.0.1:8080/",
20
+ "policy_ids": ["default"],
21
+ },
22
+ )
23
+
24
+ # Wrap requests.Session
25
+ import requests
26
+ session = requests.Session()
27
+ tng.wrap_requests(session)
28
+ resp = session.get("http://tng-server:10001/api/data")
29
+
30
+ # Wrap httpx.Client
31
+ import httpx
32
+ client = httpx.Client()
33
+ tng.wrap_httpx(client)
34
+ resp = client.get("http://tng-server:10001/api/data")
35
+
36
+ # Wrap OpenAI client
37
+ from openai import OpenAI
38
+ openai_client = OpenAI(api_key="sk-xxx", base_url="http://tng-server:10001/v1")
39
+ tng.wrap_openai(openai_client)
40
+ completion = openai_client.chat.completions.create(model="xxx", messages=[...])
41
+ """
42
+
43
+ from tng._tng import Tng
44
+
45
+ __all__ = ["Tng"]
tng/_tng.py ADDED
@@ -0,0 +1,394 @@
1
+ """Tng class: manages a TNG subprocess (http_proxy ingress) and provides
2
+ wrapper methods for requests, httpx, and OpenAI using native proxy support."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import atexit
7
+ import copy
8
+ import json
9
+ import os
10
+ import shutil
11
+ import socket
12
+ import subprocess
13
+ import tempfile
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ __all__ = ["Tng"]
19
+
20
+
21
+ class Tng:
22
+ """High-level TNG client.
23
+
24
+ Starts the TNG binary as a subprocess with an auto-configured
25
+ ``http_proxy`` ingress, then provides ``wrap_*`` methods that use
26
+ each library's native HTTP proxy support to route traffic through
27
+ the encrypted TNG tunnel.
28
+
29
+ The user only specifies security options (``no_ra``, ``verify``,
30
+ ``attest``, ``ohttp``, ``rats_tls``). The target address is carried
31
+ in the request URL's Host header and read by the http_proxy ingress.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ no_ra: bool = False,
37
+ verify: dict[str, Any] | None = None,
38
+ attest: dict[str, Any] | None = None,
39
+ ohttp: dict[str, Any] | None = None,
40
+ rats_tls: dict[str, Any] | None = None,
41
+ ) -> None:
42
+ """Create and start a local TNG proxy.
43
+
44
+ Spawns the ``tng`` binary as a subprocess with an auto-configured
45
+ ``http_proxy`` ingress, then provides ``wrap_*`` methods that route
46
+ HTTP traffic through the encrypted tunnel.
47
+
48
+ Args:
49
+ no_ra:
50
+ Disable remote attestation. Set to ``True`` for local testing
51
+ or when the AA/AS services are unavailable.
52
+ verify:
53
+ Verifier configuration for validating the remote server's
54
+ attestation. Keys: ``as_addr`` (Attestation Service URL),
55
+ ``policy_ids`` (list of policy IDs to verify against).
56
+ attest:
57
+ Attester configuration for providing the client's own
58
+ attestation to the server. Keys: ``aa_addr`` (Attestation
59
+ Agent socket path), ``model`` (e.g. ``"passport"``),
60
+ ``as_addr`` (optional, for nested attestation).
61
+ ohttp:
62
+ OHTTP configuration dict. Omitted keys use defaults.
63
+ Common keys: ``key`` (``{"source": "self_generated",
64
+ "rotation_interval": 300}``), ``path_rewrites``.
65
+ rats_tls:
66
+ rats-TLS configuration dict as an alternative to OHTTP.
67
+ Common keys: ``multiplex`` (enable connection multiplexing).
68
+
69
+ Raises:
70
+ ValueError: If both ``ohttp`` and ``rats_tls`` are provided
71
+ (they are mutually exclusive).
72
+ FileNotFoundError: If the ``tng`` binary cannot be found.
73
+ RuntimeError: If the TNG process fails to start.
74
+ TimeoutError: If the proxy port is not ready within 30 seconds.
75
+
76
+ Example::
77
+
78
+ # Simplest: no remote attestation
79
+ tng = Tng(no_ra=True)
80
+
81
+ # With server verification
82
+ tng = Tng(verify={"as_addr": "http://127.0.0.1:8080/", "policy_ids": ["default"]})
83
+
84
+ # Using rats-TLS instead of OHTTP
85
+ tng = Tng(no_ra=True, rats_tls={"multiplex": True})
86
+
87
+ # Context manager for automatic cleanup
88
+ with Tng(no_ra=True) as tng:
89
+ tng.wrap_requests(session)
90
+ resp = session.get("http://server:10001/api/data")
91
+ """
92
+ # Validate mutual exclusivity: ohttp and rats_tls conflict
93
+ if ohttp is not None and rats_tls is not None:
94
+ raise ValueError("ohttp and rats_tls are mutually exclusive")
95
+
96
+ # Auto-assign http_proxy port
97
+ proxy_port = _find_free_port()
98
+ self._proxy_port: int = proxy_port
99
+
100
+ # Build TNG config (ingress only, no egress)
101
+ config = _build_tng_config(
102
+ proxy_port=proxy_port,
103
+ no_ra=no_ra,
104
+ verify=verify,
105
+ attest=attest,
106
+ ohttp=ohttp,
107
+ rats_tls=rats_tls,
108
+ )
109
+
110
+ # Find TNG binary
111
+ tng_bin = _find_tng_binary()
112
+
113
+ # Write config to temp JSON file
114
+ fd, config_path = tempfile.mkstemp(suffix=".json", prefix="tng_cfg_")
115
+ try:
116
+ with os.fdopen(fd, "w") as f:
117
+ json.dump(config, f)
118
+ except Exception:
119
+ os.unlink(config_path)
120
+ raise
121
+ self._config_path: str = config_path
122
+
123
+ # Start subprocess — capture stderr to a temp file for debugging
124
+ import io
125
+
126
+ stderr_path = tempfile.mkstemp(suffix=".log", prefix="tng_stderr_")[1]
127
+ self._stderr_path: str = stderr_path
128
+ self._proc: subprocess.Popen = subprocess.Popen(
129
+ [tng_bin, "launch", "--config-file", config_path],
130
+ stdout=subprocess.DEVNULL,
131
+ stderr=open(stderr_path, "w"),
132
+ )
133
+
134
+ # Wait for proxy port to be ready, checking for process failure
135
+ _wait_for_port("127.0.0.1", proxy_port, self._proc, timeout=30.0)
136
+
137
+ # Register cleanup
138
+ atexit.register(self._cleanup)
139
+
140
+ # ------------------------------------------------------------------
141
+ # Wrapper injection methods — native proxy support
142
+ # ------------------------------------------------------------------
143
+
144
+ def wrap_requests(self, session: Any) -> Any:
145
+ """Inject the TNG http_proxy into a ``requests.Session``.
146
+
147
+ After calling this, all ``session.get/post/request(...)`` calls
148
+ go through the TNG encrypted tunnel.
149
+
150
+ Args:
151
+ session: A ``requests.Session`` instance.
152
+
153
+ Returns:
154
+ The same session (for chaining).
155
+ """
156
+ proxy_url = f"http://127.0.0.1:{self._proxy_port}"
157
+ session.proxies = {"http": proxy_url, "https": proxy_url}
158
+ # Disable trust_env so system proxy env vars don't interfere
159
+ session.trust_env = False
160
+ return session
161
+
162
+ def wrap_httpx(self, client: Any) -> Any:
163
+ """Inject the TNG http_proxy into an ``httpx.Client`` or
164
+ ``httpx.AsyncClient``.
165
+
166
+ After calling this, all ``client.get/post/request(...)`` calls
167
+ go through the TNG encrypted tunnel.
168
+
169
+ Args:
170
+ client: An ``httpx.Client`` or ``httpx.AsyncClient`` instance.
171
+
172
+ Returns:
173
+ The same client (for chaining).
174
+ """
175
+ import httpx
176
+ from httpx._utils import URLPattern
177
+
178
+ proxy_url = f"http://127.0.0.1:{self._proxy_port}"
179
+ proxy = httpx.Proxy(proxy_url)
180
+
181
+ # Set _proxy for introspection compatibility, and _mounts for routing.
182
+ # _init_proxy_transport creates a properly configured transport with
183
+ # SSL, timeouts, etc. We mount it for all:// URLs.
184
+ proxy_transport = client._init_proxy_transport(
185
+ proxy, verify=True, cert=None, trust_env=False,
186
+ http1=True, http2=False, limits=httpx.Limits(),
187
+ )
188
+ client._proxy = proxy
189
+ client._mounts = {URLPattern("all://"): proxy_transport}
190
+ return client
191
+
192
+ def wrap_openai(self, client: Any) -> Any:
193
+ """Inject the TNG http_proxy into an ``openai.OpenAI`` or
194
+ ``openai.AsyncOpenAI`` client.
195
+
196
+ OpenAI internally uses ``httpx.Client`` / ``httpx.AsyncClient``
197
+ stored at ``client._client``. We set its proxy so all API calls
198
+ go through TNG.
199
+
200
+ Args:
201
+ client: An ``openai.OpenAI`` or ``openai.AsyncOpenAI`` instance.
202
+
203
+ Returns:
204
+ The same client (for chaining).
205
+ """
206
+ import httpx
207
+ from httpx._utils import URLPattern
208
+
209
+ proxy_url = f"http://127.0.0.1:{self._proxy_port}"
210
+ proxy = httpx.Proxy(proxy_url)
211
+ inner = client._client
212
+
213
+ proxy_transport = inner._init_proxy_transport(
214
+ proxy, verify=True, cert=None, trust_env=False,
215
+ http1=True, http2=False, limits=httpx.Limits(),
216
+ )
217
+ inner._proxy = proxy
218
+ inner._mounts = {URLPattern("all://"): proxy_transport}
219
+ return client
220
+
221
+ # ------------------------------------------------------------------
222
+ # Cleanup
223
+ # ------------------------------------------------------------------
224
+
225
+ def _cleanup(self) -> None:
226
+ """Terminate the TNG subprocess and remove the temp config file."""
227
+ if getattr(self, "_proc", None) is not None:
228
+ proc = self._proc
229
+ if proc.poll() is None:
230
+ proc.terminate()
231
+ try:
232
+ proc.wait(timeout=10)
233
+ except subprocess.TimeoutExpired:
234
+ proc.kill()
235
+ proc.wait(timeout=5)
236
+ self._proc = None # type: ignore[assignment]
237
+
238
+ config_path = getattr(self, "_config_path", None)
239
+ if config_path and os.path.exists(config_path):
240
+ try:
241
+ os.unlink(config_path)
242
+ except OSError:
243
+ pass
244
+
245
+ def __del__(self) -> None:
246
+ self._cleanup()
247
+
248
+ def close(self) -> None:
249
+ """Explicitly stop the TNG subprocess and clean up resources.
250
+
251
+ Safe to call multiple times.
252
+ """
253
+ self._cleanup()
254
+
255
+ def __enter__(self) -> "Tng":
256
+ return self
257
+
258
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
259
+ self._cleanup()
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Helpers
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ def _find_free_port() -> int:
268
+ """Find a free TCP port on 127.0.0.1."""
269
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
270
+ s.bind(("127.0.0.1", 0))
271
+ return s.getsockname()[1]
272
+
273
+
274
+ def _wait_for_port(
275
+ host: str, port: int, proc: subprocess.Popen | None = None, timeout: float = 30.0,
276
+ ) -> None:
277
+ """Wait until a TCP port is accepting connections.
278
+
279
+ If *proc* is given, also checks whether the process exited early
280
+ and raises a more useful error message.
281
+
282
+ Raises:
283
+ RuntimeError: If the subprocess exited before the port was ready.
284
+ TimeoutError: If the port is not ready within the timeout.
285
+ """
286
+ deadline = time.monotonic() + timeout
287
+ while time.monotonic() < deadline:
288
+ # Check if the subprocess exited early
289
+ if proc is not None:
290
+ exit_code = proc.poll()
291
+ if exit_code is not None:
292
+ raise RuntimeError(
293
+ f"TNG process exited with code {exit_code} before proxy was ready"
294
+ )
295
+ try:
296
+ with socket.create_connection((host, port), timeout=0.5):
297
+ return
298
+ except (ConnectionRefusedError, OSError):
299
+ time.sleep(0.1)
300
+ raise TimeoutError(f"Port {port} not ready within {timeout}s")
301
+
302
+
303
+ def _find_tng_binary() -> str:
304
+ """Locate the TNG binary.
305
+
306
+ Search order:
307
+ 1. ``TNG_BINARY`` environment variable (if set)
308
+ 2. ``shutil.which("tng")`` — system PATH (wheel installs to {prefix}/bin/tng)
309
+ 3. ``{module_dir}/../bin/tng`` — development mode
310
+ 4. ``/usr/bin/tng`` — system install (Unix only)
311
+ """
312
+ import sys
313
+
314
+ # 1. Check environment variable
315
+ env_bin = os.environ.get("TNG_BINARY")
316
+ if env_bin and Path(env_bin).is_file():
317
+ return env_bin
318
+
319
+ # 2. Check PATH (wheel installation puts it in {prefix}/bin/tng)
320
+ found = shutil.which("tng")
321
+ if found:
322
+ return found
323
+
324
+ # 3. Check development mode location
325
+ module_dir = Path(__file__).resolve().parent
326
+ # Try platform-specific binary names
327
+ suffix = ".exe" if sys.platform == "win32" else ""
328
+ dev_bin = module_dir.parent / "bin" / f"tng{suffix}"
329
+ if dev_bin.is_file():
330
+ return str(dev_bin)
331
+
332
+ # 4. Check system install (Unix only)
333
+ if sys.platform != "win32":
334
+ system = Path("/usr/bin/tng")
335
+ if system.is_file():
336
+ return str(system)
337
+
338
+ raise FileNotFoundError(
339
+ "TNG binary not found. Options:\n"
340
+ " 1. Set TNG_BINARY environment variable to the tng binary path\n"
341
+ " 2. Install tng on your system PATH\n"
342
+ " 3. Place tng binary at tng-python/bin/tng (development mode)"
343
+ )
344
+
345
+
346
+ def _build_tng_config(
347
+ proxy_port: int,
348
+ no_ra: bool,
349
+ verify: dict[str, Any] | None,
350
+ attest: dict[str, Any] | None,
351
+ ohttp: dict[str, Any] | None,
352
+ rats_tls: dict[str, Any] | None,
353
+ ) -> dict[str, Any]:
354
+ """Build the TNG config dict (ingress only, no egress).
355
+
356
+ The http_proxy ingress listens on ``127.0.0.1:<proxy_port>``.
357
+ Incoming requests have their target extracted from the Host header.
358
+
359
+ Args:
360
+ proxy_port: Port for the http_proxy ingress to listen on.
361
+ no_ra: Disable remote attestation (debugging only).
362
+ verify: Verifier config (as_addr, policy_ids, etc.).
363
+ attest: Attester config (aa_addr, as_addr, model, etc.).
364
+ ohttp: OHTTP customization (key, path_rewrites, etc.).
365
+ rats_tls: rats-TLS customization.
366
+
367
+ Returns:
368
+ A TNG config dict with ``add_ingress`` only.
369
+ """
370
+ ingress_entry: dict[str, Any] = {
371
+ "http_proxy": {
372
+ "proxy_listen": {"host": "127.0.0.1", "port": proxy_port},
373
+ },
374
+ }
375
+
376
+ # Default to ohttp={} when neither ohttp nor rats_tls is specified
377
+ if ohttp is not None:
378
+ ingress_entry["ohttp"] = copy.deepcopy(ohttp)
379
+ elif rats_tls is None:
380
+ ingress_entry["ohttp"] = {}
381
+
382
+ if rats_tls is not None:
383
+ ingress_entry["rats_tls"] = copy.deepcopy(rats_tls)
384
+
385
+ if no_ra:
386
+ ingress_entry["no_ra"] = True
387
+
388
+ if verify is not None:
389
+ ingress_entry["verify"] = copy.deepcopy(verify)
390
+
391
+ if attest is not None:
392
+ ingress_entry["attest"] = copy.deepcopy(attest)
393
+
394
+ return {"add_ingress": [ingress_entry]}
File without changes
Binary file
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: tng-sdk
3
+ Version: 2.7.3
4
+ Summary: TNG (Trusted Network Gateway) Python SDK — encrypted HTTP requests with remote attestation
5
+ Project-URL: Homepage, https://github.com/inclavare-containers/TNG
6
+ Project-URL: Documentation, https://github.com/inclavare-containers/TNG/tree/master/tng-python
7
+ Project-URL: Source, https://github.com/inclavare-containers/TNG/tree/master/tng-python
8
+ License: Apache-2.0
9
+ Requires-Python: >=3.8
10
+ Provides-Extra: dev
11
+ Requires-Dist: httpx>=0.27; extra == 'dev'
12
+ Requires-Dist: openai>=1.0; extra == 'dev'
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Requires-Dist: requests>=2.28; extra == 'dev'
16
+ Provides-Extra: httpx
17
+ Requires-Dist: httpx>=0.27; extra == 'httpx'
18
+ Provides-Extra: openai
19
+ Requires-Dist: openai>=1.0; extra == 'openai'
20
+ Provides-Extra: requests
21
+ Requires-Dist: requests>=2.28; extra == 'requests'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # TNG Python SDK
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/tng-sdk?logo=pypi&label=pypi)](https://pypi.org/project/tng-sdk/)
27
+
28
+ [中文文档](https://github.com/inclavare-containers/TNG/blob/master/tng-python/README_zh.md)
29
+
30
+ Trusted Network Gateway (TNG) Python SDK — encrypted HTTP requests with remote attestation.
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ pip install tng-sdk
36
+ ```
37
+
38
+ ```python
39
+ from tng import Tng
40
+ import requests
41
+
42
+ # Create TNG client (no_ra = disable remote attestation for testing)
43
+ tng = Tng(no_ra=True)
44
+
45
+ # Wrap a requests session
46
+ session = requests.Session()
47
+ tng.wrap_requests(session)
48
+
49
+ # All requests flow through the encrypted TNG tunnel
50
+ resp = session.get("http://tng-server:10001/api/data")
51
+ print(resp.json())
52
+
53
+ tng.close()
54
+ ```
55
+
56
+ ## Supported Clients
57
+
58
+ | Client | Method |
59
+ |--------|--------|
60
+ | **requests** | `tng.wrap_requests(session)` |
61
+ | **httpx** | `tng.wrap_httpx(client)` |
62
+ | **openai** | `tng.wrap_openai(client)` |
63
+
64
+ ## Security Options
65
+
66
+ ```python
67
+ # Disable remote attestation (testing only)
68
+ tng = Tng(no_ra=True)
69
+
70
+ # With verifier
71
+ tng = Tng(verify={"as_addr": "http://127.0.0.1:8080/", "policy_ids": ["default"]})
72
+
73
+ # With attester
74
+ tng = Tng(attest={"aa_addr": "unix:///run/aa.sock", "model": "passport"})
75
+
76
+ # Bidirectional attestation
77
+ tng = Tng(
78
+ attest={"aa_addr": "unix:///run/aa.sock"},
79
+ verify={"as_addr": "http://127.0.0.1:8080/", "policy_ids": ["default"]}
80
+ )
81
+ ```
82
+
83
+ ## Encryption Protocol
84
+
85
+ ```python
86
+ # OHTTP is the default
87
+ tng = Tng(no_ra=True)
88
+
89
+ # Use rats-TLS instead of OHTTP (mutually exclusive)
90
+ tng = Tng(no_ra=True, rats_tls={"multiplex": True})
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ - **[Getting Started](https://github.com/inclavare-containers/TNG/blob/master/tng-python/docs/getting-started.md)** — Complete installation, usage, and configuration guide
96
+ - **[Getting Started (中文)](https://github.com/inclavare-containers/TNG/blob/master/tng-python/docs/getting-started_zh.md)** — 完整的安装、使用和配置指南
@@ -0,0 +1,8 @@
1
+ bin/scripts/tng.exe,sha256=uu1IQXRISXKBcTiSNjYMCA1RLC8UYGcJGhSTqZVxvGM,37597696
2
+ tng/__init__.py,sha256=xEv5jAfrhwLgMMBFTDx7DnI1jzi7CBsX5V8ElrBhhgc,1301
3
+ tng/_tng.py,sha256=OgeHK99Faa6QibK2lxjafGsRwN-1S_2zd3Gc88kb8JA,13633
4
+ tng/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ tng_sdk-2.7.3.data/data/bin/tng.exe,sha256=uu1IQXRISXKBcTiSNjYMCA1RLC8UYGcJGhSTqZVxvGM,37597696
6
+ tng_sdk-2.7.3.dist-info/METADATA,sha256=nDZ0qQ5uzsImcl7vRcN3wlacJsjReKHN0D1iWzHTpgo,2885
7
+ tng_sdk-2.7.3.dist-info/WHEEL,sha256=_VsS0eVp3FGI7Mp-ReiZMJWtuJY7p8LSM-m3Ez2oUOk,93
8
+ tng_sdk-2.7.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64