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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyVaultRCE
3
- Version: 2.0.0
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.0.0
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
@@ -0,0 +1,2 @@
1
+ requests>=2.28.0
2
+ cryptography>=41.0.0
@@ -18,5 +18,5 @@ Quick start:
18
18
  from .manager import CodeManager
19
19
 
20
20
  __all__ = ["CodeManager"]
21
- __version__ = "2.0.0"
21
+ __version__ = "2.2.0"
22
22
  __author__ = "PyVault"
@@ -15,12 +15,36 @@ except ImportError:
15
15
  "The 'requests' library is required. Install it with: pip install requests"
16
16
  )
17
17
 
18
- _DEFAULT_BASE = os.environ.get("PYVAULT_URL", "http://localhost:5000")
19
- _MAX_CHARS = 10_000
20
- _SID_LEN = 21
21
- _HEX_SET = frozenset("0123456789abcdef")
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
- # ── Helpers ───────────────────────────────────────────────────────────────────
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 _upload_code(code: str, base_url: Optional[str], timeout: int) -> str:
117
- """Internal: push code string to /pyv/save and return the session ID."""
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
- if len(code) > _MAX_CHARS:
160
+ line_count = len(code.splitlines())
161
+ if line_count > _MAX_LINES:
121
162
  raise ValueError(
122
- f"Code is {len(code):,} characters, which exceeds the server limit of {_MAX_CHARS:,}."
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, {"code": code}, timeout)
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 = resp.json()
133
- sid = data.get("session_id", "")
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 (default: http://localhost:5000)
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: Path to the local Python file.
168
- base_url: Override the server URL.
169
- timeout: HTTP timeout in seconds.
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] Size : {len(code):,} chars", flush=True)
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.0"},
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] Size : {len(code):,} chars", flush=True)
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 and execute it locally.
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 over the network and run via exec().
249
- It is never written to disk and is not accessible after execution.
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
- f"Session '{session_id}' not found on the server. {err}"
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
- code = data.get("code", "")
358
+ encrypted = data.get("code", "")
284
359
  run_count = data.get("execution_count", "?")
285
360
 
286
- if not code:
287
- raise RuntimeError(f"Server returned empty code for session '{session_id}'.")
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']:,} chars",
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 (admin-only operation).
515
+ Replace the code stored for a Session ID.
387
516
 
388
- Requires the admin access token from the PyVault server.
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 or not admin_token.strip():
402
- raise ValueError("admin_token is required. Check the server console for your 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
- if len(code) > _MAX_CHARS:
408
- raise ValueError(f"Code is {len(code):,} chars, exceeds {_MAX_CHARS:,} limit.")
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("Admin token rejected. Check your token and try again.")
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 : {os.path.abspath(file_path)}", flush=True)
438
- print(f"[PyVault] Size : {len(code):,} chars", flush=True)
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.0.0",
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