pywebexec 1.2.10__py3-none-any.whl → 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywebexec/pywebexec.py +96 -19
- pywebexec/static/js/commands.js +4 -2
- pywebexec/static/js/script.js +16 -9
- pywebexec/templates/index.html +1 -1
- pywebexec/version.py +2 -2
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/METADATA +21 -6
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/RECORD +11 -11
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/LICENSE +0 -0
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/WHEEL +0 -0
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.2.10.dist-info → pywebexec-1.3.1.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -13,9 +13,12 @@ from datetime import datetime, timezone, timedelta
|
|
13
13
|
import shlex
|
14
14
|
from gunicorn.app.base import Application
|
15
15
|
import ipaddress
|
16
|
-
from socket import gethostname, gethostbyname_ex
|
16
|
+
from socket import gethostname, gethostbyname_ex, gethostbyaddr, inet_aton, inet_ntoa
|
17
17
|
import ssl
|
18
18
|
import re
|
19
|
+
import pwd
|
20
|
+
from secrets import token_urlsafe
|
21
|
+
|
19
22
|
if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
20
23
|
from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
|
21
24
|
|
@@ -48,11 +51,38 @@ def generate_random_password(length=12):
|
|
48
51
|
|
49
52
|
|
50
53
|
def resolve_hostname(host):
|
51
|
-
"""try get fqdn from DNS"""
|
54
|
+
"""try get fqdn from DNS/hosts"""
|
55
|
+
try:
|
56
|
+
hostinfo = gethostbyname_ex(host)
|
57
|
+
return (hostinfo[0].rstrip('.'), hostinfo[2][0])
|
58
|
+
except OSError:
|
59
|
+
return (host, host)
|
60
|
+
|
61
|
+
|
62
|
+
def resolve_ip(ip):
|
63
|
+
"""try resolve hostname by reverse dns query on ip addr"""
|
64
|
+
ip = inet_ntoa(inet_aton(ip))
|
52
65
|
try:
|
53
|
-
|
66
|
+
ipinfo = gethostbyaddr(ip)
|
67
|
+
return (ipinfo[0].rstrip('.'), ipinfo[2][0])
|
54
68
|
except OSError:
|
55
|
-
return
|
69
|
+
return (ip, ip)
|
70
|
+
|
71
|
+
|
72
|
+
def is_ip(host):
|
73
|
+
"""determine if host is valid ip"""
|
74
|
+
try:
|
75
|
+
inet_aton(host)
|
76
|
+
return True
|
77
|
+
except OSError:
|
78
|
+
return False
|
79
|
+
|
80
|
+
|
81
|
+
def resolve(host_or_ip):
|
82
|
+
"""resolve hostname from ip / hostname"""
|
83
|
+
if is_ip(host_or_ip):
|
84
|
+
return resolve_ip(host_or_ip)
|
85
|
+
return resolve_hostname(host_or_ip)
|
56
86
|
|
57
87
|
|
58
88
|
def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
@@ -118,7 +148,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
118
148
|
|
119
149
|
|
120
150
|
|
121
|
-
class
|
151
|
+
class PyWebExec(Application):
|
122
152
|
|
123
153
|
def __init__(self, app, options=None):
|
124
154
|
self.options = options or {}
|
@@ -180,11 +210,14 @@ def get_last_non_empty_line_of_file(file_path):
|
|
180
210
|
return last_line(f)
|
181
211
|
|
182
212
|
|
183
|
-
def start_gunicorn(
|
184
|
-
if
|
213
|
+
def start_gunicorn(daemonized=False, baselog=None):
|
214
|
+
if daemonized:
|
185
215
|
errorlog = f"{baselog}.log"
|
186
216
|
accesslog = None # f"{baselog}.access.log"
|
187
217
|
pidfile = f"{baselog}.pid"
|
218
|
+
if daemon_d('status', pidfilepath=baselog, silent=True):
|
219
|
+
print(f"Error: pywebexec already running on {args.listen}:{args.port}", file=sys.stderr)
|
220
|
+
sys.exit(1)
|
188
221
|
else:
|
189
222
|
errorlog = "-"
|
190
223
|
accesslog = "-"
|
@@ -195,14 +228,14 @@ def start_gunicorn(daemon=False, baselog=None):
|
|
195
228
|
'timeout': 600,
|
196
229
|
'certfile': args.cert,
|
197
230
|
'keyfile': args.key,
|
198
|
-
'daemon':
|
231
|
+
'daemon': daemonized,
|
199
232
|
'errorlog': errorlog,
|
200
233
|
'accesslog': accesslog,
|
201
234
|
'pidfile': pidfile,
|
202
235
|
}
|
203
|
-
|
236
|
+
PyWebExec(app, options=options).run()
|
204
237
|
|
205
|
-
def daemon_d(action, pidfilepath, hostname=None, args=None):
|
238
|
+
def daemon_d(action, pidfilepath, silent=False, hostname=None, args=None):
|
206
239
|
"""start/stop daemon"""
|
207
240
|
import signal
|
208
241
|
import daemon, daemon.pidfile
|
@@ -222,10 +255,14 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
222
255
|
if status:
|
223
256
|
print(f"pywebexec running pid {pidfile.read_pid()}")
|
224
257
|
return True
|
225
|
-
|
258
|
+
if not silent:
|
259
|
+
print("pywebexec not running")
|
226
260
|
return False
|
227
261
|
elif action == "start":
|
228
|
-
|
262
|
+
status = pidfile.is_locked()
|
263
|
+
if status:
|
264
|
+
print(f"pywebexc already running pid {pidfile.read_pid()}", file=sys.stderr)
|
265
|
+
sys.exit(1)
|
229
266
|
log = open(pidfilepath + ".log", "ab+")
|
230
267
|
daemon_context = daemon.DaemonContext(
|
231
268
|
stderr=log,
|
@@ -240,7 +277,8 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
240
277
|
print(e)
|
241
278
|
|
242
279
|
def parseargs():
|
243
|
-
global app, args
|
280
|
+
global app, args, COMMAND_STATUS_DIR
|
281
|
+
|
244
282
|
parser = argparse.ArgumentParser(description='Run the command execution server.')
|
245
283
|
parser.add_argument('-u', '--user', help='Username for basic auth')
|
246
284
|
parser.add_argument('-P', '--password', help='Password for basic auth')
|
@@ -263,9 +301,11 @@ def parseargs():
|
|
263
301
|
parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
|
264
302
|
parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
|
265
303
|
parser.add_argument("-g", "--gencert", action="store_true", help="https server self signed cert")
|
266
|
-
parser.add_argument("
|
304
|
+
parser.add_argument("-T", "--tokenurl", action="store_true", help="generate safe url to access")
|
305
|
+
parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status/term", choices=["start","stop","restart","status","term"])
|
267
306
|
|
268
307
|
args = parser.parse_args()
|
308
|
+
cwd = os.getcwd()
|
269
309
|
if os.path.isdir(args.dir):
|
270
310
|
try:
|
271
311
|
os.chdir(args.dir)
|
@@ -279,8 +319,27 @@ def parseargs():
|
|
279
319
|
os.makedirs(COMMAND_STATUS_DIR)
|
280
320
|
if not os.path.exists(CONFDIR):
|
281
321
|
os.mkdir(CONFDIR, mode=0o700)
|
322
|
+
if args.action == "term":
|
323
|
+
COMMAND_STATUS_DIR = f"{os.getcwd()}/{COMMAND_STATUS_DIR}"
|
324
|
+
os.chdir(cwd)
|
325
|
+
command_id = str(uuid.uuid4())
|
326
|
+
start_time = datetime.now().isoformat()
|
327
|
+
user = pwd.getpwuid(os.getuid())[0]
|
328
|
+
update_command_status(command_id, 'running', command="term", params=[user,os.ttyname(sys.stdout.fileno())], start_time=start_time, user=user)
|
329
|
+
output_file_path = get_output_file_path(command_id)
|
330
|
+
res = os.system(f"script -f {output_file_path}")
|
331
|
+
end_time = datetime.now().isoformat()
|
332
|
+
update_command_status(command_id, status="success", end_time=end_time, exit_code=res)
|
333
|
+
sys.exit(res)
|
334
|
+
(hostname, ip) = resolve(gethostname()) if args.listen == '0.0.0.0' else resolve(args.listen)
|
335
|
+
url_params = ""
|
336
|
+
|
337
|
+
if args.tokenurl:
|
338
|
+
token = token_urlsafe()
|
339
|
+
app.config["TOKEN_URL"] = token
|
340
|
+
url_params = f"?token={token}"
|
341
|
+
|
282
342
|
if args.gencert:
|
283
|
-
hostname = resolve_hostname(gethostname())
|
284
343
|
args.cert = args.cert or f"{CONFDIR}/pywebexec.crt"
|
285
344
|
args.key = args.key or f"{CONFDIR}/pywebexec.key"
|
286
345
|
if not os.path.exists(args.cert):
|
@@ -301,9 +360,13 @@ def parseargs():
|
|
301
360
|
app.config['USER'] = None
|
302
361
|
app.config['PASSWORD'] = None
|
303
362
|
|
304
|
-
|
363
|
+
if args.action != 'stop':
|
364
|
+
print("Starting server:")
|
365
|
+
protocol = 'https' if args.cert else 'http'
|
366
|
+
print(f"{protocol}://{hostname}:{args.port}{url_params}")
|
367
|
+
print(f"{protocol}://{ip}:{args.port}{url_params}")
|
305
368
|
|
306
|
-
|
369
|
+
return args
|
307
370
|
|
308
371
|
def get_status_file_path(command_id):
|
309
372
|
return os.path.join(COMMAND_STATUS_DIR, f'{command_id}.json')
|
@@ -392,10 +455,22 @@ def run_command(command, params, command_id, user):
|
|
392
455
|
with open(get_output_file_path(command_id), 'a') as output_file:
|
393
456
|
output_file.write(str(e))
|
394
457
|
|
458
|
+
|
459
|
+
parseargs()
|
460
|
+
|
461
|
+
|
395
462
|
@app.before_request
|
396
463
|
def check_authentication():
|
464
|
+
# Check for token in URL if TOKEN_URL is set
|
465
|
+
token = app.config.get('TOKEN_URL')
|
466
|
+
if token and request.endpoint not in ['login', 'static']:
|
467
|
+
if request.args.get('token') == token:
|
468
|
+
return
|
469
|
+
return jsonify({'error': 'Forbidden'}), 403
|
470
|
+
|
397
471
|
if not app.config['USER'] and not app.config['LDAP_SERVER']:
|
398
472
|
return
|
473
|
+
|
399
474
|
if 'username' not in session and request.endpoint not in ['login', 'static']:
|
400
475
|
return auth.login_required(lambda: None)()
|
401
476
|
|
@@ -564,11 +639,13 @@ def get_command_output(command_id):
|
|
564
639
|
output = output_file.read().decode('utf-8', errors='replace')
|
565
640
|
new_offset = output_file.tell()
|
566
641
|
status_data = read_command_status(command_id) or {}
|
642
|
+
token = app.config.get("TOKEN_URL")
|
643
|
+
token_param = f"&token={token}" if token else ""
|
567
644
|
response = {
|
568
645
|
'output': output,
|
569
646
|
'status': status_data.get("status"),
|
570
647
|
'links': {
|
571
|
-
'next': f'{request.url_root}command_output/{command_id}?offset={new_offset}'
|
648
|
+
'next': f'{request.url_root}command_output/{command_id}?offset={new_offset}{token_param}'
|
572
649
|
}
|
573
650
|
}
|
574
651
|
if request.headers.get('Accept') == 'text/plain':
|
@@ -585,7 +662,7 @@ def list_executables():
|
|
585
662
|
def main():
|
586
663
|
basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
|
587
664
|
if args.action == "start":
|
588
|
-
return start_gunicorn(
|
665
|
+
return start_gunicorn(daemonized=True, baselog=basef)
|
589
666
|
if args.action:
|
590
667
|
return daemon_d(args.action, pidfilepath=basef)
|
591
668
|
return start_gunicorn()
|
pywebexec/static/js/commands.js
CHANGED
@@ -195,7 +195,7 @@ window.addEventListener('load', () => {
|
|
195
195
|
|
196
196
|
async function fetchExecutables() {
|
197
197
|
try {
|
198
|
-
const response = await fetch(
|
198
|
+
const response = await fetch(`/executables${urlToken}`);
|
199
199
|
if (!response.ok) {
|
200
200
|
throw new Error('Failed to fetch command status');
|
201
201
|
}
|
@@ -210,6 +210,8 @@ async function fetchExecutables() {
|
|
210
210
|
} catch (error) {
|
211
211
|
alert("Failed to fetch executables");
|
212
212
|
}
|
213
|
-
commandListSelect.size = Math.min(20, commandListSelect.options.length)
|
213
|
+
commandListSelect.size = Math.min(20, commandListSelect.options.length);
|
214
|
+
if (commandListSelect.options.length == 0)
|
215
|
+
document.getElementById('launchForm').style.display = 'none';
|
214
216
|
|
215
217
|
}
|
pywebexec/static/js/script.js
CHANGED
@@ -8,7 +8,7 @@ const terminal = new Terminal({
|
|
8
8
|
disableStdin: true,
|
9
9
|
convertEol: true,
|
10
10
|
fontFamily: 'Consolas NF, monospace, courier-new, courier',
|
11
|
-
|
11
|
+
scrollback: 999999,
|
12
12
|
theme: {
|
13
13
|
background: '#111412',
|
14
14
|
black: '#111412',
|
@@ -27,6 +27,12 @@ const fitAddon = new FitAddon.FitAddon();
|
|
27
27
|
terminal.loadAddon(fitAddon);
|
28
28
|
terminal.open(document.getElementById('output'));
|
29
29
|
fitAddon.fit();
|
30
|
+
function getTokenParam() {
|
31
|
+
const urlParams = new URLSearchParams(window.location.search);
|
32
|
+
return urlParams.get('token') ? `?token=${urlParams.get('token')}` : '';
|
33
|
+
}
|
34
|
+
const urlToken = getTokenParam();
|
35
|
+
|
30
36
|
|
31
37
|
terminal.onSelectionChange(() => {
|
32
38
|
const selectionText = terminal.getSelection();
|
@@ -37,12 +43,13 @@ terminal.onSelectionChange(() => {
|
|
37
43
|
}
|
38
44
|
});
|
39
45
|
|
46
|
+
|
40
47
|
document.getElementById('launchForm').addEventListener('submit', async (event) => {
|
41
48
|
event.preventDefault();
|
42
49
|
const commandName = document.getElementById('commandName').value;
|
43
50
|
const params = document.getElementById('params').value.split(' ');
|
44
51
|
try {
|
45
|
-
const response = await fetch(
|
52
|
+
const response = await fetch(`/run_command${urlToken}`, {
|
46
53
|
method: 'POST',
|
47
54
|
headers: {
|
48
55
|
'Content-Type': 'application/json'
|
@@ -65,7 +72,7 @@ document.getElementById('launchForm').addEventListener('submit', async (event) =
|
|
65
72
|
|
66
73
|
async function fetchCommands() {
|
67
74
|
try {
|
68
|
-
const response = await fetch(
|
75
|
+
const response = await fetch(`/commands${urlToken}`);
|
69
76
|
if (!response.ok) {
|
70
77
|
document.getElementById('dimmer').style.display = 'block';
|
71
78
|
return;
|
@@ -91,7 +98,7 @@ async function fetchCommands() {
|
|
91
98
|
<td>${command.command.replace(/^\.\//, '')}</td>
|
92
99
|
<td><span class="status-icon status-${command.status}"></span>${command.status}${command.status === 'failed' ? ` (${command.exit_code})` : ''}</td>
|
93
100
|
<td>
|
94
|
-
${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}', event)">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}', event)">Run</button>`}
|
101
|
+
${command.command.startsWith('term') ? '' : command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}', event)">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}', event)">Run</button>`}
|
95
102
|
</td>
|
96
103
|
<td class="monospace outcol">${command.last_output_line || ''}</td>
|
97
104
|
`;
|
@@ -129,11 +136,11 @@ async function fetchOutput(url) {
|
|
129
136
|
async function viewOutput(command_id) {
|
130
137
|
adjustOutputHeight();
|
131
138
|
currentCommandId = command_id;
|
132
|
-
nextOutputLink = `/command_output/${command_id}`;
|
139
|
+
nextOutputLink = `/command_output/${command_id}${urlToken}`;
|
133
140
|
clearInterval(outputInterval);
|
134
141
|
terminal.clear();
|
135
142
|
try {
|
136
|
-
const response = await fetch(`/command_status/${command_id}`);
|
143
|
+
const response = await fetch(`/command_status/${command_id}${urlToken}`);
|
137
144
|
if (!response.ok) {
|
138
145
|
return;
|
139
146
|
}
|
@@ -154,7 +161,7 @@ async function relaunchCommand(command_id, event) {
|
|
154
161
|
event.stopPropagation();
|
155
162
|
event.stopImmediatePropagation();
|
156
163
|
try {
|
157
|
-
const response = await fetch(`/command_status/${command_id}`);
|
164
|
+
const response = await fetch(`/command_status/${command_id}${urlToken}`);
|
158
165
|
if (!response.ok) {
|
159
166
|
throw new Error('Failed to fetch command status');
|
160
167
|
}
|
@@ -163,7 +170,7 @@ async function relaunchCommand(command_id, event) {
|
|
163
170
|
alert(data.error);
|
164
171
|
return;
|
165
172
|
}
|
166
|
-
const relaunchResponse = await fetch(
|
173
|
+
const relaunchResponse = await fetch(`/run_command${urlToken}`, {
|
167
174
|
method: 'POST',
|
168
175
|
headers: {
|
169
176
|
'Content-Type': 'application/json'
|
@@ -189,7 +196,7 @@ async function stopCommand(command_id, event) {
|
|
189
196
|
event.stopPropagation();
|
190
197
|
event.stopImmediatePropagation();
|
191
198
|
try {
|
192
|
-
const response = await fetch(`/stop_command/${command_id}`, {
|
199
|
+
const response = await fetch(`/stop_command/${command_id}${urlToken}`, {
|
193
200
|
method: 'POST'
|
194
201
|
});
|
195
202
|
if (!response.ok) {
|
pywebexec/templates/index.html
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="dimmer" class="dimmer">
|
12
|
-
<div class="dimmer-text">Server not
|
12
|
+
<div class="dimmer-text">Server not available</div>
|
13
13
|
</div>
|
14
14
|
<h2><span class="status-icon title-icon"></span>{{ title }}</h2>
|
15
15
|
<form id="launchForm" class="form-inline">
|
pywebexec/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: pywebexec
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.1
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
6
6
|
Author: Franck Jouvanceau
|
@@ -67,7 +67,7 @@ Requires-Dist: ldap3>=2.9.1
|
|
67
67
|
[](https://shields.io/)
|
68
68
|
|
69
69
|
# pywebexec
|
70
|
-
Simple Python HTTP(S) API/Web Command Launcher
|
70
|
+
Simple Python HTTP(S) API/Web Command Launcher and Terminal sharing
|
71
71
|
|
72
72
|
## Install
|
73
73
|
```
|
@@ -79,25 +79,29 @@ $ pip install pywebexec
|
|
79
79
|
* put in a directory the scripts/commands/links to commands you want to expose
|
80
80
|
* start http server serving current directory executables listening on 0.0.0.0 port 8080
|
81
81
|
```shell
|
82
|
-
$ pywebexec
|
82
|
+
$ pywebexec -d <dir>
|
83
83
|
```
|
84
84
|
|
85
85
|
* Launch commands with params/view live output/Status using browser
|
86
|
-
|
86
|
+
* Share your terminal output using `pywebexec -d <dir> term`
|
87
|
+
|
87
88
|
|
88
89
|
all commands output / statuses are available in the executables directory in subdirectory `.web_status`
|
90
|
+

|
89
91
|
|
90
92
|
## features
|
91
93
|
|
92
94
|
* Serve executables in a directory
|
93
95
|
* Launch commands with params from web browser or API call
|
96
|
+
* multiple share terminal output
|
94
97
|
* Follow live output
|
95
98
|
* Stop command
|
96
99
|
* Relaunch command
|
97
100
|
* HTTPS support
|
98
101
|
* HTTPS self-signed certificate generator
|
99
102
|
* Basic Auth
|
100
|
-
* LDAP(S)
|
103
|
+
* LDAP(S) password check/group member
|
104
|
+
* Safe url token generation
|
101
105
|
* Can be started as a daemon (POSIX)
|
102
106
|
* Uses gunicorn to serve http/https
|
103
107
|
* Linux/MacOS compatible
|
@@ -108,7 +112,18 @@ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080 --title myscripts
|
|
108
112
|
$ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080 -t myscripts
|
109
113
|
```
|
110
114
|
|
111
|
-
##
|
115
|
+
## Safe url token
|
116
|
+
|
117
|
+
* generate safe url, use the url to access the server
|
118
|
+
```shell
|
119
|
+
$ pywebexec -T
|
120
|
+
$ pywebexec --tokenurl
|
121
|
+
Starting server:
|
122
|
+
http://<host>:8080?token=jSTWiNgEVkddeEJ7I97x2ekOeaiXs2mErRSKNxm3DP0
|
123
|
+
http://x.x.x.x:8080?token=jSTWiNgEVkddeEJ7I97x2ekOeaiXs2mErRSKNxm3DP0
|
124
|
+
```
|
125
|
+
|
126
|
+
## Basic auth
|
112
127
|
|
113
128
|
* single user/password
|
114
129
|
```shell
|
@@ -1,6 +1,6 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
|
2
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
-
pywebexec/version.py,sha256=
|
2
|
+
pywebexec/pywebexec.py,sha256=ljvGw-inDwsP6KQqYwwfKUeN_q9k7JwyP7PfRacnqFQ,25475
|
3
|
+
pywebexec/version.py,sha256=cOVPCvD2h2G_2KB6G3ddreYkIQfAnS6WqgAkF_qgGOQ,411
|
4
4
|
pywebexec/static/css/style.css,sha256=nuJodEFojt_kCLPqbDBQAaBtWcRZ6uLjfI52mSf3EJA,5302
|
5
5
|
pywebexec/static/css/xterm.css,sha256=gy8_LGA7Q61DUf8ElwFQzHqHMBQnbbEmpgZcbdgeSHI,5383
|
6
6
|
pywebexec/static/images/aborted.svg,sha256=_mP43hU5QdRLFZIknBgjx-dIXrHgQG23-QV27ApXK2A,381
|
@@ -11,17 +11,17 @@ pywebexec/static/images/failed.svg,sha256=ADZ7IKrUyOXtqpivnz3VcH0-Wru-I5MOi3OJAk
|
|
11
11
|
pywebexec/static/images/favicon.svg,sha256=ti80IfuDZwIvQcmJxkOeUaB1iMsiyOPmQmVO-h0y1IU,1126
|
12
12
|
pywebexec/static/images/running.gif,sha256=iYuzQGkMxrakSIwt6gPieKCImGZoSAHmU5MUNZa7cpw,25696
|
13
13
|
pywebexec/static/images/success.svg,sha256=PJDcCSTevJh7rkfSFLtc7P0pbeh8PVQBS8DaOLQemmc,489
|
14
|
-
pywebexec/static/js/commands.js,sha256=
|
15
|
-
pywebexec/static/js/script.js,sha256
|
14
|
+
pywebexec/static/js/commands.js,sha256=VdMeCop9V5KwsR2v1J_OY1xFE7tJUYgcMg_lh2VGNjs,7476
|
15
|
+
pywebexec/static/js/script.js,sha256=WL8wvYjbAQpm_uMrGmuqx6rmHz9V_yMiGZPb1mU8xOU,10103
|
16
16
|
pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
|
17
17
|
pywebexec/static/js/xterm/ansi_up.min.js,sha256=KNGV0vEr30hNqKQimTAvGVy-icD5A1JqMQTtvYtKR2Y,13203
|
18
18
|
pywebexec/static/js/xterm/xterm-addon-fit.js,sha256=Pprm9pZe4SadVXS5Bc8b9VnC9Ex4QlWwA0pxOH53Gck,1460
|
19
19
|
pywebexec/static/js/xterm/xterm.js,sha256=Bzka76jZwEhVt_LlS0e0qMw7ryGa1p5qfxFyeohphBo,283371
|
20
20
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
-
pywebexec/templates/index.html,sha256=
|
22
|
-
pywebexec-1.
|
23
|
-
pywebexec-1.
|
24
|
-
pywebexec-1.
|
25
|
-
pywebexec-1.
|
26
|
-
pywebexec-1.
|
27
|
-
pywebexec-1.
|
21
|
+
pywebexec/templates/index.html,sha256=LaRXHXsOR2eWkBcLIlPxGKHSLTa8JfDkDCJZWadn_1Q,2116
|
22
|
+
pywebexec-1.3.1.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
23
|
+
pywebexec-1.3.1.dist-info/METADATA,sha256=lO1a0YsGGQR_yxguJZ5M-lSSETyT4MD8g7nLy_e0cQU,7397
|
24
|
+
pywebexec-1.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
25
|
+
pywebexec-1.3.1.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
26
|
+
pywebexec-1.3.1.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
27
|
+
pywebexec-1.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|