PyVaultRCE 2.0.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/PKG-INFO +107 -0
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/PKG-INFO +107 -0
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/SOURCES.txt +11 -0
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/dependency_links.txt +1 -0
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/requires.txt +1 -0
- pyvaultrce-2.0.0/PyVaultRCE.egg-info/top_level.txt +2 -0
- pyvaultrce-2.0.0/README.md +83 -0
- pyvaultrce-2.0.0/codemanager/__init__.py +25 -0
- pyvaultrce-2.0.0/codemanager/manager.py +318 -0
- pyvaultrce-2.0.0/pyvaultrce/__init__.py +22 -0
- pyvaultrce-2.0.0/pyvaultrce/manager.py +438 -0
- pyvaultrce-2.0.0/setup.cfg +4 -0
- pyvaultrce-2.0.0/setup.py +28 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyVaultRCE
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
|
+
Author: PyVault
|
|
6
|
+
Keywords: remote execution code hosting vault rce session
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: keywords
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# PyVaultRCE
|
|
26
|
+
|
|
27
|
+
Remote Code Execution & Hosting client for **PyVault**.
|
|
28
|
+
|
|
29
|
+
Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install PyVaultRCE
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pyvaultrce import CodeManager
|
|
41
|
+
|
|
42
|
+
# Upload a local .py file → get Session ID
|
|
43
|
+
sid = CodeManager.enc("my_script.py")
|
|
44
|
+
# [PyVault] ✓ Uploaded — Session ID : 3a7f1c9b2d0e8a5f41c7e
|
|
45
|
+
|
|
46
|
+
# Upload from a paste service URL
|
|
47
|
+
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
48
|
+
|
|
49
|
+
# Execute remotely (source stays on server)
|
|
50
|
+
CodeManager.run(sid)
|
|
51
|
+
|
|
52
|
+
# Check metadata without incrementing execution counter
|
|
53
|
+
CodeManager.info(sid)
|
|
54
|
+
|
|
55
|
+
# Verify server is up
|
|
56
|
+
CodeManager.ping()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export PYVAULT_URL=https://your-server.replit.app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
72
|
+
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
73
|
+
| `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
|
|
74
|
+
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
75
|
+
| `CodeManager.ping()` | Check server connectivity |
|
|
76
|
+
| `CodeManager.edit(session_id, file_path, admin_token)` | Replace stored code (admin only) |
|
|
77
|
+
|
|
78
|
+
## Supported Paste Services (`enc_url`)
|
|
79
|
+
|
|
80
|
+
- `pastebin.com`
|
|
81
|
+
- `hastebin.com`
|
|
82
|
+
- `dpaste.com`
|
|
83
|
+
- `paste.ofcode.org`
|
|
84
|
+
- GitHub raw URLs
|
|
85
|
+
- Any URL returning raw Python text
|
|
86
|
+
|
|
87
|
+
## Error Handling
|
|
88
|
+
|
|
89
|
+
| Exception | When |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `FileNotFoundError` | Local file not found |
|
|
92
|
+
| `ValueError` | Invalid Session ID or empty file |
|
|
93
|
+
| `ConnectionError` | Server unreachable |
|
|
94
|
+
| `TimeoutError` | Request timed out |
|
|
95
|
+
| `RuntimeError` | Server error (404, 403, 500, etc.) |
|
|
96
|
+
| `SyntaxError` | Remote code syntax error (during `run`) |
|
|
97
|
+
|
|
98
|
+
## Security
|
|
99
|
+
|
|
100
|
+
- Session IDs are 21-character cryptographically random hex strings
|
|
101
|
+
- Source code is never written to disk on the client
|
|
102
|
+
- Edit operations require a server-side admin token
|
|
103
|
+
- Regular users cannot modify stored code
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyVaultRCE
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
|
+
Author: PyVault
|
|
6
|
+
Keywords: remote execution code hosting vault rce session
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: keywords
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# PyVaultRCE
|
|
26
|
+
|
|
27
|
+
Remote Code Execution & Hosting client for **PyVault**.
|
|
28
|
+
|
|
29
|
+
Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install PyVaultRCE
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pyvaultrce import CodeManager
|
|
41
|
+
|
|
42
|
+
# Upload a local .py file → get Session ID
|
|
43
|
+
sid = CodeManager.enc("my_script.py")
|
|
44
|
+
# [PyVault] ✓ Uploaded — Session ID : 3a7f1c9b2d0e8a5f41c7e
|
|
45
|
+
|
|
46
|
+
# Upload from a paste service URL
|
|
47
|
+
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
48
|
+
|
|
49
|
+
# Execute remotely (source stays on server)
|
|
50
|
+
CodeManager.run(sid)
|
|
51
|
+
|
|
52
|
+
# Check metadata without incrementing execution counter
|
|
53
|
+
CodeManager.info(sid)
|
|
54
|
+
|
|
55
|
+
# Verify server is up
|
|
56
|
+
CodeManager.ping()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export PYVAULT_URL=https://your-server.replit.app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
72
|
+
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
73
|
+
| `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
|
|
74
|
+
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
75
|
+
| `CodeManager.ping()` | Check server connectivity |
|
|
76
|
+
| `CodeManager.edit(session_id, file_path, admin_token)` | Replace stored code (admin only) |
|
|
77
|
+
|
|
78
|
+
## Supported Paste Services (`enc_url`)
|
|
79
|
+
|
|
80
|
+
- `pastebin.com`
|
|
81
|
+
- `hastebin.com`
|
|
82
|
+
- `dpaste.com`
|
|
83
|
+
- `paste.ofcode.org`
|
|
84
|
+
- GitHub raw URLs
|
|
85
|
+
- Any URL returning raw Python text
|
|
86
|
+
|
|
87
|
+
## Error Handling
|
|
88
|
+
|
|
89
|
+
| Exception | When |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `FileNotFoundError` | Local file not found |
|
|
92
|
+
| `ValueError` | Invalid Session ID or empty file |
|
|
93
|
+
| `ConnectionError` | Server unreachable |
|
|
94
|
+
| `TimeoutError` | Request timed out |
|
|
95
|
+
| `RuntimeError` | Server error (404, 403, 500, etc.) |
|
|
96
|
+
| `SyntaxError` | Remote code syntax error (during `run`) |
|
|
97
|
+
|
|
98
|
+
## Security
|
|
99
|
+
|
|
100
|
+
- Session IDs are 21-character cryptographically random hex strings
|
|
101
|
+
- Source code is never written to disk on the client
|
|
102
|
+
- Edit operations require a server-side admin token
|
|
103
|
+
- Regular users cannot modify stored code
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
PyVaultRCE.egg-info/PKG-INFO
|
|
4
|
+
PyVaultRCE.egg-info/SOURCES.txt
|
|
5
|
+
PyVaultRCE.egg-info/dependency_links.txt
|
|
6
|
+
PyVaultRCE.egg-info/requires.txt
|
|
7
|
+
PyVaultRCE.egg-info/top_level.txt
|
|
8
|
+
codemanager/__init__.py
|
|
9
|
+
codemanager/manager.py
|
|
10
|
+
pyvaultrce/__init__.py
|
|
11
|
+
pyvaultrce/manager.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# PyVaultRCE
|
|
2
|
+
|
|
3
|
+
Remote Code Execution & Hosting client for **PyVault**.
|
|
4
|
+
|
|
5
|
+
Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install PyVaultRCE
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pyvaultrce import CodeManager
|
|
17
|
+
|
|
18
|
+
# Upload a local .py file → get Session ID
|
|
19
|
+
sid = CodeManager.enc("my_script.py")
|
|
20
|
+
# [PyVault] ✓ Uploaded — Session ID : 3a7f1c9b2d0e8a5f41c7e
|
|
21
|
+
|
|
22
|
+
# Upload from a paste service URL
|
|
23
|
+
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
24
|
+
|
|
25
|
+
# Execute remotely (source stays on server)
|
|
26
|
+
CodeManager.run(sid)
|
|
27
|
+
|
|
28
|
+
# Check metadata without incrementing execution counter
|
|
29
|
+
CodeManager.info(sid)
|
|
30
|
+
|
|
31
|
+
# Verify server is up
|
|
32
|
+
CodeManager.ping()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export PYVAULT_URL=https://your-server.replit.app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API Reference
|
|
44
|
+
|
|
45
|
+
| Method | Description |
|
|
46
|
+
|--------|-------------|
|
|
47
|
+
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
48
|
+
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
49
|
+
| `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
|
|
50
|
+
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
51
|
+
| `CodeManager.ping()` | Check server connectivity |
|
|
52
|
+
| `CodeManager.edit(session_id, file_path, admin_token)` | Replace stored code (admin only) |
|
|
53
|
+
|
|
54
|
+
## Supported Paste Services (`enc_url`)
|
|
55
|
+
|
|
56
|
+
- `pastebin.com`
|
|
57
|
+
- `hastebin.com`
|
|
58
|
+
- `dpaste.com`
|
|
59
|
+
- `paste.ofcode.org`
|
|
60
|
+
- GitHub raw URLs
|
|
61
|
+
- Any URL returning raw Python text
|
|
62
|
+
|
|
63
|
+
## Error Handling
|
|
64
|
+
|
|
65
|
+
| Exception | When |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `FileNotFoundError` | Local file not found |
|
|
68
|
+
| `ValueError` | Invalid Session ID or empty file |
|
|
69
|
+
| `ConnectionError` | Server unreachable |
|
|
70
|
+
| `TimeoutError` | Request timed out |
|
|
71
|
+
| `RuntimeError` | Server error (404, 403, 500, etc.) |
|
|
72
|
+
| `SyntaxError` | Remote code syntax error (during `run`) |
|
|
73
|
+
|
|
74
|
+
## Security
|
|
75
|
+
|
|
76
|
+
- Session IDs are 21-character cryptographically random hex strings
|
|
77
|
+
- Source code is never written to disk on the client
|
|
78
|
+
- Edit operations require a server-side admin token
|
|
79
|
+
- Regular users cannot modify stored code
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codemanager — Remote Code Execution & Hosting PyPI Module
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Provides the CodeManager class to upload, execute, and edit
|
|
5
|
+
Python scripts hosted on a PyVault backend — without exposing
|
|
6
|
+
the source code to the client.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from codemanager import CodeManager
|
|
10
|
+
|
|
11
|
+
# Upload a local script and get a Session ID
|
|
12
|
+
session_id = CodeManager.enc("my_script.py")
|
|
13
|
+
|
|
14
|
+
# Execute the remote script by Session ID
|
|
15
|
+
CodeManager.run(session_id)
|
|
16
|
+
|
|
17
|
+
# Push an updated version of the script
|
|
18
|
+
CodeManager.edit(session_id, "my_script_v2.py")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .manager import CodeManager
|
|
22
|
+
|
|
23
|
+
__all__ = ["CodeManager"]
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
__author__ = "PyVault"
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
manager.py — CodeManager implementation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
import textwrap
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import requests
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"The 'requests' library is required. Install it with: pip install requests"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_DEFAULT_BASE_URL = os.environ.get("PYVAULT_URL", "http://localhost:5000")
|
|
19
|
+
|
|
20
|
+
# ── Validation helpers ───────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def _is_valid_session_id(session_id: str) -> bool:
|
|
23
|
+
return (
|
|
24
|
+
isinstance(session_id, str)
|
|
25
|
+
and len(session_id) == 21
|
|
26
|
+
and all(c in "0123456789abcdef" for c in session_id)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _validate_session_id(session_id: str) -> None:
|
|
31
|
+
if not _is_valid_session_id(session_id):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Invalid Session ID: '{session_id}'. "
|
|
34
|
+
"A Session ID must be exactly 21 lowercase hexadecimal characters."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_base_url(base_url: Optional[str]) -> str:
|
|
39
|
+
url = (base_url or _DEFAULT_BASE_URL).rstrip("/")
|
|
40
|
+
return url
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── CodeManager ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
class CodeManager:
|
|
46
|
+
"""
|
|
47
|
+
Remote code management client for PyVault.
|
|
48
|
+
|
|
49
|
+
All methods are static so no instantiation is needed:
|
|
50
|
+
|
|
51
|
+
from codemanager import CodeManager
|
|
52
|
+
sid = CodeManager.enc("script.py")
|
|
53
|
+
CodeManager.run(sid)
|
|
54
|
+
CodeManager.edit(sid, "script_v2.py")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def enc(
|
|
59
|
+
file_path: str,
|
|
60
|
+
base_url: Optional[str] = None,
|
|
61
|
+
timeout: int = 30,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Read a local Python file and upload it to the PyVault backend.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
file_path: Path to the local .py file to upload.
|
|
68
|
+
base_url: Override the server URL (default: PYVAULT_URL env or localhost:5000).
|
|
69
|
+
timeout: HTTP timeout in seconds.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The 21-character hex Session ID assigned to this upload.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
FileNotFoundError: If file_path does not exist.
|
|
76
|
+
ValueError: If the file is empty.
|
|
77
|
+
ConnectionError: If the server is unreachable.
|
|
78
|
+
RuntimeError: If the server returns an error response.
|
|
79
|
+
"""
|
|
80
|
+
path = os.path.abspath(file_path)
|
|
81
|
+
if not os.path.isfile(path):
|
|
82
|
+
raise FileNotFoundError(
|
|
83
|
+
f"File not found: '{file_path}'. "
|
|
84
|
+
"Ensure the path is correct and the file exists."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
89
|
+
code = fh.read()
|
|
90
|
+
except UnicodeDecodeError:
|
|
91
|
+
with open(path, "r", encoding="latin-1") as fh:
|
|
92
|
+
code = fh.read()
|
|
93
|
+
|
|
94
|
+
if not code.strip():
|
|
95
|
+
raise ValueError(f"The file '{file_path}' is empty. Nothing to upload.")
|
|
96
|
+
|
|
97
|
+
url = _get_base_url(base_url) + "/pyv/save"
|
|
98
|
+
try:
|
|
99
|
+
response = requests.post(
|
|
100
|
+
url,
|
|
101
|
+
json={"code": code},
|
|
102
|
+
timeout=timeout,
|
|
103
|
+
)
|
|
104
|
+
except requests.exceptions.ConnectionError:
|
|
105
|
+
raise ConnectionError(
|
|
106
|
+
f"Unable to connect to PyVault at '{url}'. "
|
|
107
|
+
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
108
|
+
)
|
|
109
|
+
except requests.exceptions.Timeout:
|
|
110
|
+
raise TimeoutError(
|
|
111
|
+
f"Request to '{url}' timed out after {timeout}s. "
|
|
112
|
+
"Try increasing the timeout parameter or check the server."
|
|
113
|
+
)
|
|
114
|
+
except requests.exceptions.RequestException as exc:
|
|
115
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
116
|
+
|
|
117
|
+
if response.status_code != 201:
|
|
118
|
+
try:
|
|
119
|
+
err = response.json().get("error", response.text)
|
|
120
|
+
except Exception:
|
|
121
|
+
err = response.text
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"Server rejected the upload (HTTP {response.status_code}): {err}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
data = response.json()
|
|
127
|
+
session_id = data.get("session_id", "")
|
|
128
|
+
if not _is_valid_session_id(session_id):
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
f"Server returned an unexpected Session ID format: '{session_id}'"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
print(f"[PyVault] ✓ Code uploaded successfully.")
|
|
134
|
+
print(f"[PyVault] Session ID : {session_id}")
|
|
135
|
+
print(f"[PyVault] File : {path}")
|
|
136
|
+
print(f"[PyVault] Size : {len(code)} characters")
|
|
137
|
+
return session_id
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def run(
|
|
141
|
+
session_id: str,
|
|
142
|
+
base_url: Optional[str] = None,
|
|
143
|
+
timeout: int = 30,
|
|
144
|
+
globals_dict: Optional[dict] = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Fetch the code for a Session ID from PyVault and execute it locally.
|
|
148
|
+
|
|
149
|
+
The source code is fetched over the network and run via exec() —
|
|
150
|
+
the caller never has direct access to the source file on disk.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
session_id: The 21-character hex Session ID.
|
|
154
|
+
base_url: Override the server URL.
|
|
155
|
+
timeout: HTTP timeout in seconds.
|
|
156
|
+
globals_dict: Optional globals dict passed to exec(). If None,
|
|
157
|
+
a clean namespace is used to avoid polluting the
|
|
158
|
+
caller's namespace.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If the Session ID format is invalid.
|
|
162
|
+
ConnectionError: If the server is unreachable.
|
|
163
|
+
RuntimeError: If the session is not found or the server errors.
|
|
164
|
+
Exception: Re-raises any exception that occurs during exec().
|
|
165
|
+
"""
|
|
166
|
+
_validate_session_id(session_id)
|
|
167
|
+
|
|
168
|
+
url = _get_base_url(base_url) + f"/pyv/get/{session_id}"
|
|
169
|
+
try:
|
|
170
|
+
response = requests.get(url, timeout=timeout)
|
|
171
|
+
except requests.exceptions.ConnectionError:
|
|
172
|
+
raise ConnectionError(
|
|
173
|
+
f"Unable to connect to PyVault at '{url}'. "
|
|
174
|
+
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
175
|
+
)
|
|
176
|
+
except requests.exceptions.Timeout:
|
|
177
|
+
raise TimeoutError(
|
|
178
|
+
f"Request timed out after {timeout}s while fetching session '{session_id}'."
|
|
179
|
+
)
|
|
180
|
+
except requests.exceptions.RequestException as exc:
|
|
181
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
182
|
+
|
|
183
|
+
if response.status_code == 404:
|
|
184
|
+
try:
|
|
185
|
+
err = response.json().get("error", "Session not found.")
|
|
186
|
+
except Exception:
|
|
187
|
+
err = "Session not found."
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
f"Session ID '{session_id}' was not found on the server. "
|
|
190
|
+
f"Server message: {err}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if response.status_code != 200:
|
|
194
|
+
try:
|
|
195
|
+
err = response.json().get("error", response.text)
|
|
196
|
+
except Exception:
|
|
197
|
+
err = response.text
|
|
198
|
+
raise RuntimeError(
|
|
199
|
+
f"Server error (HTTP {response.status_code}): {err}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
data = response.json()
|
|
203
|
+
code = data.get("code", "")
|
|
204
|
+
exec_count = data.get("execution_count", "?")
|
|
205
|
+
|
|
206
|
+
if not code:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Server returned empty code for session '{session_id}'."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
print(f"[PyVault] Executing session '{session_id}' (run #{exec_count})…")
|
|
212
|
+
|
|
213
|
+
namespace = globals_dict if globals_dict is not None else {
|
|
214
|
+
"__name__": "__pyvault__",
|
|
215
|
+
"__builtins__": __builtins__,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
compiled = compile(code, f"<pyvault:{session_id}>", "exec")
|
|
220
|
+
exec(compiled, namespace)
|
|
221
|
+
except SyntaxError as exc:
|
|
222
|
+
print(f"\n[PyVault] ✕ Syntax error in remote code:", file=sys.stderr)
|
|
223
|
+
print(f" Line {exc.lineno}: {exc.msg}", file=sys.stderr)
|
|
224
|
+
if exc.text:
|
|
225
|
+
print(f" >>> {exc.text.strip()}", file=sys.stderr)
|
|
226
|
+
raise
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
print(f"\n[PyVault] ✕ Runtime error during execution:", file=sys.stderr)
|
|
229
|
+
tb_lines = traceback.format_exc().splitlines()
|
|
230
|
+
for line in tb_lines:
|
|
231
|
+
print(f" {line}", file=sys.stderr)
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
print(f"[PyVault] ✓ Execution complete.")
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def edit(
|
|
238
|
+
session_id: str,
|
|
239
|
+
file_path: str,
|
|
240
|
+
base_url: Optional[str] = None,
|
|
241
|
+
timeout: int = 30,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Read a local Python file and push it as an update to an existing session.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
session_id: The 21-character hex Session ID to update.
|
|
248
|
+
file_path: Path to the local .py file with the updated code.
|
|
249
|
+
base_url: Override the server URL.
|
|
250
|
+
timeout: HTTP timeout in seconds.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If the Session ID format is invalid.
|
|
254
|
+
FileNotFoundError: If file_path does not exist.
|
|
255
|
+
ValueError: If the file is empty.
|
|
256
|
+
ConnectionError: If the server is unreachable.
|
|
257
|
+
RuntimeError: If the session is not found or the server errors.
|
|
258
|
+
"""
|
|
259
|
+
_validate_session_id(session_id)
|
|
260
|
+
|
|
261
|
+
path = os.path.abspath(file_path)
|
|
262
|
+
if not os.path.isfile(path):
|
|
263
|
+
raise FileNotFoundError(
|
|
264
|
+
f"File not found: '{file_path}'. "
|
|
265
|
+
"Ensure the path is correct and the file exists."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
270
|
+
code = fh.read()
|
|
271
|
+
except UnicodeDecodeError:
|
|
272
|
+
with open(path, "r", encoding="latin-1") as fh:
|
|
273
|
+
code = fh.read()
|
|
274
|
+
|
|
275
|
+
if not code.strip():
|
|
276
|
+
raise ValueError(f"The file '{file_path}' is empty. Nothing to upload.")
|
|
277
|
+
|
|
278
|
+
url = _get_base_url(base_url) + f"/pyv/edit/{session_id}"
|
|
279
|
+
try:
|
|
280
|
+
response = requests.put(
|
|
281
|
+
url,
|
|
282
|
+
json={"code": code},
|
|
283
|
+
timeout=timeout,
|
|
284
|
+
)
|
|
285
|
+
except requests.exceptions.ConnectionError:
|
|
286
|
+
raise ConnectionError(
|
|
287
|
+
f"Unable to connect to PyVault at '{url}'. "
|
|
288
|
+
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
289
|
+
)
|
|
290
|
+
except requests.exceptions.Timeout:
|
|
291
|
+
raise TimeoutError(
|
|
292
|
+
f"Request timed out after {timeout}s while editing session '{session_id}'."
|
|
293
|
+
)
|
|
294
|
+
except requests.exceptions.RequestException as exc:
|
|
295
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
296
|
+
|
|
297
|
+
if response.status_code == 404:
|
|
298
|
+
try:
|
|
299
|
+
err = response.json().get("error", "Session not found.")
|
|
300
|
+
except Exception:
|
|
301
|
+
err = "Session not found."
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"Session ID '{session_id}' was not found. Cannot edit a non-existent session. "
|
|
304
|
+
f"Server message: {err}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if response.status_code != 200:
|
|
308
|
+
try:
|
|
309
|
+
err = response.json().get("error", response.text)
|
|
310
|
+
except Exception:
|
|
311
|
+
err = response.text
|
|
312
|
+
raise RuntimeError(
|
|
313
|
+
f"Server rejected the edit (HTTP {response.status_code}): {err}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
print(f"[PyVault] ✓ Session '{session_id}' updated successfully.")
|
|
317
|
+
print(f"[PyVault] File : {path}")
|
|
318
|
+
print(f"[PyVault] Size : {len(code)} characters")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyvaultrce — Remote Code Execution & Hosting Module for PyVault
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Upload Python scripts to a PyVault server and execute them remotely.
|
|
5
|
+
The source code never leaves the server — clients only ever hold a
|
|
6
|
+
21-character hex Session ID.
|
|
7
|
+
|
|
8
|
+
Quick start:
|
|
9
|
+
from pyvaultrce import CodeManager
|
|
10
|
+
|
|
11
|
+
sid = CodeManager.enc("script.py") # upload local file
|
|
12
|
+
sid = CodeManager.enc_url("https://...") # upload from paste URL
|
|
13
|
+
CodeManager.run(sid) # execute remotely
|
|
14
|
+
CodeManager.info(sid) # check metadata
|
|
15
|
+
CodeManager.ping() # verify server is up
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .manager import CodeManager
|
|
19
|
+
|
|
20
|
+
__all__ = ["CodeManager"]
|
|
21
|
+
__version__ = "2.0.0"
|
|
22
|
+
__author__ = "PyVault"
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
manager.py — CodeManager implementation for PyVaultRCE
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import requests
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"The 'requests' library is required. Install it with: pip install requests"
|
|
16
|
+
)
|
|
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")
|
|
22
|
+
|
|
23
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def _base(url: Optional[str]) -> str:
|
|
26
|
+
return (url or _DEFAULT_BASE).rstrip("/")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _valid_sid(sid: str) -> bool:
|
|
30
|
+
return isinstance(sid, str) and len(sid) == _SID_LEN and all(c in _HEX_SET for c in sid)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_sid(sid: str) -> None:
|
|
34
|
+
if not _valid_sid(sid):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Invalid Session ID: '{sid}'. "
|
|
37
|
+
f"Must be exactly {_SID_LEN} lowercase hexadecimal characters."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_file(path: str) -> str:
|
|
42
|
+
path = os.path.abspath(path)
|
|
43
|
+
if not os.path.isfile(path):
|
|
44
|
+
raise FileNotFoundError(
|
|
45
|
+
f"File not found: '{path}'. Check the path and try again."
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
49
|
+
return fh.read()
|
|
50
|
+
except UnicodeDecodeError:
|
|
51
|
+
with open(path, "r", encoding="latin-1") as fh:
|
|
52
|
+
return fh.read()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_paste_url(url: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Rewrite common paste service URLs to their raw endpoints.
|
|
58
|
+
Supports pastebin.com, hastebin.com, dpaste.com, paste.ofcode.org,
|
|
59
|
+
GitHub Gist raw URLs, and any URL already pointing to raw text.
|
|
60
|
+
"""
|
|
61
|
+
# pastebin.com/XXXX → pastebin.com/raw/XXXX
|
|
62
|
+
url = re.sub(
|
|
63
|
+
r"(https?://pastebin\.com/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
64
|
+
r"\1raw/\2",
|
|
65
|
+
url,
|
|
66
|
+
)
|
|
67
|
+
# hastebin.com/XXXX → hastebin.com/raw/XXXX
|
|
68
|
+
url = re.sub(
|
|
69
|
+
r"(https?://hastebin\.com/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
70
|
+
r"\1raw/\2",
|
|
71
|
+
url,
|
|
72
|
+
)
|
|
73
|
+
# dpaste.com/XXXX → dpaste.com/XXXX.txt
|
|
74
|
+
url = re.sub(
|
|
75
|
+
r"(https?://dpaste\.com/)([A-Z0-9]+)(?!\.txt)(\?.*)?$",
|
|
76
|
+
r"\1\2.txt",
|
|
77
|
+
url,
|
|
78
|
+
)
|
|
79
|
+
# paste.ofcode.org/XXXX → paste.ofcode.org/raw/XXXX
|
|
80
|
+
url = re.sub(
|
|
81
|
+
r"(https?://paste\.ofcode\.org/)(?!raw/)([A-Za-z0-9]+)(\?.*)?$",
|
|
82
|
+
r"\1raw/\2",
|
|
83
|
+
url,
|
|
84
|
+
)
|
|
85
|
+
return url
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _http_post(url: str, payload: dict, timeout: int) -> requests.Response:
|
|
89
|
+
try:
|
|
90
|
+
return requests.post(url, json=payload, timeout=timeout)
|
|
91
|
+
except requests.exceptions.ConnectionError:
|
|
92
|
+
raise ConnectionError(
|
|
93
|
+
f"Unable to connect to PyVault at '{url}'. "
|
|
94
|
+
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
95
|
+
)
|
|
96
|
+
except requests.exceptions.Timeout:
|
|
97
|
+
raise TimeoutError(f"Request to '{url}' timed out after {timeout}s.")
|
|
98
|
+
except requests.exceptions.RequestException as exc:
|
|
99
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _http_get(url: str, timeout: int, headers: Optional[dict] = None) -> requests.Response:
|
|
103
|
+
try:
|
|
104
|
+
return requests.get(url, timeout=timeout, headers=headers or {})
|
|
105
|
+
except requests.exceptions.ConnectionError:
|
|
106
|
+
raise ConnectionError(
|
|
107
|
+
f"Unable to connect at '{url}'. "
|
|
108
|
+
"Ensure the server is running and PYVAULT_URL is set correctly."
|
|
109
|
+
)
|
|
110
|
+
except requests.exceptions.Timeout:
|
|
111
|
+
raise TimeoutError(f"Request to '{url}' timed out after {timeout}s.")
|
|
112
|
+
except requests.exceptions.RequestException as exc:
|
|
113
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
114
|
+
|
|
115
|
+
|
|
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."""
|
|
118
|
+
if not code.strip():
|
|
119
|
+
raise ValueError("Code is empty — nothing to upload.")
|
|
120
|
+
if len(code) > _MAX_CHARS:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Code is {len(code):,} characters, which exceeds the server limit of {_MAX_CHARS:,}."
|
|
123
|
+
)
|
|
124
|
+
url = _base(base_url) + "/pyv/save"
|
|
125
|
+
resp = _http_post(url, {"code": code}, timeout)
|
|
126
|
+
if resp.status_code != 201:
|
|
127
|
+
try:
|
|
128
|
+
err = resp.json().get("error", resp.text)
|
|
129
|
+
except Exception:
|
|
130
|
+
err = resp.text
|
|
131
|
+
raise RuntimeError(f"Server rejected upload (HTTP {resp.status_code}): {err}")
|
|
132
|
+
data = resp.json()
|
|
133
|
+
sid = data.get("session_id", "")
|
|
134
|
+
if not _valid_sid(sid):
|
|
135
|
+
raise RuntimeError(f"Server returned unexpected Session ID: '{sid}'")
|
|
136
|
+
return sid
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── CodeManager ───────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
class CodeManager:
|
|
142
|
+
"""
|
|
143
|
+
Remote code management client for PyVault.
|
|
144
|
+
|
|
145
|
+
All methods are static — no instantiation required.
|
|
146
|
+
|
|
147
|
+
Typical flow:
|
|
148
|
+
from pyvaultrce import CodeManager
|
|
149
|
+
|
|
150
|
+
sid = CodeManager.enc("script.py")
|
|
151
|
+
CodeManager.run(sid)
|
|
152
|
+
|
|
153
|
+
Environment variables:
|
|
154
|
+
PYVAULT_URL — base URL of the PyVault server (default: http://localhost:5000)
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def enc(
|
|
159
|
+
file_path: str,
|
|
160
|
+
base_url: Optional[str] = None,
|
|
161
|
+
timeout: int = 30,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Read a local .py file and upload it to PyVault.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
file_path: Path to the local Python file.
|
|
168
|
+
base_url: Override the server URL.
|
|
169
|
+
timeout: HTTP timeout in seconds.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The 21-character hex Session ID.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
FileNotFoundError, ValueError, ConnectionError, RuntimeError
|
|
176
|
+
"""
|
|
177
|
+
code = _read_file(file_path)
|
|
178
|
+
if not code.strip():
|
|
179
|
+
raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
|
|
180
|
+
sid = _upload_code(code, base_url, timeout)
|
|
181
|
+
print(f"[PyVault] ✓ Uploaded — Session ID : {sid}", flush=True)
|
|
182
|
+
print(f"[PyVault] Source : {os.path.abspath(file_path)}", flush=True)
|
|
183
|
+
print(f"[PyVault] Size : {len(code):,} chars", flush=True)
|
|
184
|
+
return sid
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def enc_url(
|
|
188
|
+
paste_url: str,
|
|
189
|
+
base_url: Optional[str] = None,
|
|
190
|
+
timeout: int = 30,
|
|
191
|
+
) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Download Python code from a paste service URL and upload to PyVault.
|
|
194
|
+
|
|
195
|
+
Automatically rewrites paste service links to their raw endpoints
|
|
196
|
+
(pastebin.com, hastebin.com, dpaste.com, paste.ofcode.org, GitHub raw).
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
paste_url: URL pointing to Python source code.
|
|
200
|
+
base_url: Override the server URL.
|
|
201
|
+
timeout: HTTP timeout in seconds.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The 21-character hex Session ID.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ValueError, ConnectionError, RuntimeError
|
|
208
|
+
"""
|
|
209
|
+
raw_url = _resolve_paste_url(paste_url.strip())
|
|
210
|
+
try:
|
|
211
|
+
resp = requests.get(
|
|
212
|
+
raw_url,
|
|
213
|
+
timeout=timeout,
|
|
214
|
+
headers={"User-Agent": "PyVaultRCE/2.0"},
|
|
215
|
+
)
|
|
216
|
+
resp.raise_for_status()
|
|
217
|
+
except requests.exceptions.ConnectionError:
|
|
218
|
+
raise ConnectionError(f"Could not fetch code from '{paste_url}'.")
|
|
219
|
+
except requests.exceptions.HTTPError as exc:
|
|
220
|
+
raise RuntimeError(
|
|
221
|
+
f"Failed to download from '{paste_url}': HTTP {exc.response.status_code}"
|
|
222
|
+
)
|
|
223
|
+
except requests.exceptions.Timeout:
|
|
224
|
+
raise TimeoutError(f"Timed out fetching '{paste_url}'.")
|
|
225
|
+
except requests.exceptions.RequestException as exc:
|
|
226
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
227
|
+
|
|
228
|
+
code = resp.text
|
|
229
|
+
if not code.strip():
|
|
230
|
+
raise ValueError("Downloaded content is empty.")
|
|
231
|
+
|
|
232
|
+
sid = _upload_code(code, base_url, timeout)
|
|
233
|
+
print(f"[PyVault] ✓ Uploaded from URL — Session ID : {sid}", flush=True)
|
|
234
|
+
print(f"[PyVault] Source : {paste_url}", flush=True)
|
|
235
|
+
print(f"[PyVault] Size : {len(code):,} chars", flush=True)
|
|
236
|
+
return sid
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def run(
|
|
240
|
+
session_id: str,
|
|
241
|
+
base_url: Optional[str] = None,
|
|
242
|
+
timeout: int = 30,
|
|
243
|
+
_ns: Optional[dict] = None,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Fetch the code for a Session ID from PyVault and execute it locally.
|
|
247
|
+
|
|
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.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
session_id: The 21-character hex Session ID.
|
|
253
|
+
base_url: Override the server URL.
|
|
254
|
+
timeout: HTTP timeout in seconds.
|
|
255
|
+
_ns: Optional namespace dict passed to exec(). Leave as None
|
|
256
|
+
unless you intentionally want to share a namespace.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValueError, ConnectionError, RuntimeError, plus any exception
|
|
260
|
+
raised by the remote code itself.
|
|
261
|
+
"""
|
|
262
|
+
_check_sid(session_id)
|
|
263
|
+
|
|
264
|
+
url = _base(base_url) + f"/pyv/get/{session_id}"
|
|
265
|
+
resp = _http_get(url, timeout)
|
|
266
|
+
|
|
267
|
+
if resp.status_code == 404:
|
|
268
|
+
try:
|
|
269
|
+
err = resp.json().get("error", "Session not found.")
|
|
270
|
+
except Exception:
|
|
271
|
+
err = "Session not found."
|
|
272
|
+
raise RuntimeError(
|
|
273
|
+
f"Session '{session_id}' not found on the server. {err}"
|
|
274
|
+
)
|
|
275
|
+
if resp.status_code != 200:
|
|
276
|
+
try:
|
|
277
|
+
err = resp.json().get("error", resp.text)
|
|
278
|
+
except Exception:
|
|
279
|
+
err = resp.text
|
|
280
|
+
raise RuntimeError(f"Server error (HTTP {resp.status_code}): {err}")
|
|
281
|
+
|
|
282
|
+
data = resp.json()
|
|
283
|
+
code = data.get("code", "")
|
|
284
|
+
run_count = data.get("execution_count", "?")
|
|
285
|
+
|
|
286
|
+
if not code:
|
|
287
|
+
raise RuntimeError(f"Server returned empty code for session '{session_id}'.")
|
|
288
|
+
|
|
289
|
+
print(f"[PyVault] ▶ Running session '{session_id}' (execution #{run_count})…", flush=True)
|
|
290
|
+
|
|
291
|
+
namespace = _ns if _ns is not None else {
|
|
292
|
+
"__name__": "__pyvault__",
|
|
293
|
+
"__builtins__": __builtins__,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
exec(compile(code, f"<vault:{session_id[:8]}…>", "exec"), namespace)
|
|
298
|
+
except SyntaxError as exc:
|
|
299
|
+
print(f"\n[PyVault] ✕ Syntax error:", file=sys.stderr, flush=True)
|
|
300
|
+
print(f" Line {exc.lineno}: {exc.msg}", file=sys.stderr)
|
|
301
|
+
if exc.text:
|
|
302
|
+
print(f" >>> {exc.text.strip()}", file=sys.stderr)
|
|
303
|
+
raise
|
|
304
|
+
except Exception:
|
|
305
|
+
print(f"\n[PyVault] ✕ Runtime error:", file=sys.stderr, flush=True)
|
|
306
|
+
for line in traceback.format_exc().splitlines():
|
|
307
|
+
print(f" {line}", file=sys.stderr)
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
print(f"[PyVault] ✓ Execution complete.", flush=True)
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def info(
|
|
314
|
+
session_id: str,
|
|
315
|
+
base_url: Optional[str] = None,
|
|
316
|
+
timeout: int = 30,
|
|
317
|
+
) -> dict:
|
|
318
|
+
"""
|
|
319
|
+
Fetch metadata for a Session ID without executing the code and without
|
|
320
|
+
incrementing the execution counter.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
dict with keys: session_id, execution_count, created_at, code_size
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
ValueError, ConnectionError, RuntimeError
|
|
327
|
+
"""
|
|
328
|
+
_check_sid(session_id)
|
|
329
|
+
url = _base(base_url) + f"/pyv/info/{session_id}"
|
|
330
|
+
resp = _http_get(url, timeout)
|
|
331
|
+
if resp.status_code == 404:
|
|
332
|
+
raise RuntimeError(f"Session '{session_id}' not found.")
|
|
333
|
+
if resp.status_code != 200:
|
|
334
|
+
try:
|
|
335
|
+
err = resp.json().get("error", resp.text)
|
|
336
|
+
except Exception:
|
|
337
|
+
err = resp.text
|
|
338
|
+
raise RuntimeError(f"Server error (HTTP {resp.status_code}): {err}")
|
|
339
|
+
data = resp.json()
|
|
340
|
+
print(
|
|
341
|
+
f"[PyVault] ℹ Session : {data['session_id']}\n"
|
|
342
|
+
f"[PyVault] Executions: {data['execution_count']}\n"
|
|
343
|
+
f"[PyVault] Created : {data['created_at']}\n"
|
|
344
|
+
f"[PyVault] Code size : {data['code_size']:,} chars",
|
|
345
|
+
flush=True,
|
|
346
|
+
)
|
|
347
|
+
return data
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def ping(
|
|
351
|
+
base_url: Optional[str] = None,
|
|
352
|
+
timeout: int = 10,
|
|
353
|
+
) -> bool:
|
|
354
|
+
"""
|
|
355
|
+
Check whether the PyVault server is reachable.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
True if server responded with valid stats, False otherwise.
|
|
359
|
+
"""
|
|
360
|
+
url = _base(base_url) + "/pyv/stats"
|
|
361
|
+
try:
|
|
362
|
+
resp = requests.get(url, timeout=timeout)
|
|
363
|
+
if resp.status_code == 200:
|
|
364
|
+
d = resp.json()
|
|
365
|
+
print(
|
|
366
|
+
f"[PyVault] ✓ Server online — "
|
|
367
|
+
f"{d.get('total_sessions', '?')} sessions, "
|
|
368
|
+
f"{d.get('total_executions', '?')} total executions.",
|
|
369
|
+
flush=True,
|
|
370
|
+
)
|
|
371
|
+
return True
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
print(f"[PyVault] ✕ Server unreachable at '{_base(base_url)}'.", flush=True)
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def edit(
|
|
379
|
+
session_id: str,
|
|
380
|
+
file_path: str,
|
|
381
|
+
admin_token: str,
|
|
382
|
+
base_url: Optional[str] = None,
|
|
383
|
+
timeout: int = 30,
|
|
384
|
+
) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Replace the code stored for a Session ID (admin-only operation).
|
|
387
|
+
|
|
388
|
+
Requires the admin access token from the PyVault server.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
session_id: The 21-character hex Session ID to update.
|
|
392
|
+
file_path: Path to the .py file with the new code.
|
|
393
|
+
admin_token: Admin access token (from server console on first start).
|
|
394
|
+
base_url: Override the server URL.
|
|
395
|
+
timeout: HTTP timeout in seconds.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
ValueError, FileNotFoundError, ConnectionError, RuntimeError
|
|
399
|
+
"""
|
|
400
|
+
_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.")
|
|
403
|
+
|
|
404
|
+
code = _read_file(file_path)
|
|
405
|
+
if not code.strip():
|
|
406
|
+
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.")
|
|
409
|
+
|
|
410
|
+
url = _base(base_url) + f"/pyv/edit/{session_id}"
|
|
411
|
+
try:
|
|
412
|
+
resp = requests.put(
|
|
413
|
+
url,
|
|
414
|
+
json={"code": code},
|
|
415
|
+
headers={"X-Admin-Token": admin_token},
|
|
416
|
+
timeout=timeout,
|
|
417
|
+
)
|
|
418
|
+
except requests.exceptions.ConnectionError:
|
|
419
|
+
raise ConnectionError(f"Unable to connect to PyVault at '{url}'.")
|
|
420
|
+
except requests.exceptions.Timeout:
|
|
421
|
+
raise TimeoutError(f"Request timed out after {timeout}s.")
|
|
422
|
+
except requests.exceptions.RequestException as exc:
|
|
423
|
+
raise ConnectionError(f"HTTP request failed: {exc}") from exc
|
|
424
|
+
|
|
425
|
+
if resp.status_code == 403:
|
|
426
|
+
raise RuntimeError("Admin token rejected. Check your token and try again.")
|
|
427
|
+
if resp.status_code == 404:
|
|
428
|
+
raise RuntimeError(f"Session '{session_id}' not found.")
|
|
429
|
+
if resp.status_code != 200:
|
|
430
|
+
try:
|
|
431
|
+
err = resp.json().get("error", resp.text)
|
|
432
|
+
except Exception:
|
|
433
|
+
err = resp.text
|
|
434
|
+
raise RuntimeError(f"Server rejected edit (HTTP {resp.status_code}): {err}")
|
|
435
|
+
|
|
436
|
+
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)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="PyVaultRCE",
|
|
8
|
+
version="2.0.0",
|
|
9
|
+
author="PyVault",
|
|
10
|
+
description="Remote Code Execution & Hosting client for PyVault — source never exposed",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
packages=find_packages(),
|
|
14
|
+
python_requires=">=3.8",
|
|
15
|
+
install_requires=[
|
|
16
|
+
"requests>=2.28.0",
|
|
17
|
+
],
|
|
18
|
+
classifiers=[
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
],
|
|
26
|
+
keywords="remote execution code hosting vault rce session",
|
|
27
|
+
entry_points={},
|
|
28
|
+
)
|