PyVaultRCE 2.0.0__tar.gz → 2.2.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.
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/PKG-INFO +2 -1
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/PyVaultRCE.egg-info/PKG-INFO +2 -1
- pyvaultrce-2.2.0/PyVaultRCE.egg-info/requires.txt +2 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/pyvaultrce/__init__.py +1 -1
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/pyvaultrce/manager.py +189 -57
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/setup.py +2 -1
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/requires.txt +0 -1
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/PyVaultRCE.egg-info/SOURCES.txt +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/PyVaultRCE.egg-info/dependency_links.txt +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/PyVaultRCE.egg-info/top_level.txt +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/README.md +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/codemanager/__init__.py +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/codemanager/manager.py +0 -0
- {pyvaultrce-2.0.0 → pyvaultrce-2.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVaultRCE
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
5
|
Author: PyVault
|
|
6
6
|
Keywords: remote execution code hosting vault rce session
|
|
@@ -13,6 +13,7 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Requires-Dist: cryptography>=41.0.0
|
|
16
17
|
Dynamic: author
|
|
17
18
|
Dynamic: classifier
|
|
18
19
|
Dynamic: description
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVaultRCE
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
5
|
Author: PyVault
|
|
6
6
|
Keywords: remote execution code hosting vault rce session
|
|
@@ -13,6 +13,7 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Requires-Dist: cryptography>=41.0.0
|
|
16
17
|
Dynamic: author
|
|
17
18
|
Dynamic: classifier
|
|
18
19
|
Dynamic: description
|
|
@@ -15,12 +15,36 @@ except ImportError:
|
|
|
15
15
|
"The 'requests' library is required. Install it with: pip install requests"
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
try:
|
|
19
|
+
from cryptography.fernet import Fernet, InvalidToken as _InvalidToken
|
|
20
|
+
_CIPHER_KEY = b"aK3vytd8SaduKCt6D8-yL-DA2pDNOGiNLfZhnBPJebo="
|
|
21
|
+
_cipher = Fernet(_CIPHER_KEY)
|
|
22
|
+
_HAS_CRYPTO = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
_HAS_CRYPTO = False
|
|
25
|
+
|
|
26
|
+
_DEFAULT_BASE = os.environ.get(
|
|
27
|
+
"PYVAULT_URL",
|
|
28
|
+
"https://secure-code-runner--diwasreplit.replit.app"
|
|
29
|
+
)
|
|
30
|
+
_MAX_LINES = 10_000
|
|
31
|
+
_SID_LEN = 21
|
|
32
|
+
_HEX_SET = frozenset("0123456789abcdef")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Encryption helpers ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def _decrypt(data: str) -> str:
|
|
38
|
+
"""Decrypt a Fernet-encrypted payload. Falls back to plaintext on failure."""
|
|
39
|
+
if not _HAS_CRYPTO:
|
|
40
|
+
return data
|
|
41
|
+
try:
|
|
42
|
+
return _cipher.decrypt(data.encode("ascii")).decode("utf-8")
|
|
43
|
+
except (_InvalidToken, Exception):
|
|
44
|
+
return data
|
|
22
45
|
|
|
23
|
-
|
|
46
|
+
|
|
47
|
+
# ── HTTP helpers ───────────────────────────────────────────────────────────────
|
|
24
48
|
|
|
25
49
|
def _base(url: Optional[str]) -> str:
|
|
26
50
|
return (url or _DEFAULT_BASE).rstrip("/")
|
|
@@ -58,25 +82,21 @@ def _resolve_paste_url(url: str) -> str:
|
|
|
58
82
|
Supports pastebin.com, hastebin.com, dpaste.com, paste.ofcode.org,
|
|
59
83
|
GitHub Gist raw URLs, and any URL already pointing to raw text.
|
|
60
84
|
"""
|
|
61
|
-
# pastebin.com/XXXX → pastebin.com/raw/XXXX
|
|
62
85
|
url = re.sub(
|
|
63
86
|
r"(https?://pastebin\.com/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
64
87
|
r"\1raw/\2",
|
|
65
88
|
url,
|
|
66
89
|
)
|
|
67
|
-
# hastebin.com/XXXX → hastebin.com/raw/XXXX
|
|
68
90
|
url = re.sub(
|
|
69
91
|
r"(https?://hastebin\.com/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
70
92
|
r"\1raw/\2",
|
|
71
93
|
url,
|
|
72
94
|
)
|
|
73
|
-
# dpaste.com/XXXX → dpaste.com/XXXX.txt
|
|
74
95
|
url = re.sub(
|
|
75
96
|
r"(https?://dpaste\.com/)([A-Z0-9]+)(?!\.txt)(\?.*)?$",
|
|
76
97
|
r"\1\2.txt",
|
|
77
98
|
url,
|
|
78
99
|
)
|
|
79
|
-
# paste.ofcode.org/XXXX → paste.ofcode.org/raw/XXXX
|
|
80
100
|
url = re.sub(
|
|
81
101
|
r"(https?://paste\.ofcode\.org/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
82
102
|
r"\1raw/\2",
|
|
@@ -89,9 +109,9 @@ def _http_post(url: str, payload: dict, timeout: int) -> requests.Response:
|
|
|
89
109
|
try:
|
|
90
110
|
return requests.post(url, json=payload, timeout=timeout)
|
|
91
111
|
except requests.exceptions.ConnectionError:
|
|
112
|
+
_hint = _localhost_hint(url)
|
|
92
113
|
raise ConnectionError(
|
|
93
|
-
f"Unable to connect to PyVault at '{url}'.
|
|
94
|
-
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
114
|
+
f"Unable to connect to PyVault at '{url}'.{_hint}"
|
|
95
115
|
)
|
|
96
116
|
except requests.exceptions.Timeout:
|
|
97
117
|
raise TimeoutError(f"Request to '{url}' timed out after {timeout}s.")
|
|
@@ -103,9 +123,9 @@ def _http_get(url: str, timeout: int, headers: Optional[dict] = None) -> request
|
|
|
103
123
|
try:
|
|
104
124
|
return requests.get(url, timeout=timeout, headers=headers or {})
|
|
105
125
|
except requests.exceptions.ConnectionError:
|
|
126
|
+
_hint = _localhost_hint(url)
|
|
106
127
|
raise ConnectionError(
|
|
107
|
-
f"Unable to connect at '{url}'.
|
|
108
|
-
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
128
|
+
f"Unable to connect to PyVault at '{url}'.{_hint}"
|
|
109
129
|
)
|
|
110
130
|
except requests.exceptions.Timeout:
|
|
111
131
|
raise TimeoutError(f"Request to '{url}' timed out after {timeout}s.")
|
|
@@ -113,27 +133,55 @@ def _http_get(url: str, timeout: int, headers: Optional[dict] = None) -> request
|
|
|
113
133
|
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
114
134
|
|
|
115
135
|
|
|
116
|
-
def
|
|
117
|
-
""
|
|
136
|
+
def _localhost_hint(url: str) -> str:
|
|
137
|
+
if "localhost" in url or "127.0.0.1" in url:
|
|
138
|
+
return (
|
|
139
|
+
"\n\n ✗ PYVAULT_URL is not set — defaulting to localhost will not work "
|
|
140
|
+
"on a phone or remote machine.\n"
|
|
141
|
+
" → Set it before running:\n"
|
|
142
|
+
" import os\n"
|
|
143
|
+
" os.environ['PYVAULT_URL'] = 'https://secure-code-runner--diwasreplit.replit.app'\n"
|
|
144
|
+
" or in your shell:\n"
|
|
145
|
+
" export PYVAULT_URL=https://secure-code-runner--diwasreplit.replit.app"
|
|
146
|
+
)
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _upload_code(
|
|
151
|
+
code: str,
|
|
152
|
+
base_url: Optional[str],
|
|
153
|
+
timeout: int,
|
|
154
|
+
expires_in_hours: Optional[float] = None,
|
|
155
|
+
max_executions: int = 0,
|
|
156
|
+
) -> tuple:
|
|
157
|
+
"""Internal: push code string to /pyv/save. Returns (session_id, owner_token)."""
|
|
118
158
|
if not code.strip():
|
|
119
159
|
raise ValueError("Code is empty — nothing to upload.")
|
|
120
|
-
|
|
160
|
+
line_count = len(code.splitlines())
|
|
161
|
+
if line_count > _MAX_LINES:
|
|
121
162
|
raise ValueError(
|
|
122
|
-
f"Code is {
|
|
163
|
+
f"Code is {line_count:,} lines, which exceeds the server limit of {_MAX_LINES:,} lines."
|
|
123
164
|
)
|
|
165
|
+
payload = {"code": code}
|
|
166
|
+
if expires_in_hours and expires_in_hours > 0:
|
|
167
|
+
payload["expires_in_hours"] = expires_in_hours
|
|
168
|
+
if max_executions and max_executions > 0:
|
|
169
|
+
payload["max_executions"] = max_executions
|
|
170
|
+
|
|
124
171
|
url = _base(base_url) + "/pyv/save"
|
|
125
|
-
resp = _http_post(url,
|
|
172
|
+
resp = _http_post(url, payload, timeout)
|
|
126
173
|
if resp.status_code != 201:
|
|
127
174
|
try:
|
|
128
175
|
err = resp.json().get("error", resp.text)
|
|
129
176
|
except Exception:
|
|
130
177
|
err = resp.text
|
|
131
178
|
raise RuntimeError(f"Server rejected upload (HTTP {resp.status_code}): {err}")
|
|
132
|
-
data
|
|
133
|
-
sid
|
|
179
|
+
data = resp.json()
|
|
180
|
+
sid = data.get("session_id", "")
|
|
181
|
+
owner_token = data.get("owner_token", "")
|
|
134
182
|
if not _valid_sid(sid):
|
|
135
183
|
raise RuntimeError(f"Server returned unexpected Session ID: '{sid}'")
|
|
136
|
-
return sid
|
|
184
|
+
return sid, owner_token
|
|
137
185
|
|
|
138
186
|
|
|
139
187
|
# ── CodeManager ───────────────────────────────────────────────────────────────
|
|
@@ -151,7 +199,8 @@ class CodeManager:
|
|
|
151
199
|
CodeManager.run(sid)
|
|
152
200
|
|
|
153
201
|
Environment variables:
|
|
154
|
-
PYVAULT_URL — base URL of the PyVault server
|
|
202
|
+
PYVAULT_URL — base URL of the PyVault server
|
|
203
|
+
(default: https://secure-code-runner--diwasreplit.replit.app)
|
|
155
204
|
"""
|
|
156
205
|
|
|
157
206
|
@staticmethod
|
|
@@ -159,14 +208,18 @@ class CodeManager:
|
|
|
159
208
|
file_path: str,
|
|
160
209
|
base_url: Optional[str] = None,
|
|
161
210
|
timeout: int = 30,
|
|
211
|
+
expires_in_hours: Optional[float] = None,
|
|
212
|
+
max_executions: int = 0,
|
|
162
213
|
) -> str:
|
|
163
214
|
"""
|
|
164
215
|
Read a local .py file and upload it to PyVault.
|
|
165
216
|
|
|
166
217
|
Args:
|
|
167
|
-
file_path:
|
|
168
|
-
base_url:
|
|
169
|
-
timeout:
|
|
218
|
+
file_path: Path to the local Python file.
|
|
219
|
+
base_url: Override the server URL.
|
|
220
|
+
timeout: HTTP timeout in seconds.
|
|
221
|
+
expires_in_hours: Optional auto-expiry in hours from now.
|
|
222
|
+
max_executions: Max times the session can be executed (0 = unlimited).
|
|
170
223
|
|
|
171
224
|
Returns:
|
|
172
225
|
The 21-character hex Session ID.
|
|
@@ -177,10 +230,14 @@ class CodeManager:
|
|
|
177
230
|
code = _read_file(file_path)
|
|
178
231
|
if not code.strip():
|
|
179
232
|
raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
|
|
180
|
-
sid = _upload_code(code, base_url, timeout)
|
|
233
|
+
sid, owner_token = _upload_code(code, base_url, timeout, expires_in_hours, max_executions)
|
|
181
234
|
print(f"[PyVault] ✓ Uploaded — Session ID : {sid}", flush=True)
|
|
182
235
|
print(f"[PyVault] Source : {os.path.abspath(file_path)}", flush=True)
|
|
183
|
-
print(f"[PyVault]
|
|
236
|
+
print(f"[PyVault] Lines : {len(code.splitlines()):,}", flush=True)
|
|
237
|
+
if owner_token:
|
|
238
|
+
print(f"\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", flush=True)
|
|
239
|
+
print(f"[PyVault] {owner_token}", flush=True)
|
|
240
|
+
print(f"[PyVault] Use this to delete or edit your session later.\n", flush=True)
|
|
184
241
|
return sid
|
|
185
242
|
|
|
186
243
|
@staticmethod
|
|
@@ -211,7 +268,7 @@ class CodeManager:
|
|
|
211
268
|
resp = requests.get(
|
|
212
269
|
raw_url,
|
|
213
270
|
timeout=timeout,
|
|
214
|
-
headers={"User-Agent": "PyVaultRCE/2.
|
|
271
|
+
headers={"User-Agent": "PyVaultRCE/2.1"},
|
|
215
272
|
)
|
|
216
273
|
resp.raise_for_status()
|
|
217
274
|
except requests.exceptions.ConnectionError:
|
|
@@ -229,10 +286,14 @@ class CodeManager:
|
|
|
229
286
|
if not code.strip():
|
|
230
287
|
raise ValueError("Downloaded content is empty.")
|
|
231
288
|
|
|
232
|
-
sid = _upload_code(code, base_url, timeout)
|
|
289
|
+
sid, owner_token = _upload_code(code, base_url, timeout)
|
|
233
290
|
print(f"[PyVault] ✓ Uploaded from URL — Session ID : {sid}", flush=True)
|
|
234
291
|
print(f"[PyVault] Source : {paste_url}", flush=True)
|
|
235
|
-
print(f"[PyVault]
|
|
292
|
+
print(f"[PyVault] Lines : {len(code.splitlines()):,}", flush=True)
|
|
293
|
+
if owner_token:
|
|
294
|
+
print(f"\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", flush=True)
|
|
295
|
+
print(f"[PyVault] {owner_token}", flush=True)
|
|
296
|
+
print(f"[PyVault] Use this to delete or edit your session later.\n", flush=True)
|
|
236
297
|
return sid
|
|
237
298
|
|
|
238
299
|
@staticmethod
|
|
@@ -243,10 +304,11 @@ class CodeManager:
|
|
|
243
304
|
_ns: Optional[dict] = None,
|
|
244
305
|
) -> None:
|
|
245
306
|
"""
|
|
246
|
-
Fetch the code for a Session ID from PyVault
|
|
307
|
+
Fetch the encrypted code for a Session ID from PyVault, decrypt it,
|
|
308
|
+
and execute it locally.
|
|
247
309
|
|
|
248
|
-
The source code is transmitted
|
|
249
|
-
|
|
310
|
+
The source code is transmitted encrypted, decrypted in-memory, never
|
|
311
|
+
written to disk, and is not accessible after execution.
|
|
250
312
|
|
|
251
313
|
Args:
|
|
252
314
|
session_id: The 21-character hex Session ID.
|
|
@@ -269,9 +331,22 @@ class CodeManager:
|
|
|
269
331
|
err = resp.json().get("error", "Session not found.")
|
|
270
332
|
except Exception:
|
|
271
333
|
err = "Session not found."
|
|
272
|
-
raise RuntimeError(
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
raise RuntimeError(f"Session '{session_id}' not found on the server. {err}")
|
|
335
|
+
|
|
336
|
+
if resp.status_code == 410:
|
|
337
|
+
try:
|
|
338
|
+
d = resp.json()
|
|
339
|
+
err = d.get("error", "Session is no longer available.")
|
|
340
|
+
if d.get("expired"):
|
|
341
|
+
raise RuntimeError(f"Session '{session_id}' has expired. {err}")
|
|
342
|
+
if d.get("maxed"):
|
|
343
|
+
raise RuntimeError(f"Session '{session_id}' has reached its execution limit. {err}")
|
|
344
|
+
except RuntimeError:
|
|
345
|
+
raise
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
raise RuntimeError(f"Session '{session_id}' is no longer available (HTTP 410).")
|
|
349
|
+
|
|
275
350
|
if resp.status_code != 200:
|
|
276
351
|
try:
|
|
277
352
|
err = resp.json().get("error", resp.text)
|
|
@@ -280,11 +355,16 @@ class CodeManager:
|
|
|
280
355
|
raise RuntimeError(f"Server error (HTTP {resp.status_code}): {err}")
|
|
281
356
|
|
|
282
357
|
data = resp.json()
|
|
283
|
-
|
|
358
|
+
encrypted = data.get("code", "")
|
|
284
359
|
run_count = data.get("execution_count", "?")
|
|
285
360
|
|
|
286
|
-
if not
|
|
287
|
-
raise RuntimeError(f"Server returned empty
|
|
361
|
+
if not encrypted:
|
|
362
|
+
raise RuntimeError(f"Server returned empty payload for session '{session_id}'.")
|
|
363
|
+
|
|
364
|
+
code = _decrypt(encrypted)
|
|
365
|
+
|
|
366
|
+
if not code.strip():
|
|
367
|
+
raise RuntimeError(f"Decrypted code is empty for session '{session_id}'.")
|
|
288
368
|
|
|
289
369
|
print(f"[PyVault] ▶ Running session '{session_id}' (execution #{run_count})…", flush=True)
|
|
290
370
|
|
|
@@ -341,7 +421,7 @@ class CodeManager:
|
|
|
341
421
|
f"[PyVault] ℹ Session : {data['session_id']}\n"
|
|
342
422
|
f"[PyVault] Executions: {data['execution_count']}\n"
|
|
343
423
|
f"[PyVault] Created : {data['created_at']}\n"
|
|
344
|
-
f"[PyVault] Code size : {data['code_size']:,}
|
|
424
|
+
f"[PyVault] Code size : {data['code_size']:,} bytes (encrypted)",
|
|
345
425
|
flush=True,
|
|
346
426
|
)
|
|
347
427
|
return data
|
|
@@ -374,23 +454,73 @@ class CodeManager:
|
|
|
374
454
|
print(f"[PyVault] ✕ Server unreachable at '{_base(base_url)}'.", flush=True)
|
|
375
455
|
return False
|
|
376
456
|
|
|
457
|
+
@staticmethod
|
|
458
|
+
def delete(
|
|
459
|
+
session_id: str,
|
|
460
|
+
owner_token: str,
|
|
461
|
+
base_url: Optional[str] = None,
|
|
462
|
+
timeout: int = 30,
|
|
463
|
+
) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Delete your own session using the owner token received when it was created.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
session_id: The 21-character hex Session ID.
|
|
469
|
+
owner_token: The owner token printed when the session was first uploaded.
|
|
470
|
+
base_url: Override the server URL.
|
|
471
|
+
timeout: HTTP timeout in seconds.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ValueError, ConnectionError, RuntimeError
|
|
475
|
+
"""
|
|
476
|
+
_check_sid(session_id)
|
|
477
|
+
if not owner_token or not owner_token.strip():
|
|
478
|
+
raise ValueError("owner_token is required.")
|
|
479
|
+
url = _base(base_url) + f"/pyv/my/{session_id}"
|
|
480
|
+
try:
|
|
481
|
+
resp = requests.delete(
|
|
482
|
+
url,
|
|
483
|
+
headers={"X-Owner-Token": owner_token.strip()},
|
|
484
|
+
timeout=timeout,
|
|
485
|
+
)
|
|
486
|
+
except requests.exceptions.ConnectionError:
|
|
487
|
+
raise ConnectionError(f"Unable to connect to PyVault at '{url}'.")
|
|
488
|
+
except requests.exceptions.Timeout:
|
|
489
|
+
raise TimeoutError(f"Request timed out after {timeout}s.")
|
|
490
|
+
except requests.exceptions.RequestException as exc:
|
|
491
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
492
|
+
|
|
493
|
+
if resp.status_code == 403:
|
|
494
|
+
raise RuntimeError("Owner token rejected — check the token and try again.")
|
|
495
|
+
if resp.status_code == 404:
|
|
496
|
+
raise RuntimeError(f"Session '{session_id}' not found.")
|
|
497
|
+
if resp.status_code != 200:
|
|
498
|
+
try:
|
|
499
|
+
err = resp.json().get("error", resp.text)
|
|
500
|
+
except Exception:
|
|
501
|
+
err = resp.text
|
|
502
|
+
raise RuntimeError(f"Server error (HTTP {resp.status_code}): {err}")
|
|
503
|
+
print(f"[PyVault] ✓ Session '{session_id}' deleted.", flush=True)
|
|
504
|
+
|
|
377
505
|
@staticmethod
|
|
378
506
|
def edit(
|
|
379
507
|
session_id: str,
|
|
380
508
|
file_path: str,
|
|
381
|
-
admin_token: str,
|
|
509
|
+
admin_token: str = "",
|
|
510
|
+
owner_token: str = "",
|
|
382
511
|
base_url: Optional[str] = None,
|
|
383
512
|
timeout: int = 30,
|
|
384
513
|
) -> None:
|
|
385
514
|
"""
|
|
386
|
-
Replace the code stored for a Session ID
|
|
515
|
+
Replace the code stored for a Session ID.
|
|
387
516
|
|
|
388
|
-
|
|
517
|
+
Provide either admin_token (admin access) or owner_token (creator access).
|
|
389
518
|
|
|
390
519
|
Args:
|
|
391
520
|
session_id: The 21-character hex Session ID to update.
|
|
392
521
|
file_path: Path to the .py file with the new code.
|
|
393
522
|
admin_token: Admin access token (from server console on first start).
|
|
523
|
+
owner_token: Owner token printed when the session was first created.
|
|
394
524
|
base_url: Override the server URL.
|
|
395
525
|
timeout: HTTP timeout in seconds.
|
|
396
526
|
|
|
@@ -398,23 +528,25 @@ class CodeManager:
|
|
|
398
528
|
ValueError, FileNotFoundError, ConnectionError, RuntimeError
|
|
399
529
|
"""
|
|
400
530
|
_check_sid(session_id)
|
|
401
|
-
if not admin_token
|
|
402
|
-
raise ValueError("admin_token
|
|
531
|
+
if not admin_token.strip() and not owner_token.strip():
|
|
532
|
+
raise ValueError("Provide admin_token or owner_token.")
|
|
403
533
|
|
|
404
534
|
code = _read_file(file_path)
|
|
405
535
|
if not code.strip():
|
|
406
536
|
raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
|
|
407
|
-
|
|
408
|
-
|
|
537
|
+
line_count = len(code.splitlines())
|
|
538
|
+
if line_count > _MAX_LINES:
|
|
539
|
+
raise ValueError(f"Code is {line_count:,} lines, exceeds {_MAX_LINES:,} line limit.")
|
|
540
|
+
|
|
541
|
+
if owner_token.strip():
|
|
542
|
+
url = _base(base_url) + f"/pyv/my/{session_id}"
|
|
543
|
+
headers = {"Content-Type": "application/json", "X-Owner-Token": owner_token.strip()}
|
|
544
|
+
else:
|
|
545
|
+
url = _base(base_url) + f"/pyv/edit/{session_id}"
|
|
546
|
+
headers = {"Content-Type": "application/json", "X-Admin-Token": admin_token.strip()}
|
|
409
547
|
|
|
410
|
-
url = _base(base_url) + f"/pyv/edit/{session_id}"
|
|
411
548
|
try:
|
|
412
|
-
resp = requests.put(
|
|
413
|
-
url,
|
|
414
|
-
json={"code": code},
|
|
415
|
-
headers={"X-Admin-Token": admin_token},
|
|
416
|
-
timeout=timeout,
|
|
417
|
-
)
|
|
549
|
+
resp = requests.put(url, json={"code": code}, headers=headers, timeout=timeout)
|
|
418
550
|
except requests.exceptions.ConnectionError:
|
|
419
551
|
raise ConnectionError(f"Unable to connect to PyVault at '{url}'.")
|
|
420
552
|
except requests.exceptions.Timeout:
|
|
@@ -423,7 +555,7 @@ class CodeManager:
|
|
|
423
555
|
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
424
556
|
|
|
425
557
|
if resp.status_code == 403:
|
|
426
|
-
raise RuntimeError("
|
|
558
|
+
raise RuntimeError("Token rejected. Check your admin_token or owner_token.")
|
|
427
559
|
if resp.status_code == 404:
|
|
428
560
|
raise RuntimeError(f"Session '{session_id}' not found.")
|
|
429
561
|
if resp.status_code != 200:
|
|
@@ -434,5 +566,5 @@ class CodeManager:
|
|
|
434
566
|
raise RuntimeError(f"Server rejected edit (HTTP {resp.status_code}): {err}")
|
|
435
567
|
|
|
436
568
|
print(f"[PyVault] ✓ Session '{session_id}' updated.", flush=True)
|
|
437
|
-
print(f"[PyVault] File
|
|
438
|
-
print(f"[PyVault]
|
|
569
|
+
print(f"[PyVault] File : {os.path.abspath(file_path)}", flush=True)
|
|
570
|
+
print(f"[PyVault] Lines : {line_count:,}", flush=True)
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="PyVaultRCE",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.2.0",
|
|
9
9
|
author="PyVault",
|
|
10
10
|
description="Remote Code Execution & Hosting client for PyVault — source never exposed",
|
|
11
11
|
long_description=long_description,
|
|
@@ -14,6 +14,7 @@ setup(
|
|
|
14
14
|
python_requires=">=3.8",
|
|
15
15
|
install_requires=[
|
|
16
16
|
"requests>=2.28.0",
|
|
17
|
+
"cryptography>=41.0.0",
|
|
17
18
|
],
|
|
18
19
|
classifiers=[
|
|
19
20
|
"Programming Language :: Python :: 3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
requests>=2.28.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|