pywebexec 1.2.9__py3-none-any.whl → 1.3.0__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 -20
- 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.9.dist-info → pywebexec-1.3.0.dist-info}/METADATA +18 -4
- {pywebexec-1.2.9.dist-info → pywebexec-1.3.0.dist-info}/RECORD +11 -11
- {pywebexec-1.2.9.dist-info → pywebexec-1.3.0.dist-info}/LICENSE +0 -0
- {pywebexec-1.2.9.dist-info → pywebexec-1.3.0.dist-info}/WHEEL +0 -0
- {pywebexec-1.2.9.dist-info → pywebexec-1.3.0.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.2.9.dist-info → pywebexec-1.3.0.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,17 +455,28 @@ 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
|
|
402
477
|
@auth.verify_password
|
403
478
|
def verify_password(username, password):
|
404
479
|
if not username:
|
405
|
-
session['username'] = '-'
|
406
480
|
return False
|
407
481
|
if app.config['USER']:
|
408
482
|
if username == app.config['USER'] and password == app.config['PASSWORD']:
|
@@ -565,11 +639,13 @@ def get_command_output(command_id):
|
|
565
639
|
output = output_file.read().decode('utf-8', errors='replace')
|
566
640
|
new_offset = output_file.tell()
|
567
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 ""
|
568
644
|
response = {
|
569
645
|
'output': output,
|
570
646
|
'status': status_data.get("status"),
|
571
647
|
'links': {
|
572
|
-
'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}'
|
573
649
|
}
|
574
650
|
}
|
575
651
|
if request.headers.get('Accept') == 'text/plain':
|
@@ -586,7 +662,7 @@ def list_executables():
|
|
586
662
|
def main():
|
587
663
|
basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
|
588
664
|
if args.action == "start":
|
589
|
-
return start_gunicorn(
|
665
|
+
return start_gunicorn(daemonized=True, baselog=basef)
|
590
666
|
if args.action:
|
591
667
|
return daemon_d(args.action, pidfilepath=basef)
|
592
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.0
|
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,10 +79,12 @@ $ 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
|
+
* Share your terminal output using `pywebexec -d <dir> term`
|
87
|
+
|
86
88
|

|
87
89
|
|
88
90
|
all commands output / statuses are available in the executables directory in subdirectory `.web_status`
|
@@ -98,6 +100,7 @@ all commands output / statuses are available in the executables directory in sub
|
|
98
100
|
* HTTPS self-signed certificate generator
|
99
101
|
* Basic Auth
|
100
102
|
* LDAP(S)
|
103
|
+
* safe url token
|
101
104
|
* Can be started as a daemon (POSIX)
|
102
105
|
* Uses gunicorn to serve http/https
|
103
106
|
* Linux/MacOS compatible
|
@@ -108,7 +111,18 @@ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080 --title myscripts
|
|
108
111
|
$ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080 -t myscripts
|
109
112
|
```
|
110
113
|
|
111
|
-
##
|
114
|
+
## Safe url token
|
115
|
+
|
116
|
+
* generate safe url, use the url to access the server
|
117
|
+
```shell
|
118
|
+
$ pywebexec -T
|
119
|
+
$ pywebexec --tokenurl
|
120
|
+
Starting server:
|
121
|
+
http://<host>:8080?token=jSTWiNgEVkddeEJ7I97x2ekOeaiXs2mErRSKNxm3DP0
|
122
|
+
http://x.x.x.x:8080?token=jSTWiNgEVkddeEJ7I97x2ekOeaiXs2mErRSKNxm3DP0
|
123
|
+
```
|
124
|
+
|
125
|
+
## Basic auth
|
112
126
|
|
113
127
|
* single user/password
|
114
128
|
```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=HGwtpza1HCPtlyqElUvIyH97K44TO13CYiYVZNezQ1M,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.0.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
23
|
+
pywebexec-1.3.0.dist-info/METADATA,sha256=-9Gg5Bjeonp57OkAoVCq6BOTjG5EK6hoLH31q_AnkzU,7325
|
24
|
+
pywebexec-1.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
25
|
+
pywebexec-1.3.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
26
|
+
pywebexec-1.3.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
27
|
+
pywebexec-1.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|