appmesh 1.6.8__py3-none-any.whl → 1.6.10__py3-none-any.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.
- appmesh/__init__.py +12 -0
- appmesh/client_http.py +74 -54
- {appmesh-1.6.8.dist-info → appmesh-1.6.10.dist-info}/METADATA +1 -2
- {appmesh-1.6.8.dist-info → appmesh-1.6.10.dist-info}/RECORD +6 -6
- {appmesh-1.6.8.dist-info → appmesh-1.6.10.dist-info}/WHEEL +0 -0
- {appmesh-1.6.8.dist-info → appmesh-1.6.10.dist-info}/top_level.txt +0 -0
appmesh/__init__.py
CHANGED
@@ -11,6 +11,7 @@ Example:
|
|
11
11
|
|
12
12
|
import sys
|
13
13
|
from types import ModuleType
|
14
|
+
from typing import TYPE_CHECKING
|
14
15
|
|
15
16
|
__all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshClientOAuth", "AppMeshServer", "AppMeshServerTCP"]
|
16
17
|
|
@@ -24,6 +25,17 @@ _LAZY_IMPORTS = {
|
|
24
25
|
}
|
25
26
|
|
26
27
|
|
28
|
+
if TYPE_CHECKING:
|
29
|
+
# Provide explicit imports for static analyzers and type checkers
|
30
|
+
# These imports are only executed during type checking and won't affect runtime.
|
31
|
+
from .app import App # noqa: F401
|
32
|
+
from .client_http import AppMeshClient # noqa: F401
|
33
|
+
from .client_tcp import AppMeshClientTCP # noqa: F401
|
34
|
+
from .client_http_oauth import AppMeshClientOAuth # noqa: F401
|
35
|
+
from .server_http import AppMeshServer # noqa: F401
|
36
|
+
from .server_tcp import AppMeshServerTCP # noqa: F401
|
37
|
+
|
38
|
+
|
27
39
|
def _lazy_import(name):
|
28
40
|
"""Helper function for lazy importing."""
|
29
41
|
if name in _LAZY_IMPORTS:
|
appmesh/client_http.py
CHANGED
@@ -7,7 +7,10 @@ import locale
|
|
7
7
|
import logging
|
8
8
|
import os
|
9
9
|
import sys
|
10
|
+
import threading
|
10
11
|
import time
|
12
|
+
import requests
|
13
|
+
import http.cookiejar as cookiejar
|
11
14
|
from datetime import datetime
|
12
15
|
from enum import Enum, unique
|
13
16
|
from http import HTTPStatus
|
@@ -16,7 +19,6 @@ from typing import Optional, Tuple, Union
|
|
16
19
|
from urllib import parse
|
17
20
|
import aniso8601
|
18
21
|
import jwt
|
19
|
-
import requests
|
20
22
|
from .app import App
|
21
23
|
from .app_run import AppRun
|
22
24
|
from .app_output import AppOutput
|
@@ -74,7 +76,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
74
76
|
- wait_for_async_run()
|
75
77
|
- run_app_sync()
|
76
78
|
- run_task()
|
77
|
-
-
|
79
|
+
- cancel_task()
|
78
80
|
|
79
81
|
# System Management
|
80
82
|
- forward_to
|
@@ -127,6 +129,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
127
129
|
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
128
130
|
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
129
131
|
HTTP_HEADER_KEY_X_FILE_PATH = "X-File-Path"
|
132
|
+
HTTP_HEADER_JWT_set_cookie = "X-Set-Cookie"
|
133
|
+
HTTP_HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
|
134
|
+
COOKIE_TOKEN = "appmesh_auth_token"
|
135
|
+
COOKIE_CSRF_TOKEN = "appmesh_csrf_token"
|
130
136
|
|
131
137
|
@unique
|
132
138
|
class Method(Enum):
|
@@ -191,7 +197,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
191
197
|
rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
|
192
198
|
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(DEFAULT_SSL_CLIENT_CERT_PATH) else None,
|
193
199
|
rest_timeout=(60, 300),
|
194
|
-
jwt_token=None,
|
200
|
+
jwt_token: Optional[str] = None,
|
201
|
+
rest_cookie_file: Optional[str] = None,
|
195
202
|
auto_refresh_token=False,
|
196
203
|
):
|
197
204
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
@@ -213,13 +220,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
213
220
|
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
214
221
|
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
215
222
|
|
223
|
+
rest_cookie_file (str, optional): Path to a file for storing session cookies. If provided, cookies will be saved to and loaded from this file to maintain session state across client instances.
|
224
|
+
|
216
225
|
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
217
226
|
auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
|
218
227
|
When enabled, a background timer will monitor token expiration and attempt to refresh
|
219
228
|
the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
|
220
229
|
"""
|
221
230
|
self._ensure_logging_configured()
|
222
|
-
self.session = requests.Session()
|
223
231
|
self.auth_server_url = rest_url
|
224
232
|
self._jwt_token = jwt_token
|
225
233
|
self.ssl_verify = rest_ssl_verify
|
@@ -227,6 +235,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
227
235
|
self.rest_timeout = rest_timeout
|
228
236
|
self._forward_to = None
|
229
237
|
|
238
|
+
# Session and cookie management
|
239
|
+
self._lock = threading.Lock()
|
240
|
+
self.session = requests.Session()
|
241
|
+
self.cookie_file = self._load_cookies(rest_cookie_file)
|
242
|
+
|
230
243
|
# Token auto-refresh
|
231
244
|
self._token_refresh_timer = None
|
232
245
|
self._auto_refresh_token = auto_refresh_token
|
@@ -241,6 +254,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
241
254
|
def _get_access_token(self) -> str:
|
242
255
|
return self.jwt_token
|
243
256
|
|
257
|
+
def _load_cookies(self, cookie_file: Optional[str]) -> str:
|
258
|
+
"""Load cookies from the cookie file and return the file path ."""
|
259
|
+
if not cookie_file:
|
260
|
+
return ""
|
261
|
+
|
262
|
+
self.session.cookies = cookiejar.MozillaCookieJar(cookie_file)
|
263
|
+
if os.path.exists(cookie_file):
|
264
|
+
self.session.cookies.load(ignore_discard=True, ignore_expires=True)
|
265
|
+
else:
|
266
|
+
os.makedirs(os.path.dirname(cookie_file), exist_ok=True)
|
267
|
+
self.session.cookies.save(ignore_discard=True, ignore_expires=True)
|
268
|
+
if os.name == "posix":
|
269
|
+
os.chmod(cookie_file, 0o600) # User read/write only
|
270
|
+
return cookie_file
|
271
|
+
|
272
|
+
@staticmethod
|
273
|
+
def _get_cookie_value(cookies, name, check_expiry=True) -> Optional[str]:
|
274
|
+
"""Get cookie value by name, checking expiry if requested."""
|
275
|
+
# If it's a RequestsCookieJar, use .get() but check expiry manually if requested
|
276
|
+
if hasattr(cookies, "get") and not isinstance(cookies, list):
|
277
|
+
cookie = cookies.get(name)
|
278
|
+
if cookie is None:
|
279
|
+
return None
|
280
|
+
if check_expiry and getattr(cookie, "expires", None):
|
281
|
+
if cookie.expires < time.time():
|
282
|
+
return None # expired
|
283
|
+
return cookie.value if hasattr(cookie, "value") else cookie
|
284
|
+
|
285
|
+
# Otherwise, assume it's a MozillaCookieJar — iterate manually
|
286
|
+
for c in cookies:
|
287
|
+
if c.name == name:
|
288
|
+
if check_expiry and getattr(c, "expires", None):
|
289
|
+
if c.expires < time.time():
|
290
|
+
return None # expired
|
291
|
+
return c.value
|
292
|
+
|
293
|
+
return None
|
294
|
+
|
244
295
|
def _check_and_refresh_token(self):
|
245
296
|
"""Check and refresh token if needed, then schedule next check.
|
246
297
|
|
@@ -389,6 +440,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
389
440
|
- Refresh tokens before expiration
|
390
441
|
- Validate token format before setting
|
391
442
|
"""
|
443
|
+
if self._jwt_token == token:
|
444
|
+
return # No change
|
392
445
|
self._jwt_token = token
|
393
446
|
|
394
447
|
# handle refresh
|
@@ -398,6 +451,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
398
451
|
self._token_refresh_timer.cancel()
|
399
452
|
self._token_refresh_timer = None
|
400
453
|
|
454
|
+
# handle session
|
455
|
+
with self._lock:
|
456
|
+
if self.cookie_file:
|
457
|
+
self.session.cookies.save(ignore_discard=True, ignore_expires=True)
|
458
|
+
|
401
459
|
@property
|
402
460
|
def forward_to(self) -> str:
|
403
461
|
"""Get the target host address for request forwarding in a cluster setup.
|
@@ -444,44 +502,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
444
502
|
|
445
503
|
self._forward_to = host
|
446
504
|
|
447
|
-
def _normalize_totp_secret(self, secret: str) -> str:
|
448
|
-
"""
|
449
|
-
Normalize a TOTP secret to a valid RFC 4648 Base32 format.
|
450
|
-
This includes:
|
451
|
-
- Removing all whitespace
|
452
|
-
- Uppercasing
|
453
|
-
- Replacing visually similar or Crockford-like digits:
|
454
|
-
'1' -> 'I', '0' -> 'O', '8' -> 'B', '9' -> 'G'
|
455
|
-
- Adding RFC 4648 padding
|
456
|
-
"""
|
457
|
-
if not secret:
|
458
|
-
raise ValueError("Empty TOTP secret")
|
459
|
-
|
460
|
-
# Remove all whitespace and uppercase
|
461
|
-
secret = "".join(secret.split()).upper()
|
462
|
-
|
463
|
-
# Crockford/Base32-friendly replacements
|
464
|
-
replacements = {
|
465
|
-
"1": "I",
|
466
|
-
"0": "O",
|
467
|
-
"8": "B",
|
468
|
-
"9": "G",
|
469
|
-
}
|
470
|
-
|
471
|
-
for old, new in replacements.items():
|
472
|
-
secret = secret.replace(old, new)
|
473
|
-
|
474
|
-
# Validate that only RFC 4648 Base32 characters remain
|
475
|
-
valid_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
|
476
|
-
if not set(secret).issubset(valid_chars):
|
477
|
-
raise ValueError("Invalid characters in TOTP secret")
|
478
|
-
|
479
|
-
# Add padding to make length a multiple of 8 (RFC 4648)
|
480
|
-
padding = (8 - (len(secret) % 8)) % 8
|
481
|
-
secret += "=" * padding
|
482
|
-
|
483
|
-
return secret
|
484
|
-
|
485
505
|
########################################
|
486
506
|
# Security
|
487
507
|
########################################
|
@@ -514,6 +534,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
514
534
|
self.HTTP_HEADER_KEY_AUTH: "Basic " + base64.b64encode(f"{user_name}:{user_pwd}".encode()).decode(),
|
515
535
|
"X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
|
516
536
|
**({"X-Audience": audience} if audience else {}),
|
537
|
+
**({self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {}),
|
517
538
|
# **({"X-Totp-Code": totp_code} if totp_code else {}),
|
518
539
|
},
|
519
540
|
)
|
@@ -648,20 +669,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
648
669
|
|
649
670
|
def get_totp_secret(self) -> str:
|
650
671
|
"""
|
651
|
-
Generate TOTP secret for the current user and return a
|
672
|
+
Generate TOTP secret for the current user and return a secret.
|
652
673
|
|
653
674
|
Returns:
|
654
|
-
str:
|
675
|
+
str: TOTP secret string
|
655
676
|
"""
|
656
677
|
resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
|
657
678
|
if resp.status_code == HTTPStatus.OK:
|
658
679
|
totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
|
659
|
-
|
660
|
-
if not secret:
|
661
|
-
raise ValueError("TOTP secret missing in response")
|
662
|
-
|
663
|
-
# Normalize to standard Base32
|
664
|
-
return self._normalize_totp_secret(secret)
|
680
|
+
return self._parse_totp_uri(totp_uri).get("secret")
|
665
681
|
|
666
682
|
raise Exception(resp.text)
|
667
683
|
|
@@ -1282,7 +1298,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1282
1298
|
|
1283
1299
|
return resp.text
|
1284
1300
|
|
1285
|
-
def
|
1301
|
+
def cancel_task(self, app_name: str) -> bool:
|
1286
1302
|
"""Client cancle a running task to a App Mesh application.
|
1287
1303
|
|
1288
1304
|
Args:
|
@@ -1442,9 +1458,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1442
1458
|
|
1443
1459
|
# Prepare headers
|
1444
1460
|
header = {} if header is None else header
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
1461
|
+
|
1462
|
+
# JWT or Cookie token
|
1463
|
+
if self.cookie_file and self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN):
|
1464
|
+
header[self.HTTP_HEADER_NAME_CSRF_TOKEN] = self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN)
|
1465
|
+
elif self._get_access_token():
|
1466
|
+
header[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {self._get_access_token()}"
|
1467
|
+
|
1448
1468
|
if self.forward_to and len(self.forward_to) > 0:
|
1449
1469
|
if ":" in self.forward_to:
|
1450
1470
|
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.6.
|
3
|
+
Version: 1.6.10
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -145,7 +145,6 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
145
145
|
- [log4cpp](http://log4cpp.sourceforge.net)
|
146
146
|
- [Crypto++](https://www.cryptopp.com)
|
147
147
|
- [ldap-cpp](https://github.com/AndreyBarmaley/ldap-cpp)
|
148
|
-
- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/liboath-api)
|
149
148
|
|
150
149
|
[language.url]: https://isocpp.org/
|
151
150
|
[language.badge]: https://img.shields.io/badge/language-C++-blue.svg
|
@@ -1,16 +1,16 @@
|
|
1
|
-
appmesh/__init__.py,sha256=
|
1
|
+
appmesh/__init__.py,sha256=uJ5LOadwZW2nOXkSuPOy2S3WH9Bx0BUn5wUvPBeFzoc,2217
|
2
2
|
appmesh/app.py,sha256=crD4DRFZJuHtZMfSsz7C-EwvjPmGZbFXYXvA_wCdvdI,10734
|
3
3
|
appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
|
4
4
|
appmesh/app_run.py,sha256=aYq852a29OThIi32Xtx5s0sTXZ97T0lHD5WXH8yfPoc,2018
|
5
5
|
appmesh/appmesh_client.py,sha256=ywB2222PtJUffdfdxZcBfdhZs1KYyc7JvzMxwuK2qyI,378
|
6
|
-
appmesh/client_http.py,sha256=
|
6
|
+
appmesh/client_http.py,sha256=KD4AMcMbHqaLJWSrra0J03kMqWAiwYHiyusUc5kpr6o,60443
|
7
7
|
appmesh/client_http_oauth.py,sha256=1d51o0JX_xtB8d2bEuM7_XJHcwMnhcjkbIq7GE1Zxm8,6120
|
8
8
|
appmesh/client_tcp.py,sha256=aq6UUzytZA4ibE9WQMMWdo1uW8sHETEhJjsbM6IYSno,11457
|
9
9
|
appmesh/server_http.py,sha256=rBIYO9rbR-r3x1Jcry440Sp--IM-OWKRaOhNpGdkxh8,4299
|
10
10
|
appmesh/server_tcp.py,sha256=-CU5tw97WJmDcUNsNPWqpdZ0wxRzRD6kUP3XyNZUTHc,1444
|
11
11
|
appmesh/tcp_messages.py,sha256=H9S_iCy0IuufY2v50_SUgRvcyQmJsySG65tBe_xb3Ko,1878
|
12
12
|
appmesh/tcp_transport.py,sha256=0hRSp5fpL9wKB05JIyIRIuyBC8w1IdokryhMDHqtN4M,8946
|
13
|
-
appmesh-1.6.
|
14
|
-
appmesh-1.6.
|
15
|
-
appmesh-1.6.
|
16
|
-
appmesh-1.6.
|
13
|
+
appmesh-1.6.10.dist-info/METADATA,sha256=do80dYMa4qK9tg1qKDM4M0klzIgzfcEGlWo6w1OIBfg,11764
|
14
|
+
appmesh-1.6.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
+
appmesh-1.6.10.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
16
|
+
appmesh-1.6.10.dist-info/RECORD,,
|
File without changes
|
File without changes
|