PyVaultRCE 2.3.0__tar.gz → 2.5.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.3.0 → pyvaultrce-2.5.0}/PKG-INFO +36 -5
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/PKG-INFO +36 -5
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/README.md +35 -4
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/codemanager/__init__.py +1 -1
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/codemanager/manager.py +123 -27
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/pyvaultrce/__init__.py +1 -1
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/pyvaultrce/manager.py +203 -43
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/setup.py +1 -1
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/SOURCES.txt +0 -0
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/dependency_links.txt +0 -0
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/requires.txt +0 -0
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/top_level.txt +0 -0
- {pyvaultrce-2.3.0 → pyvaultrce-2.5.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.5.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
|
|
@@ -25,7 +25,7 @@ Dynamic: summary
|
|
|
25
25
|
|
|
26
26
|
# PyVaultRCE
|
|
27
27
|
|
|
28
|
-
Remote Code
|
|
28
|
+
Remote Code Hosting client for **PyVault**.
|
|
29
29
|
|
|
30
30
|
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.
|
|
31
31
|
|
|
@@ -47,9 +47,22 @@ sid = CodeManager.enc("my_script.py")
|
|
|
47
47
|
# Upload from a paste service URL
|
|
48
48
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
49
49
|
|
|
50
|
-
# Execute
|
|
50
|
+
# Execute in a short-lived isolated subprocess by default
|
|
51
51
|
CodeManager.run(sid)
|
|
52
52
|
|
|
53
|
+
# Upload with a short share link and library metadata
|
|
54
|
+
sid = CodeManager.enc(
|
|
55
|
+
"my_script.py",
|
|
56
|
+
label="analytics demo",
|
|
57
|
+
libraries=["requests", "pandas"],
|
|
58
|
+
username="your_name",
|
|
59
|
+
description="A public demo post",
|
|
60
|
+
tags=["python", "demo"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Hide PyVault status messages while running
|
|
64
|
+
CodeManager.run(sid, show_terminal=False)
|
|
65
|
+
|
|
53
66
|
# Check metadata without incrementing execution counter
|
|
54
67
|
CodeManager.info(sid)
|
|
55
68
|
|
|
@@ -62,16 +75,29 @@ CodeManager.ping()
|
|
|
62
75
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
63
76
|
|
|
64
77
|
```bash
|
|
65
|
-
export PYVAULT_URL=https://
|
|
78
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
66
79
|
```
|
|
67
80
|
|
|
81
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
82
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
83
|
+
You can also control status output globally:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export PYVAULT_TERMINAL=off
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Library names are metadata only. PyVault does not auto-install packages or
|
|
90
|
+
execute dependency installers; install the libraries in the Python environment
|
|
91
|
+
where `CodeManager.run()` executes.
|
|
92
|
+
|
|
68
93
|
## API Reference
|
|
69
94
|
|
|
70
95
|
| Method | Description |
|
|
71
96
|
|--------|-------------|
|
|
72
97
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
98
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
73
99
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
74
|
-
| `CodeManager.run(session_id)` | Fetch and execute code
|
|
100
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
75
101
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
76
102
|
| `CodeManager.ping()` | Check server connectivity |
|
|
77
103
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -102,6 +128,11 @@ export PYVAULT_URL=https://your-server.replit.app
|
|
|
102
128
|
- Session IDs are 21-character cryptographically random hex strings
|
|
103
129
|
- Source code is never written to disk on the client
|
|
104
130
|
- Code is encrypted at rest with Fernet on the server
|
|
131
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
132
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
133
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
134
|
+
it is not recommended for untrusted sessions
|
|
135
|
+
- Share links expose only session metadata and declared library names, never source
|
|
105
136
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
106
137
|
- Sessions never expire unless an expiry is explicitly selected
|
|
107
138
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVaultRCE
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -25,7 +25,7 @@ Dynamic: summary
|
|
|
25
25
|
|
|
26
26
|
# PyVaultRCE
|
|
27
27
|
|
|
28
|
-
Remote Code
|
|
28
|
+
Remote Code Hosting client for **PyVault**.
|
|
29
29
|
|
|
30
30
|
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.
|
|
31
31
|
|
|
@@ -47,9 +47,22 @@ sid = CodeManager.enc("my_script.py")
|
|
|
47
47
|
# Upload from a paste service URL
|
|
48
48
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
49
49
|
|
|
50
|
-
# Execute
|
|
50
|
+
# Execute in a short-lived isolated subprocess by default
|
|
51
51
|
CodeManager.run(sid)
|
|
52
52
|
|
|
53
|
+
# Upload with a short share link and library metadata
|
|
54
|
+
sid = CodeManager.enc(
|
|
55
|
+
"my_script.py",
|
|
56
|
+
label="analytics demo",
|
|
57
|
+
libraries=["requests", "pandas"],
|
|
58
|
+
username="your_name",
|
|
59
|
+
description="A public demo post",
|
|
60
|
+
tags=["python", "demo"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Hide PyVault status messages while running
|
|
64
|
+
CodeManager.run(sid, show_terminal=False)
|
|
65
|
+
|
|
53
66
|
# Check metadata without incrementing execution counter
|
|
54
67
|
CodeManager.info(sid)
|
|
55
68
|
|
|
@@ -62,16 +75,29 @@ CodeManager.ping()
|
|
|
62
75
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
63
76
|
|
|
64
77
|
```bash
|
|
65
|
-
export PYVAULT_URL=https://
|
|
78
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
66
79
|
```
|
|
67
80
|
|
|
81
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
82
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
83
|
+
You can also control status output globally:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export PYVAULT_TERMINAL=off
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Library names are metadata only. PyVault does not auto-install packages or
|
|
90
|
+
execute dependency installers; install the libraries in the Python environment
|
|
91
|
+
where `CodeManager.run()` executes.
|
|
92
|
+
|
|
68
93
|
## API Reference
|
|
69
94
|
|
|
70
95
|
| Method | Description |
|
|
71
96
|
|--------|-------------|
|
|
72
97
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
98
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
73
99
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
74
|
-
| `CodeManager.run(session_id)` | Fetch and execute code
|
|
100
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
75
101
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
76
102
|
| `CodeManager.ping()` | Check server connectivity |
|
|
77
103
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -102,6 +128,11 @@ export PYVAULT_URL=https://your-server.replit.app
|
|
|
102
128
|
- Session IDs are 21-character cryptographically random hex strings
|
|
103
129
|
- Source code is never written to disk on the client
|
|
104
130
|
- Code is encrypted at rest with Fernet on the server
|
|
131
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
132
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
133
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
134
|
+
it is not recommended for untrusted sessions
|
|
135
|
+
- Share links expose only session metadata and declared library names, never source
|
|
105
136
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
106
137
|
- Sessions never expire unless an expiry is explicitly selected
|
|
107
138
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PyVaultRCE
|
|
2
2
|
|
|
3
|
-
Remote Code
|
|
3
|
+
Remote Code Hosting client for **PyVault**.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -22,9 +22,22 @@ sid = CodeManager.enc("my_script.py")
|
|
|
22
22
|
# Upload from a paste service URL
|
|
23
23
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
24
24
|
|
|
25
|
-
# Execute
|
|
25
|
+
# Execute in a short-lived isolated subprocess by default
|
|
26
26
|
CodeManager.run(sid)
|
|
27
27
|
|
|
28
|
+
# Upload with a short share link and library metadata
|
|
29
|
+
sid = CodeManager.enc(
|
|
30
|
+
"my_script.py",
|
|
31
|
+
label="analytics demo",
|
|
32
|
+
libraries=["requests", "pandas"],
|
|
33
|
+
username="your_name",
|
|
34
|
+
description="A public demo post",
|
|
35
|
+
tags=["python", "demo"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Hide PyVault status messages while running
|
|
39
|
+
CodeManager.run(sid, show_terminal=False)
|
|
40
|
+
|
|
28
41
|
# Check metadata without incrementing execution counter
|
|
29
42
|
CodeManager.info(sid)
|
|
30
43
|
|
|
@@ -37,16 +50,29 @@ CodeManager.ping()
|
|
|
37
50
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
38
51
|
|
|
39
52
|
```bash
|
|
40
|
-
export PYVAULT_URL=https://
|
|
53
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
41
54
|
```
|
|
42
55
|
|
|
56
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
57
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
58
|
+
You can also control status output globally:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export PYVAULT_TERMINAL=off
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Library names are metadata only. PyVault does not auto-install packages or
|
|
65
|
+
execute dependency installers; install the libraries in the Python environment
|
|
66
|
+
where `CodeManager.run()` executes.
|
|
67
|
+
|
|
43
68
|
## API Reference
|
|
44
69
|
|
|
45
70
|
| Method | Description |
|
|
46
71
|
|--------|-------------|
|
|
47
72
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
73
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
48
74
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
49
|
-
| `CodeManager.run(session_id)` | Fetch and execute code
|
|
75
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
50
76
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
51
77
|
| `CodeManager.ping()` | Check server connectivity |
|
|
52
78
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -77,6 +103,11 @@ export PYVAULT_URL=https://your-server.replit.app
|
|
|
77
103
|
- Session IDs are 21-character cryptographically random hex strings
|
|
78
104
|
- Source code is never written to disk on the client
|
|
79
105
|
- Code is encrypted at rest with Fernet on the server
|
|
106
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
107
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
108
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
109
|
+
it is not recommended for untrusted sessions
|
|
110
|
+
- Share links expose only session metadata and declared library names, never source
|
|
80
111
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
81
112
|
- Sessions never expire unless an expiry is explicitly selected
|
|
82
113
|
|
|
@@ -3,7 +3,9 @@ manager.py — CodeManager implementation
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
6
7
|
import sys
|
|
8
|
+
import tempfile
|
|
7
9
|
import traceback
|
|
8
10
|
import textwrap
|
|
9
11
|
from typing import Optional
|
|
@@ -15,7 +17,7 @@ except ImportError:
|
|
|
15
17
|
"The 'requests' library is required. Install it with: pip install requests"
|
|
16
18
|
)
|
|
17
19
|
|
|
18
|
-
_DEFAULT_BASE_URL = "https://secure-code-runner--
|
|
20
|
+
_DEFAULT_BASE_URL = "https://secure-code-runner--diwasrepl.replit.app"
|
|
19
21
|
|
|
20
22
|
# ── Validation helpers ───────────────────────────────────────────────────────
|
|
21
23
|
|
|
@@ -40,6 +42,70 @@ def _get_base_url(base_url: Optional[str]) -> str:
|
|
|
40
42
|
return url
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
def _terminal_enabled(show_terminal=None) -> bool:
|
|
46
|
+
if show_terminal is not None:
|
|
47
|
+
return bool(show_terminal)
|
|
48
|
+
return os.environ.get("PYVAULT_TERMINAL", "on").strip().lower() not in {
|
|
49
|
+
"0", "false", "off", "no", "quiet"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _emit(message: str, show_terminal=None, *, error=False) -> None:
|
|
54
|
+
if _terminal_enabled(show_terminal):
|
|
55
|
+
print(message, file=sys.stderr if error else sys.stdout, flush=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
|
|
59
|
+
"""Execute in a short-lived process with clean environment and limits."""
|
|
60
|
+
def limit_resources():
|
|
61
|
+
if os.name != "posix":
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
import resource
|
|
65
|
+
cpu = max(1, min(int(timeout), 30))
|
|
66
|
+
# Python's runtime reserves virtual address space during startup;
|
|
67
|
+
# 512 MiB breaks otherwise tiny scripts on some Linux builds.
|
|
68
|
+
memory = 2 * 1024 * 1024 * 1024
|
|
69
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
|
|
70
|
+
resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
|
|
71
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
|
|
72
|
+
except (ImportError, OSError, ValueError):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
clean_env = {
|
|
76
|
+
key: os.environ[key]
|
|
77
|
+
for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL")
|
|
78
|
+
if key in os.environ
|
|
79
|
+
}
|
|
80
|
+
clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
|
|
81
|
+
with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
|
|
82
|
+
script_path = os.path.join(workdir, "session.py")
|
|
83
|
+
with open(script_path, "w", encoding="utf-8") as handle:
|
|
84
|
+
handle.write(code)
|
|
85
|
+
try:
|
|
86
|
+
return subprocess.run(
|
|
87
|
+
[sys.executable, "-I", "-u", script_path],
|
|
88
|
+
cwd=workdir,
|
|
89
|
+
env=clean_env,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=max(1, int(timeout)),
|
|
93
|
+
check=False,
|
|
94
|
+
preexec_fn=limit_resources if os.name == "posix" else None,
|
|
95
|
+
)
|
|
96
|
+
except subprocess.TimeoutExpired as exc:
|
|
97
|
+
raise TimeoutError(
|
|
98
|
+
f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _emit_child_output(result: subprocess.CompletedProcess) -> None:
|
|
103
|
+
if result.stdout:
|
|
104
|
+
print(result.stdout, end="", flush=True)
|
|
105
|
+
if result.stderr:
|
|
106
|
+
print(result.stderr, end="", file=sys.stderr, flush=True)
|
|
107
|
+
|
|
108
|
+
|
|
43
109
|
# ── CodeManager ─────────────────────────────────────────────────────────────
|
|
44
110
|
|
|
45
111
|
class CodeManager:
|
|
@@ -59,6 +125,11 @@ class CodeManager:
|
|
|
59
125
|
file_path: str,
|
|
60
126
|
base_url: Optional[str] = None,
|
|
61
127
|
timeout: int = 30,
|
|
128
|
+
label: str = "",
|
|
129
|
+
libraries = "",
|
|
130
|
+
username: str = "",
|
|
131
|
+
description: str = "",
|
|
132
|
+
tags = "",
|
|
62
133
|
) -> str:
|
|
63
134
|
"""
|
|
64
135
|
Read a local Python file and upload it to the PyVault backend.
|
|
@@ -98,7 +169,14 @@ class CodeManager:
|
|
|
98
169
|
try:
|
|
99
170
|
response = requests.post(
|
|
100
171
|
url,
|
|
101
|
-
json={
|
|
172
|
+
json={
|
|
173
|
+
"code": code,
|
|
174
|
+
"label": label,
|
|
175
|
+
"libraries": libraries,
|
|
176
|
+
"username": username,
|
|
177
|
+
"description": description,
|
|
178
|
+
"tags": tags,
|
|
179
|
+
},
|
|
102
180
|
timeout=timeout,
|
|
103
181
|
)
|
|
104
182
|
except requests.exceptions.ConnectionError:
|
|
@@ -142,12 +220,17 @@ class CodeManager:
|
|
|
142
220
|
base_url: Optional[str] = None,
|
|
143
221
|
timeout: int = 30,
|
|
144
222
|
globals_dict: Optional[dict] = None,
|
|
223
|
+
show_terminal=None,
|
|
224
|
+
isolated: bool = True,
|
|
145
225
|
) -> None:
|
|
146
226
|
"""
|
|
147
|
-
Fetch the code for a Session ID from PyVault and execute it
|
|
227
|
+
Fetch the code for a Session ID from PyVault and execute it in a
|
|
228
|
+
short-lived isolated subprocess by default.
|
|
148
229
|
|
|
149
|
-
The source code is fetched over the network
|
|
150
|
-
|
|
230
|
+
The source code is fetched over the network. The default subprocess
|
|
231
|
+
uses a clean environment, a temporary working directory, and resource
|
|
232
|
+
limits. Use isolated=False only for trusted code that needs a shared
|
|
233
|
+
in-process namespace.
|
|
151
234
|
|
|
152
235
|
Args:
|
|
153
236
|
session_id: The 21-character hex Session ID.
|
|
@@ -208,30 +291,43 @@ class CodeManager:
|
|
|
208
291
|
f"Server returned empty code for session '{session_id}'."
|
|
209
292
|
)
|
|
210
293
|
|
|
211
|
-
|
|
294
|
+
_emit(
|
|
295
|
+
f"[PyVault] Executing session '{session_id}' "
|
|
296
|
+
f"(run #{exec_count}, isolated={isolated})…",
|
|
297
|
+
show_terminal,
|
|
298
|
+
)
|
|
212
299
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
300
|
+
if isolated:
|
|
301
|
+
if globals_dict is not None:
|
|
302
|
+
raise ValueError("isolated=True cannot use globals_dict; pass isolated=False for a shared namespace.")
|
|
303
|
+
result = _isolated_run(code, session_id, timeout)
|
|
304
|
+
_emit_child_output(result)
|
|
305
|
+
if result.returncode != 0:
|
|
306
|
+
raise RuntimeError(
|
|
307
|
+
f"Isolated session '{session_id}' exited with code {result.returncode}."
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
namespace = globals_dict if globals_dict is not None else {
|
|
311
|
+
"__name__": "__pyvault__",
|
|
312
|
+
"__builtins__": __builtins__,
|
|
313
|
+
}
|
|
314
|
+
try:
|
|
315
|
+
compiled = compile(code, f"<pyvault:{session_id}>", "exec")
|
|
316
|
+
exec(compiled, namespace)
|
|
317
|
+
except SyntaxError as exc:
|
|
318
|
+
_emit("\n[PyVault] ✕ Syntax error in remote code:", show_terminal, error=True)
|
|
319
|
+
_emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
|
|
320
|
+
if exc.text:
|
|
321
|
+
_emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
|
|
322
|
+
raise
|
|
323
|
+
except Exception:
|
|
324
|
+
_emit("\n[PyVault] ✕ Runtime error during execution:", show_terminal, error=True)
|
|
325
|
+
tb_lines = traceback.format_exc().splitlines()
|
|
326
|
+
for line in tb_lines:
|
|
327
|
+
_emit(f" {line}", show_terminal, error=True)
|
|
328
|
+
raise
|
|
217
329
|
|
|
218
|
-
|
|
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.")
|
|
330
|
+
_emit("[PyVault] ✓ Execution complete.", show_terminal)
|
|
235
331
|
|
|
236
332
|
@staticmethod
|
|
237
333
|
def edit(
|
|
@@ -4,7 +4,9 @@ manager.py — CodeManager implementation for PyVaultRCE
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import subprocess
|
|
7
8
|
import sys
|
|
9
|
+
import tempfile
|
|
8
10
|
import traceback
|
|
9
11
|
from typing import Optional
|
|
10
12
|
|
|
@@ -23,7 +25,7 @@ try:
|
|
|
23
25
|
except ImportError:
|
|
24
26
|
_HAS_CRYPTO = False
|
|
25
27
|
|
|
26
|
-
_DEFAULT_BASE = "https://secure-code-runner--
|
|
28
|
+
_DEFAULT_BASE = "https://secure-code-runner--diwasrepl.replit.app"
|
|
27
29
|
_MAX_LINES = 10_000
|
|
28
30
|
_SID_LEN = 21
|
|
29
31
|
_HEX_SET = frozenset("0123456789abcdef")
|
|
@@ -47,6 +49,33 @@ def _base(url: Optional[str]) -> str:
|
|
|
47
49
|
return (url or os.environ.get("PYVAULT_URL") or _DEFAULT_BASE).rstrip("/")
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
def _terminal_enabled(show_terminal: Optional[bool]) -> bool:
|
|
53
|
+
if show_terminal is not None:
|
|
54
|
+
return bool(show_terminal)
|
|
55
|
+
value = os.environ.get("PYVAULT_TERMINAL", "on").strip().lower()
|
|
56
|
+
return value not in {"0", "false", "off", "no", "quiet"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _emit(message: str, show_terminal: Optional[bool] = None, *, error: bool = False) -> None:
|
|
60
|
+
if _terminal_enabled(show_terminal):
|
|
61
|
+
print(message, file=sys.stderr if error else sys.stdout, flush=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalise_libraries(value) -> str:
|
|
65
|
+
if isinstance(value, (list, tuple)):
|
|
66
|
+
values = value
|
|
67
|
+
elif isinstance(value, str):
|
|
68
|
+
values = re.split(r"[,\n]", value)
|
|
69
|
+
else:
|
|
70
|
+
values = []
|
|
71
|
+
cleaned = []
|
|
72
|
+
for item in values:
|
|
73
|
+
name = str(item).strip()
|
|
74
|
+
if name and re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", name) and name not in cleaned:
|
|
75
|
+
cleaned.append(name)
|
|
76
|
+
return ", ".join(cleaned)[:500]
|
|
77
|
+
|
|
78
|
+
|
|
50
79
|
def _valid_sid(sid: str) -> bool:
|
|
51
80
|
return isinstance(sid, str) and len(sid) == _SID_LEN and all(c in _HEX_SET for c in sid)
|
|
52
81
|
|
|
@@ -144,14 +173,75 @@ def _localhost_hint(url: str) -> str:
|
|
|
144
173
|
return ""
|
|
145
174
|
|
|
146
175
|
|
|
176
|
+
def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
|
|
177
|
+
"""Run fetched code in a short-lived process with a clean environment.
|
|
178
|
+
|
|
179
|
+
This is a defense-in-depth boundary for the client. It is intentionally
|
|
180
|
+
not described as a container or a complete OS sandbox: callers who need
|
|
181
|
+
hostile-code isolation should use a dedicated VM/container policy.
|
|
182
|
+
"""
|
|
183
|
+
def limit_resources():
|
|
184
|
+
if os.name != "posix":
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
import resource
|
|
188
|
+
cpu = max(1, min(int(timeout), 30))
|
|
189
|
+
# Python's runtime reserves virtual address space during startup;
|
|
190
|
+
# 512 MiB breaks otherwise tiny scripts on some Linux builds.
|
|
191
|
+
memory = 2 * 1024 * 1024 * 1024
|
|
192
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
|
|
193
|
+
resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
|
|
194
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
|
|
195
|
+
except (ImportError, OSError, ValueError):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
clean_env = {}
|
|
199
|
+
for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL"):
|
|
200
|
+
if key in os.environ:
|
|
201
|
+
clean_env[key] = os.environ[key]
|
|
202
|
+
clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
|
|
203
|
+
|
|
204
|
+
with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
|
|
205
|
+
script_path = os.path.join(workdir, "session.py")
|
|
206
|
+
with open(script_path, "w", encoding="utf-8") as handle:
|
|
207
|
+
handle.write(code)
|
|
208
|
+
try:
|
|
209
|
+
return subprocess.run(
|
|
210
|
+
[sys.executable, "-I", "-u", script_path],
|
|
211
|
+
cwd=workdir,
|
|
212
|
+
env=clean_env,
|
|
213
|
+
capture_output=True,
|
|
214
|
+
text=True,
|
|
215
|
+
timeout=max(1, int(timeout)),
|
|
216
|
+
check=False,
|
|
217
|
+
preexec_fn=limit_resources if os.name == "posix" else None,
|
|
218
|
+
)
|
|
219
|
+
except subprocess.TimeoutExpired as exc:
|
|
220
|
+
raise TimeoutError(
|
|
221
|
+
f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
|
|
222
|
+
) from exc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _emit_child_output(result: subprocess.CompletedProcess) -> None:
|
|
226
|
+
if result.stdout:
|
|
227
|
+
print(result.stdout, end="", flush=True)
|
|
228
|
+
if result.stderr:
|
|
229
|
+
print(result.stderr, end="", file=sys.stderr, flush=True)
|
|
230
|
+
|
|
231
|
+
|
|
147
232
|
def _upload_code(
|
|
148
233
|
code: str,
|
|
149
234
|
base_url: Optional[str],
|
|
150
235
|
timeout: int,
|
|
151
236
|
expires_in_hours: Optional[float] = None,
|
|
152
237
|
max_executions: int = 0,
|
|
238
|
+
label: str = "",
|
|
239
|
+
libraries = "",
|
|
240
|
+
username: str = "",
|
|
241
|
+
description: str = "",
|
|
242
|
+
tags = "",
|
|
153
243
|
) -> tuple:
|
|
154
|
-
"""Internal: push code string to /pyv/save. Returns
|
|
244
|
+
"""Internal: push code string to /pyv/save. Returns session metadata."""
|
|
155
245
|
if not code.strip():
|
|
156
246
|
raise ValueError("Code is empty — nothing to upload.")
|
|
157
247
|
line_count = len(code.splitlines())
|
|
@@ -164,6 +254,16 @@ def _upload_code(
|
|
|
164
254
|
payload["expires_in_hours"] = expires_in_hours
|
|
165
255
|
if max_executions and max_executions > 0:
|
|
166
256
|
payload["max_executions"] = max_executions
|
|
257
|
+
if label:
|
|
258
|
+
payload["label"] = str(label)[:200]
|
|
259
|
+
if libraries:
|
|
260
|
+
payload["libraries"] = _normalise_libraries(libraries)
|
|
261
|
+
if username:
|
|
262
|
+
payload["username"] = str(username)[:24]
|
|
263
|
+
if description:
|
|
264
|
+
payload["description"] = str(description)[:1000]
|
|
265
|
+
if tags:
|
|
266
|
+
payload["tags"] = tags
|
|
167
267
|
|
|
168
268
|
url = _base(base_url) + "/pyv/save"
|
|
169
269
|
resp = _http_post(url, payload, timeout)
|
|
@@ -178,7 +278,7 @@ def _upload_code(
|
|
|
178
278
|
owner_token = data.get("owner_token", "")
|
|
179
279
|
if not _valid_sid(sid):
|
|
180
280
|
raise RuntimeError(f"Server returned unexpected Session ID: '{sid}'")
|
|
181
|
-
return sid, owner_token
|
|
281
|
+
return sid, owner_token, data.get("share_url", ""), data.get("libraries", "")
|
|
182
282
|
|
|
183
283
|
|
|
184
284
|
# ── CodeManager ───────────────────────────────────────────────────────────────
|
|
@@ -197,7 +297,8 @@ class CodeManager:
|
|
|
197
297
|
|
|
198
298
|
Environment variables:
|
|
199
299
|
PYVAULT_URL — base URL of the PyVault server
|
|
200
|
-
|
|
300
|
+
(default: the PyVault public deployment; set PYVAULT_URL to override)
|
|
301
|
+
PYVAULT_TERMINAL — on/off terminal status messages (default: on)
|
|
201
302
|
"""
|
|
202
303
|
|
|
203
304
|
@staticmethod
|
|
@@ -207,6 +308,12 @@ class CodeManager:
|
|
|
207
308
|
timeout: int = 30,
|
|
208
309
|
expires_in_hours: Optional[float] = None,
|
|
209
310
|
max_executions: int = 0,
|
|
311
|
+
label: str = "",
|
|
312
|
+
libraries = "",
|
|
313
|
+
username: str = "",
|
|
314
|
+
description: str = "",
|
|
315
|
+
tags = "",
|
|
316
|
+
show_terminal: Optional[bool] = None,
|
|
210
317
|
) -> str:
|
|
211
318
|
"""
|
|
212
319
|
Read a local .py file and upload it to PyVault.
|
|
@@ -227,14 +334,21 @@ class CodeManager:
|
|
|
227
334
|
code = _read_file(file_path)
|
|
228
335
|
if not code.strip():
|
|
229
336
|
raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
|
|
230
|
-
sid, owner_token
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
337
|
+
sid, owner_token, share_url, safe_libraries = _upload_code(
|
|
338
|
+
code, base_url, timeout, expires_in_hours, max_executions, label, libraries,
|
|
339
|
+
username, description, tags
|
|
340
|
+
)
|
|
341
|
+
_emit(f"[PyVault] ✓ Uploaded — Session ID : {sid}", show_terminal)
|
|
342
|
+
_emit(f"[PyVault] Source : {os.path.abspath(file_path)}", show_terminal)
|
|
343
|
+
_emit(f"[PyVault] Lines : {len(code.splitlines()):,}", show_terminal)
|
|
344
|
+
if share_url:
|
|
345
|
+
_emit(f"[PyVault] Share link: {share_url}", show_terminal)
|
|
346
|
+
if safe_libraries:
|
|
347
|
+
_emit(f"[PyVault] Libraries: {safe_libraries}", show_terminal)
|
|
234
348
|
if owner_token:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
349
|
+
_emit("\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", show_terminal)
|
|
350
|
+
_emit(f"[PyVault] {owner_token}", show_terminal)
|
|
351
|
+
_emit("[PyVault] Use this to delete or edit your session later.\n", show_terminal)
|
|
238
352
|
return sid
|
|
239
353
|
|
|
240
354
|
@staticmethod
|
|
@@ -242,6 +356,12 @@ class CodeManager:
|
|
|
242
356
|
paste_url: str,
|
|
243
357
|
base_url: Optional[str] = None,
|
|
244
358
|
timeout: int = 30,
|
|
359
|
+
label: str = "",
|
|
360
|
+
libraries = "",
|
|
361
|
+
username: str = "",
|
|
362
|
+
description: str = "",
|
|
363
|
+
tags = "",
|
|
364
|
+
show_terminal: Optional[bool] = None,
|
|
245
365
|
) -> str:
|
|
246
366
|
"""
|
|
247
367
|
Download Python code from a paste service URL and upload to PyVault.
|
|
@@ -265,7 +385,7 @@ class CodeManager:
|
|
|
265
385
|
resp = requests.get(
|
|
266
386
|
raw_url,
|
|
267
387
|
timeout=timeout,
|
|
268
|
-
headers={"User-Agent": "PyVaultRCE/2.
|
|
388
|
+
headers={"User-Agent": "PyVaultRCE/2.5"},
|
|
269
389
|
)
|
|
270
390
|
resp.raise_for_status()
|
|
271
391
|
except requests.exceptions.ConnectionError:
|
|
@@ -283,14 +403,21 @@ class CodeManager:
|
|
|
283
403
|
if not code.strip():
|
|
284
404
|
raise ValueError("Downloaded content is empty.")
|
|
285
405
|
|
|
286
|
-
sid, owner_token = _upload_code(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
406
|
+
sid, owner_token, share_url, safe_libraries = _upload_code(
|
|
407
|
+
code, base_url, timeout, label=label, libraries=libraries,
|
|
408
|
+
username=username, description=description, tags=tags
|
|
409
|
+
)
|
|
410
|
+
_emit(f"[PyVault] ✓ Uploaded from URL — Session ID : {sid}", show_terminal)
|
|
411
|
+
_emit(f"[PyVault] Source : {paste_url}", show_terminal)
|
|
412
|
+
_emit(f"[PyVault] Lines : {len(code.splitlines()):,}", show_terminal)
|
|
413
|
+
if share_url:
|
|
414
|
+
_emit(f"[PyVault] Share link: {share_url}", show_terminal)
|
|
415
|
+
if safe_libraries:
|
|
416
|
+
_emit(f"[PyVault] Libraries: {safe_libraries}", show_terminal)
|
|
290
417
|
if owner_token:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
418
|
+
_emit("\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", show_terminal)
|
|
419
|
+
_emit(f"[PyVault] {owner_token}", show_terminal)
|
|
420
|
+
_emit("[PyVault] Use this to delete or edit your session later.\n", show_terminal)
|
|
294
421
|
return sid
|
|
295
422
|
|
|
296
423
|
@staticmethod
|
|
@@ -299,20 +426,27 @@ class CodeManager:
|
|
|
299
426
|
base_url: Optional[str] = None,
|
|
300
427
|
timeout: int = 30,
|
|
301
428
|
_ns: Optional[dict] = None,
|
|
429
|
+
show_terminal: Optional[bool] = None,
|
|
430
|
+
isolated: bool = True,
|
|
302
431
|
) -> None:
|
|
303
432
|
"""
|
|
304
433
|
Fetch the encrypted code for a Session ID from PyVault, decrypt it,
|
|
305
|
-
and execute it
|
|
434
|
+
and execute it in a short-lived isolated subprocess by default.
|
|
306
435
|
|
|
307
|
-
The source code is transmitted encrypted
|
|
308
|
-
|
|
436
|
+
The source code is transmitted encrypted and decrypted in memory. The
|
|
437
|
+
default subprocess writes it only to a temporary file for the child
|
|
438
|
+
process, then removes that directory after execution.
|
|
309
439
|
|
|
310
440
|
Args:
|
|
311
441
|
session_id: The 21-character hex Session ID.
|
|
312
442
|
base_url: Override the server URL.
|
|
313
443
|
timeout: HTTP timeout in seconds.
|
|
314
444
|
_ns: Optional namespace dict passed to exec(). Leave as None
|
|
315
|
-
|
|
445
|
+
unless you intentionally want to share a namespace;
|
|
446
|
+
providing it requires isolated=False.
|
|
447
|
+
isolated: Use the short-lived subprocess boundary (default True).
|
|
448
|
+
Set False only for trusted code that needs an in-process
|
|
449
|
+
namespace.
|
|
316
450
|
|
|
317
451
|
Raises:
|
|
318
452
|
ValueError, ConnectionError, RuntimeError, plus any exception
|
|
@@ -363,28 +497,41 @@ class CodeManager:
|
|
|
363
497
|
if not code.strip():
|
|
364
498
|
raise RuntimeError(f"Decrypted code is empty for session '{session_id}'.")
|
|
365
499
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
500
|
+
_emit(
|
|
501
|
+
f"[PyVault] ▶ Running session '{session_id}' "
|
|
502
|
+
f"(execution #{run_count}, isolated={isolated})…",
|
|
503
|
+
show_terminal,
|
|
504
|
+
)
|
|
372
505
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
506
|
+
if isolated:
|
|
507
|
+
if _ns is not None:
|
|
508
|
+
raise ValueError("isolated=True cannot use _ns; pass isolated=False for a shared namespace.")
|
|
509
|
+
result = _isolated_run(code, session_id, timeout)
|
|
510
|
+
_emit_child_output(result)
|
|
511
|
+
if result.returncode != 0:
|
|
512
|
+
raise RuntimeError(
|
|
513
|
+
f"Isolated session '{session_id}' exited with code {result.returncode}."
|
|
514
|
+
)
|
|
515
|
+
else:
|
|
516
|
+
namespace = _ns if _ns is not None else {
|
|
517
|
+
"__name__": "__pyvault__",
|
|
518
|
+
"__builtins__": __builtins__,
|
|
519
|
+
}
|
|
520
|
+
try:
|
|
521
|
+
exec(compile(code, f"<vault:{session_id[:8]}…>", "exec"), namespace)
|
|
522
|
+
except SyntaxError as exc:
|
|
523
|
+
_emit("\n[PyVault] ✕ Syntax error:", show_terminal, error=True)
|
|
524
|
+
_emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
|
|
525
|
+
if exc.text:
|
|
526
|
+
_emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
|
|
527
|
+
raise
|
|
528
|
+
except Exception:
|
|
529
|
+
_emit("\n[PyVault] ✕ Runtime error:", show_terminal, error=True)
|
|
530
|
+
for line in traceback.format_exc().splitlines():
|
|
531
|
+
_emit(f" {line}", show_terminal, error=True)
|
|
532
|
+
raise
|
|
386
533
|
|
|
387
|
-
|
|
534
|
+
_emit("[PyVault] ✓ Execution complete.", show_terminal)
|
|
388
535
|
|
|
389
536
|
@staticmethod
|
|
390
537
|
def info(
|
|
@@ -426,6 +573,19 @@ class CodeManager:
|
|
|
426
573
|
)
|
|
427
574
|
return data
|
|
428
575
|
|
|
576
|
+
@staticmethod
|
|
577
|
+
def share(
|
|
578
|
+
session_id: str,
|
|
579
|
+
base_url: Optional[str] = None,
|
|
580
|
+
timeout: int = 30,
|
|
581
|
+
) -> str:
|
|
582
|
+
"""Return the short public metadata link for a session."""
|
|
583
|
+
data = CodeManager.info(session_id, base_url=base_url, timeout=timeout)
|
|
584
|
+
share_url = data.get("share_url", "")
|
|
585
|
+
if not share_url:
|
|
586
|
+
raise RuntimeError("This server did not return a share URL.")
|
|
587
|
+
return share_url
|
|
588
|
+
|
|
429
589
|
@staticmethod
|
|
430
590
|
def ping(
|
|
431
591
|
base_url: Optional[str] = None,
|
|
@@ -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.5.0",
|
|
9
9
|
author="PyVault",
|
|
10
10
|
description="Remote Code Execution & Hosting client for PyVault — source never exposed",
|
|
11
11
|
long_description=long_description,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|