appmesh 1.4.6__py3-none-any.whl → 1.4.8__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/http_client.py +108 -32
- appmesh/keycloak.py +248 -0
- appmesh/tcp_transport.py +58 -31
- {appmesh-1.4.6.dist-info → appmesh-1.4.8.dist-info}/METADATA +6 -3
- {appmesh-1.4.6.dist-info → appmesh-1.4.8.dist-info}/RECORD +7 -6
- {appmesh-1.4.6.dist-info → appmesh-1.4.8.dist-info}/WHEEL +1 -1
- {appmesh-1.4.6.dist-info → appmesh-1.4.8.dist-info}/top_level.txt +0 -0
appmesh/http_client.py
CHANGED
@@ -8,9 +8,9 @@ from datetime import datetime
|
|
8
8
|
from enum import Enum, unique
|
9
9
|
from http import HTTPStatus
|
10
10
|
from typing import Optional, Tuple, Union
|
11
|
+
from urllib import parse
|
11
12
|
import aniso8601
|
12
13
|
import requests
|
13
|
-
import urllib
|
14
14
|
from .app import App
|
15
15
|
from .app_run import AppRun
|
16
16
|
from .app_output import AppOutput
|
@@ -131,6 +131,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
131
131
|
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,
|
132
132
|
rest_timeout=(60, 300),
|
133
133
|
jwt_token=None,
|
134
|
+
oauth2_config=None,
|
134
135
|
):
|
135
136
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
136
137
|
|
@@ -143,24 +144,68 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
143
144
|
- `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
|
144
145
|
which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
|
145
146
|
|
146
|
-
|
147
|
-
-
|
148
|
-
-
|
147
|
+
ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
|
148
|
+
- `str`: A path to a single PEM file containing both the client certificate and private key.
|
149
|
+
- `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
|
149
150
|
|
150
151
|
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
151
152
|
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
152
153
|
|
153
154
|
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
154
155
|
|
156
|
+
oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
|
157
|
+
- auth_server_url: Keycloak server URL (e.g. "https://keycloak.example.com")
|
158
|
+
- realm: Keycloak realm
|
159
|
+
- client_id: Keycloak client ID
|
160
|
+
- client_secret: Keycloak client secret (optional)
|
155
161
|
"""
|
156
162
|
|
157
|
-
self.
|
163
|
+
self.session = requests.Session()
|
164
|
+
self.auth_server_url = rest_url
|
158
165
|
self._jwt_token = jwt_token
|
159
166
|
self.ssl_verify = rest_ssl_verify
|
160
167
|
self.ssl_client_cert = rest_ssl_client_cert
|
161
168
|
self.rest_timeout = rest_timeout
|
162
169
|
self._forward_to = None
|
163
170
|
|
171
|
+
# Keycloak integration
|
172
|
+
self._keycloak_client = None
|
173
|
+
if oauth2_config:
|
174
|
+
from .keycloak import KeycloakClient # lazy import
|
175
|
+
|
176
|
+
self._keycloak_client = KeycloakClient(
|
177
|
+
auth_server_url=oauth2_config.get("auth_server_url"),
|
178
|
+
realm=oauth2_config.get("realm"),
|
179
|
+
client_id=oauth2_config.get("client_id"),
|
180
|
+
client_secret=oauth2_config.get("client_secret"),
|
181
|
+
ssl_verify=oauth2_config.get("ssl_verify", self.ssl_verify),
|
182
|
+
timeout=oauth2_config.get("timeout", (30, 60)),
|
183
|
+
)
|
184
|
+
|
185
|
+
def close(self):
|
186
|
+
"""Close the session and release resources."""
|
187
|
+
if hasattr(self, "session") and self.session:
|
188
|
+
self.session.close()
|
189
|
+
self.session = None
|
190
|
+
if self._keycloak_client and hasattr(self._keycloak_client, "close"):
|
191
|
+
self._keycloak_client.close()
|
192
|
+
self._keycloak_client = None
|
193
|
+
|
194
|
+
def __enter__(self):
|
195
|
+
"""Support for context manager protocol."""
|
196
|
+
return self
|
197
|
+
|
198
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
199
|
+
"""Support for context manager protocol, ensuring resources are released."""
|
200
|
+
self.close()
|
201
|
+
|
202
|
+
def __del__(self):
|
203
|
+
"""Ensure resources are properly released when the object is garbage collected."""
|
204
|
+
try:
|
205
|
+
self.close()
|
206
|
+
except Exception:
|
207
|
+
pass # Avoid exceptions during garbage collection
|
208
|
+
|
164
209
|
@property
|
165
210
|
def jwt_token(self) -> str:
|
166
211
|
"""Get the current JWT (JSON Web Token) used for authentication.
|
@@ -266,6 +311,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
266
311
|
Returns:
|
267
312
|
str: JWT token.
|
268
313
|
"""
|
314
|
+
# Keycloak authentication if configured
|
315
|
+
if self._keycloak_client:
|
316
|
+
self.jwt_token = self._keycloak_client.authenticate(user_name, user_pwd)
|
317
|
+
return self.jwt_token
|
318
|
+
|
319
|
+
# Standard App Mesh authentication
|
269
320
|
self.jwt_token = None
|
270
321
|
resp = self._request_http(
|
271
322
|
AppMeshClient.Method.POST,
|
@@ -293,7 +344,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
293
344
|
username (str): Username to validate
|
294
345
|
challenge (str): Challenge string from server
|
295
346
|
code (str): TOTP code to validate
|
296
|
-
timeout (Union[int, str], optional): Token expiry timeout.
|
347
|
+
timeout (Union[int, str], optional): Token expiry timeout.
|
297
348
|
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P1W') or seconds.
|
298
349
|
Defaults to DURATION_ONE_WEEK_ISO.
|
299
350
|
|
@@ -325,11 +376,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
325
376
|
Returns:
|
326
377
|
bool: logoff success or failure.
|
327
378
|
"""
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
return
|
379
|
+
if self.jwt_token:
|
380
|
+
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
381
|
+
self.jwt_token = None
|
382
|
+
return resp.status_code == HTTPStatus.OK
|
383
|
+
return True
|
333
384
|
|
334
385
|
def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
|
335
386
|
"""Deprecated: Use authenticate() instead."""
|
@@ -370,6 +421,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
370
421
|
Returns:
|
371
422
|
str: The new JWT token if renew success, the old token will be blocked.
|
372
423
|
"""
|
424
|
+
# Refresh the Keycloak token
|
425
|
+
if self._keycloak_client:
|
426
|
+
self.jwt_token = self._keycloak_client.refresh_tokens()
|
427
|
+
return self.jwt_token
|
428
|
+
|
429
|
+
# Refresh the App Mesh token
|
373
430
|
assert self.jwt_token
|
374
431
|
resp = self._request_http(
|
375
432
|
AppMeshClient.Method.POST,
|
@@ -445,13 +502,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
445
502
|
dict: eextract parameters
|
446
503
|
"""
|
447
504
|
parsed_info = {}
|
448
|
-
parsed_uri =
|
505
|
+
parsed_uri = parse.urlparse(totp_uri)
|
449
506
|
|
450
507
|
# Extract label from the path
|
451
508
|
parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
|
452
509
|
|
453
510
|
# Extract parameters from the query string
|
454
|
-
query_params =
|
511
|
+
query_params = parse.parse_qs(parsed_uri.query)
|
455
512
|
for key, value in query_params.items():
|
456
513
|
parsed_info[key] = value[0]
|
457
514
|
return parsed_info
|
@@ -953,7 +1010,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
953
1010
|
|
954
1011
|
with open(file=local_file, mode="rb") as fp:
|
955
1012
|
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
956
|
-
header = {"File-Path":
|
1013
|
+
header = {"File-Path": parse.quote(remote_file), "Content-Type": encoder.content_type}
|
957
1014
|
|
958
1015
|
# Include file attributes (permissions, owner, group) if requested
|
959
1016
|
if apply_file_attributes:
|
@@ -1043,12 +1100,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1043
1100
|
while len(run.proc_uid) > 0:
|
1044
1101
|
app_out = self.get_app_output(app_name=run.app_name, stdout_position=last_output_position, stdout_index=0, process_uuid=run.proc_uid, timeout=interval)
|
1045
1102
|
if app_out.output and stdout_print:
|
1046
|
-
print(app_out.output, end="")
|
1103
|
+
print(app_out.output, end="", flush=True)
|
1047
1104
|
if app_out.out_position is not None:
|
1048
1105
|
last_output_position = app_out.out_position
|
1049
1106
|
if app_out.exit_code is not None:
|
1050
1107
|
# success
|
1051
|
-
|
1108
|
+
try:
|
1109
|
+
self.delete_app(run.app_name)
|
1110
|
+
except Exception:
|
1111
|
+
pass
|
1052
1112
|
return app_out.exit_code
|
1053
1113
|
if app_out.status_code != HTTPStatus.OK:
|
1054
1114
|
# failed
|
@@ -1120,7 +1180,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1120
1180
|
Returns:
|
1121
1181
|
requests.Response: HTTP response
|
1122
1182
|
"""
|
1123
|
-
|
1183
|
+
# Try to refresh token via Keycloak if using Keycloak and token needs refreshing
|
1184
|
+
if self._keycloak_client and self.jwt_token and not self._keycloak_client.validate_token():
|
1185
|
+
try:
|
1186
|
+
self.jwt_token = self._keycloak_client.get_active_token()
|
1187
|
+
except Exception as e:
|
1188
|
+
print(f"Token refresh failed: {str(e)}")
|
1189
|
+
|
1190
|
+
rest_url = parse.urljoin(self.auth_server_url, path)
|
1124
1191
|
|
1125
1192
|
header = {} if header is None else header
|
1126
1193
|
if self.jwt_token:
|
@@ -1129,20 +1196,29 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1129
1196
|
if ":" in self.forward_to:
|
1130
1197
|
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
|
1131
1198
|
else:
|
1132
|
-
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(
|
1199
|
+
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(parse.urlsplit(self.auth_server_url).port)
|
1133
1200
|
header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1134
1201
|
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1202
|
+
try:
|
1203
|
+
if method is AppMeshClient.Method.GET:
|
1204
|
+
return self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1205
|
+
elif method is AppMeshClient.Method.POST:
|
1206
|
+
return self.session.post(
|
1207
|
+
url=rest_url,
|
1208
|
+
params=query,
|
1209
|
+
headers=header,
|
1210
|
+
data=json.dumps(body) if type(body) in (dict, list) else body,
|
1211
|
+
cert=self.ssl_client_cert,
|
1212
|
+
verify=self.ssl_verify,
|
1213
|
+
timeout=self.rest_timeout,
|
1214
|
+
)
|
1215
|
+
elif method is AppMeshClient.Method.POST_STREAM:
|
1216
|
+
return self.session.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
|
1217
|
+
elif method is AppMeshClient.Method.DELETE:
|
1218
|
+
return self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1219
|
+
elif method is AppMeshClient.Method.PUT:
|
1220
|
+
return self.session.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1221
|
+
else:
|
1222
|
+
raise Exception("Invalid http method", method)
|
1223
|
+
except requests.exceptions.RequestException as e:
|
1224
|
+
raise Exception(f"HTTP request failed: {str(e)}")
|
appmesh/keycloak.py
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# Keycloak authentication client for App Mesh Python SDK
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import json
|
5
|
+
import time
|
6
|
+
from typing import Dict, Optional, Tuple, Any
|
7
|
+
import requests
|
8
|
+
|
9
|
+
|
10
|
+
class KeycloakClient:
|
11
|
+
"""
|
12
|
+
Client for authenticating with Keycloak and obtaining tokens for App Mesh.
|
13
|
+
|
14
|
+
This class handles the OAuth2/OIDC workflow with Keycloak, including:
|
15
|
+
- Direct authentication with username/password
|
16
|
+
- Token refresh
|
17
|
+
- Token validation
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
auth_server_url: str,
|
23
|
+
realm: str,
|
24
|
+
client_id: str,
|
25
|
+
client_secret: Optional[str] = None,
|
26
|
+
ssl_verify: bool = True,
|
27
|
+
timeout: Tuple[int, int] = (10, 60),
|
28
|
+
token_refresh_threshold: int = 30,
|
29
|
+
):
|
30
|
+
"""Initialize Keycloak client.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
auth_server_url (str): Keycloak server URL (e.g. https://keycloak.example.com/auth)
|
34
|
+
realm (str): Keycloak realm name
|
35
|
+
client_id (str): Client ID registered in Keycloak
|
36
|
+
client_secret (Optional[str], optional): Client secret if using confidential client. Defaults to None.
|
37
|
+
ssl_verify (bool, optional): Verify SSL certificates. Defaults to True.
|
38
|
+
timeout (Tuple[int, int], optional): Connection and read timeouts. Defaults to (10, 60).
|
39
|
+
token_refresh_threshold (int, optional): Seconds before token expiry to trigger refresh. Defaults to 30.
|
40
|
+
"""
|
41
|
+
self.auth_server_url = auth_server_url.rstrip("/")
|
42
|
+
self.realm = realm
|
43
|
+
self.client_id = client_id
|
44
|
+
self.client_secret = client_secret
|
45
|
+
self.ssl_verify = ssl_verify
|
46
|
+
self.timeout = timeout
|
47
|
+
self.token_refresh_threshold = token_refresh_threshold
|
48
|
+
|
49
|
+
# Token storage
|
50
|
+
self.access_token = None
|
51
|
+
self.refresh_token = None
|
52
|
+
self.token_expires_at = 0
|
53
|
+
|
54
|
+
# Construct the token endpoint URL
|
55
|
+
self.token_endpoint = f"{self.auth_server_url}/realms/{self.realm}/protocol/openid-connect/token"
|
56
|
+
self.userinfo_endpoint = f"{self.auth_server_url}/realms/{self.realm}/protocol/openid-connect/userinfo"
|
57
|
+
|
58
|
+
# Create a session for connection pooling
|
59
|
+
self.session = requests.Session()
|
60
|
+
|
61
|
+
def __enter__(self):
|
62
|
+
"""Support for context manager protocol."""
|
63
|
+
return self
|
64
|
+
|
65
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
66
|
+
"""Clean up resources when exiting context."""
|
67
|
+
self.close()
|
68
|
+
|
69
|
+
def __del__(self):
|
70
|
+
"""Ensure resources are properly released when the object is garbage collected."""
|
71
|
+
try:
|
72
|
+
self.close()
|
73
|
+
except Exception:
|
74
|
+
pass # Avoid exceptions during garbage collection
|
75
|
+
|
76
|
+
def authenticate(self, username: str, password: str) -> str:
|
77
|
+
"""Authenticate with username and password.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
username (str): Username
|
81
|
+
password (str): Password
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
str: Access token
|
85
|
+
|
86
|
+
Raises:
|
87
|
+
Exception: If authentication fails
|
88
|
+
"""
|
89
|
+
data = {
|
90
|
+
"client_id": self.client_id,
|
91
|
+
"grant_type": "password",
|
92
|
+
"username": username,
|
93
|
+
"password": password,
|
94
|
+
}
|
95
|
+
|
96
|
+
if self.client_secret:
|
97
|
+
data["client_secret"] = self.client_secret
|
98
|
+
|
99
|
+
try:
|
100
|
+
response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
|
101
|
+
|
102
|
+
if response.status_code != 200:
|
103
|
+
raise Exception(f"Authentication failed: {response.text}")
|
104
|
+
|
105
|
+
token_data = response.json()
|
106
|
+
self._update_token_data(token_data)
|
107
|
+
return self.access_token
|
108
|
+
|
109
|
+
except requests.RequestException as e:
|
110
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
111
|
+
|
112
|
+
def refresh_tokens(self) -> str:
|
113
|
+
"""Refresh the access token using the refresh token.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
str: New access token
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
Exception: If refresh fails
|
120
|
+
"""
|
121
|
+
if not self.refresh_token:
|
122
|
+
raise Exception("No refresh token available")
|
123
|
+
|
124
|
+
data = {
|
125
|
+
"client_id": self.client_id,
|
126
|
+
"grant_type": "refresh_token",
|
127
|
+
"refresh_token": self.refresh_token,
|
128
|
+
}
|
129
|
+
|
130
|
+
if self.client_secret:
|
131
|
+
data["client_secret"] = self.client_secret
|
132
|
+
|
133
|
+
try:
|
134
|
+
response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
|
135
|
+
|
136
|
+
if response.status_code != 200:
|
137
|
+
raise Exception(f"Token refresh failed: {response.text}")
|
138
|
+
|
139
|
+
token_data = response.json()
|
140
|
+
self._update_token_data(token_data)
|
141
|
+
return self.access_token
|
142
|
+
|
143
|
+
except requests.RequestException as e:
|
144
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
145
|
+
|
146
|
+
def validate_token(self) -> bool:
|
147
|
+
"""Check if the current token is valid and not expired.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
bool: True if valid, False otherwise
|
151
|
+
"""
|
152
|
+
if not self.access_token:
|
153
|
+
return False
|
154
|
+
|
155
|
+
# Check if the token is expired based on our local expiry time
|
156
|
+
if time.time() > self.token_expires_at:
|
157
|
+
return False
|
158
|
+
|
159
|
+
return True
|
160
|
+
|
161
|
+
def get_active_token(self) -> str:
|
162
|
+
"""Get a valid access token, refreshing if necessary.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
str: Valid access token
|
166
|
+
|
167
|
+
Raises:
|
168
|
+
Exception: If no valid token can be obtained
|
169
|
+
"""
|
170
|
+
if self.validate_token():
|
171
|
+
return self.access_token
|
172
|
+
|
173
|
+
if self.refresh_token:
|
174
|
+
try:
|
175
|
+
return self.refresh_tokens()
|
176
|
+
except Exception:
|
177
|
+
pass
|
178
|
+
|
179
|
+
raise Exception("No valid token available and unable to refresh")
|
180
|
+
|
181
|
+
def get_user_info(self) -> Dict[str, Any]:
|
182
|
+
"""Get information about the authenticated user.
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
Dict[str, Any]: User information
|
186
|
+
|
187
|
+
Raises:
|
188
|
+
Exception: If request fails
|
189
|
+
"""
|
190
|
+
if not self.access_token:
|
191
|
+
raise Exception("Not authenticated")
|
192
|
+
|
193
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
194
|
+
|
195
|
+
try:
|
196
|
+
response = self.session.get(self.userinfo_endpoint, headers=headers, verify=self.ssl_verify, timeout=self.timeout)
|
197
|
+
|
198
|
+
if response.status_code != 200:
|
199
|
+
raise Exception(f"Failed to get user info: {response.text}")
|
200
|
+
|
201
|
+
return response.json()
|
202
|
+
|
203
|
+
except requests.RequestException as e:
|
204
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
205
|
+
|
206
|
+
def _update_token_data(self, token_data: Dict[str, Any]) -> None:
|
207
|
+
"""Update the stored token data.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
token_data (Dict[str, Any]): Token data from Keycloak
|
211
|
+
"""
|
212
|
+
self.access_token = token_data["access_token"]
|
213
|
+
self.refresh_token = token_data.get("refresh_token")
|
214
|
+
|
215
|
+
# Calculate token expiration time with configurable buffer
|
216
|
+
expires_in = token_data.get("expires_in", 300)
|
217
|
+
self.token_expires_at = time.time() + expires_in - self.token_refresh_threshold
|
218
|
+
|
219
|
+
def close(self) -> None:
|
220
|
+
"""Close the session and release resources."""
|
221
|
+
if hasattr(self, "session") and self.session:
|
222
|
+
self.session.close()
|
223
|
+
self.session = None
|
224
|
+
|
225
|
+
@staticmethod
|
226
|
+
def decode_token(token: str) -> Dict[str, Any]:
|
227
|
+
"""Decode a JWT token without verification.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
token (str): JWT token
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
Dict[str, Any]: Decoded token payload
|
234
|
+
"""
|
235
|
+
parts = token.split(".")
|
236
|
+
if len(parts) != 3:
|
237
|
+
raise Exception("Invalid token format")
|
238
|
+
|
239
|
+
# Decode the payload (second part)
|
240
|
+
payload = parts[1]
|
241
|
+
# Add padding if necessary
|
242
|
+
payload += "=" * (4 - len(payload) % 4) if len(payload) % 4 else ""
|
243
|
+
|
244
|
+
try:
|
245
|
+
decoded = base64.b64decode(payload)
|
246
|
+
return json.loads(decoded)
|
247
|
+
except Exception as e:
|
248
|
+
raise Exception(f"Failed to decode token: {str(e)}")
|
appmesh/tcp_transport.py
CHANGED
@@ -31,9 +31,9 @@ class TCPTransport:
|
|
31
31
|
**Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
|
32
32
|
When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
|
33
33
|
|
34
|
-
ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key.
|
35
|
-
|
36
|
-
|
34
|
+
ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
|
35
|
+
- `str`: A path to a single PEM file containing both the client certificate and private key.
|
36
|
+
- `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
|
37
37
|
|
38
38
|
tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
|
39
39
|
Defaults to `("localhost", 6059)`.
|
@@ -83,21 +83,31 @@ class TCPTransport:
|
|
83
83
|
|
84
84
|
if self.ssl_client_cert is not None:
|
85
85
|
# Load client-side certificate and private key
|
86
|
-
|
86
|
+
if isinstance(self.ssl_client_cert, tuple) and len(self.ssl_client_cert) == 2:
|
87
|
+
context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
|
88
|
+
elif isinstance(self.ssl_client_cert, str):
|
89
|
+
# Handle case where cert and key are in the same file
|
90
|
+
context.load_cert_chain(certfile=self.ssl_client_cert)
|
91
|
+
else:
|
92
|
+
raise ValueError("ssl_client_cert must be a string filepath or a tuple of (cert_file, key_file)")
|
87
93
|
|
88
94
|
# Create a TCP socket
|
89
95
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
90
96
|
sock.setblocking(True)
|
91
97
|
sock.settimeout(30) # Connection timeout set to 30 seconds
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
98
|
+
try:
|
99
|
+
# Wrap the socket with SSL/TLS
|
100
|
+
ssl_socket = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
|
101
|
+
# Connect to the server
|
102
|
+
ssl_socket.connect(self.tcp_address)
|
103
|
+
# Disable Nagle's algorithm
|
104
|
+
ssl_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
105
|
+
# After connecting, set separate timeout for recv/send
|
106
|
+
# ssl_socket.settimeout(20) # 20 seconds for recv/send
|
107
|
+
self._socket = ssl_socket
|
108
|
+
except (socket.error, ssl.SSLError) as e:
|
109
|
+
sock.close()
|
110
|
+
raise RuntimeError(f"Failed to connect to {self.tcp_address}: {e}")
|
101
111
|
|
102
112
|
def close(self) -> None:
|
103
113
|
"""Close socket connection"""
|
@@ -115,23 +125,37 @@ class TCPTransport:
|
|
115
125
|
|
116
126
|
def send_message(self, data) -> None:
|
117
127
|
"""Send a message with a prefixed header indicating its length"""
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
128
|
+
if self._socket is None:
|
129
|
+
raise RuntimeError("Cannot send message: not connected")
|
130
|
+
|
131
|
+
try:
|
132
|
+
length = len(data)
|
133
|
+
# Pack the header into 8 bytes using big-endian format
|
134
|
+
self._socket.sendall(struct.pack("!II", self.TCP_MESSAGE_MAGIC, length))
|
135
|
+
if length > 0:
|
136
|
+
self._socket.sendall(data)
|
137
|
+
except (socket.error, ssl.SSLError) as e:
|
138
|
+
self.close()
|
139
|
+
raise RuntimeError(f"Error sending message: {e}")
|
123
140
|
|
124
141
|
def receive_message(self) -> Optional[bytearray]:
|
125
142
|
"""Receive a message with a prefixed header indicating its length and validate it"""
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
143
|
+
if self._socket is None:
|
144
|
+
raise RuntimeError("Cannot receive message: not connected")
|
145
|
+
|
146
|
+
try:
|
147
|
+
# Unpack the data (big-endian format)
|
148
|
+
magic, length = struct.unpack("!II", self._recvall(self.TCP_MESSAGE_HEADER_LENGTH))
|
149
|
+
if magic != self.TCP_MESSAGE_MAGIC:
|
150
|
+
raise ValueError(f"Invalid message: incorrect magic number 0x{magic:X}.")
|
151
|
+
if length > self.TCP_MAX_BLOCK_SIZE:
|
152
|
+
raise ValueError(f"Message size {length} exceeds the maximum allowed size of {self.TCP_MAX_BLOCK_SIZE} bytes.")
|
153
|
+
if length > 0:
|
154
|
+
return self._recvall(length)
|
155
|
+
return None
|
156
|
+
except (socket.error, ssl.SSLError) as e:
|
157
|
+
self.close()
|
158
|
+
raise RuntimeError(f"Error receiving message: {e}")
|
135
159
|
|
136
160
|
def _recvall(self, length: int) -> bytearray:
|
137
161
|
"""socket recv data with fixed length
|
@@ -156,11 +180,14 @@ class TCPTransport:
|
|
156
180
|
|
157
181
|
while bytes_received < length:
|
158
182
|
# Use recv_into to read directly into our buffer
|
159
|
-
|
183
|
+
try:
|
184
|
+
chunk_size = self._socket.recv_into(view[bytes_received:], length - bytes_received)
|
160
185
|
|
161
|
-
|
162
|
-
|
186
|
+
if chunk_size == 0:
|
187
|
+
raise EOFError("Connection closed by peer")
|
163
188
|
|
164
|
-
|
189
|
+
bytes_received += chunk_size
|
190
|
+
except socket.timeout:
|
191
|
+
raise socket.timeout(f"Socket operation timed out after receiving {bytes_received}/{length} bytes")
|
165
192
|
|
166
193
|
return buffer
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.4.
|
3
|
+
Version: 1.4.8
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -28,13 +28,14 @@ Dynamic: requires-dist
|
|
28
28
|
Dynamic: requires-python
|
29
29
|
Dynamic: summary
|
30
30
|
|
31
|
-
[![language.badge]][language.url] [![standard.badge]][standard.url] [![
|
31
|
+
[![language.badge]][language.url] [![standard.badge]][standard.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
|
32
32
|
[](https://app-mesh.readthedocs.io/en/latest/?badge=latest) [](https://gitter.im/app-mesh/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
33
33
|
<a href="https://scan.coverity.com/projects/laoshanxi-app-mesh">
|
34
34
|
<img alt="Coverity Scan Build Status"
|
35
35
|
src="https://img.shields.io/coverity/scan/21528.svg"/>
|
36
36
|
</a>
|
37
37
|
[](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh)
|
38
|
+
[![release.badge]][release.url] [![pypi.badge]][pypi.url] [![npm.badge]][npm.url]
|
38
39
|
|
39
40
|
# App Mesh: Advanced Application Management Platform
|
40
41
|
|
@@ -148,7 +149,7 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
148
149
|
[standard.url]: https://en.wikipedia.org/wiki/C%2B%2B#Standardization
|
149
150
|
[standard.badge]: https://img.shields.io/badge/C%2B%2B-11%2F14%2F17-blue.svg
|
150
151
|
[release.url]: https://github.com/laoshanxi/app-mesh/releases
|
151
|
-
[release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh
|
152
|
+
[release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh?label=Github%20package
|
152
153
|
[docker.url]: https://hub.docker.com/repository/docker/laoshanxi/appmesh
|
153
154
|
[docker.badge]: https://img.shields.io/docker/pulls/laoshanxi/appmesh.svg
|
154
155
|
[cockpit.url]: https://github.com/laoshanxi/app-mesh-ui
|
@@ -157,3 +158,5 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
157
158
|
[unittest.badge]: https://img.shields.io/badge/UnitTest-Catch2-blue?logo=appveyor
|
158
159
|
[pypi.badge]: https://img.shields.io/pypi/v/appmesh?label=PyPI%3Aappmesh
|
159
160
|
[pypi.url]: https://pypi.org/project/appmesh/
|
161
|
+
[npm.badge]: https://img.shields.io/npm/v/appmesh?label=npm%3Aappmesh
|
162
|
+
[npm.url]: https://www.npmjs.com/package/appmesh
|
@@ -3,11 +3,12 @@ appmesh/app.py,sha256=9Q-SOOej-MH13BU5Dv2iTa-p-sECCJQp6ZX9DjWWmwE,10526
|
|
3
3
|
appmesh/app_output.py,sha256=JK_TMKgjvaw4n_ys_vmN5S4MyWVZpmD7NlKz_UyMIM8,1015
|
4
4
|
appmesh/app_run.py,sha256=9ISKGZ3k3kkbQvSsPfRfkOLqD9xhbqNOM7ork9F4w9c,1712
|
5
5
|
appmesh/appmesh_client.py,sha256=0ltkqHZUq094gKneYmC0bEZCP0X9kHTp9fccKdWFWP0,339
|
6
|
-
appmesh/http_client.py,sha256=
|
6
|
+
appmesh/http_client.py,sha256=S90Ldndx3wfUYE2Ln31fnKKBNXbDROQd3FAzFTEdZtA,48314
|
7
|
+
appmesh/keycloak.py,sha256=BJZ35FPO0C2wDeDhCcd6cE4ba9BF4UZ-4o5QelmbGHg,8036
|
7
8
|
appmesh/tcp_client.py,sha256=RkHl5s8jE333BJOgxJqJ_fvjbdRQza7ciV49vLT6YO4,10923
|
8
9
|
appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
|
9
|
-
appmesh/tcp_transport.py,sha256
|
10
|
-
appmesh-1.4.
|
11
|
-
appmesh-1.4.
|
12
|
-
appmesh-1.4.
|
13
|
-
appmesh-1.4.
|
10
|
+
appmesh/tcp_transport.py,sha256=-XDTQbsKL3yCbguHeW2jNqXpYgnCyHsH4rwcaJ46AS8,8645
|
11
|
+
appmesh-1.4.8.dist-info/METADATA,sha256=_Lmle-Icw-PkTf5yUvx3wHgaH0usfqAcVD2eePzjYS8,11663
|
12
|
+
appmesh-1.4.8.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
13
|
+
appmesh-1.4.8.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
14
|
+
appmesh-1.4.8.dist-info/RECORD,,
|
File without changes
|