pywebexec 2.2.8__py3-none-any.whl → 2.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 +103 -53
- pywebexec/static/js/executables.js +2 -2
- pywebexec/static/js/schemaform.js +1 -1
- pywebexec/version.py +2 -2
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/METADATA +5 -3
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/RECORD +10 -10
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/WHEEL +0 -0
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/entry_points.txt +0 -0
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {pywebexec-2.2.8.dist-info → pywebexec-2.3.0.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -12,17 +12,23 @@ import string
|
|
12
12
|
from datetime import datetime, timezone, timedelta
|
13
13
|
import time
|
14
14
|
import shlex
|
15
|
-
|
15
|
+
import platform
|
16
|
+
import pexpect
|
17
|
+
|
18
|
+
if platform.system() != 'Windows':
|
19
|
+
from gunicorn.app.base import Application
|
20
|
+
import pwd
|
21
|
+
import fcntl
|
22
|
+
import termios
|
23
|
+
else:
|
24
|
+
from waitress import serve
|
25
|
+
import winpty
|
16
26
|
import ipaddress
|
17
27
|
from socket import socket, AF_INET, SOCK_STREAM
|
18
28
|
import ssl
|
19
29
|
import re
|
20
|
-
import pwd
|
21
30
|
from secrets import token_urlsafe
|
22
|
-
import pexpect
|
23
31
|
import signal
|
24
|
-
import fcntl
|
25
|
-
import termios
|
26
32
|
import struct
|
27
33
|
import subprocess
|
28
34
|
import logging
|
@@ -138,24 +144,24 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
138
144
|
|
139
145
|
return cert_pem, key_pem
|
140
146
|
|
147
|
+
if platform.system() != 'Windows':
|
148
|
+
class PyWebExec(Application):
|
141
149
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
self.application = app
|
147
|
-
super().__init__()
|
150
|
+
def __init__(self, app, options=None):
|
151
|
+
self.options = options or {}
|
152
|
+
self.application = app
|
153
|
+
super().__init__()
|
148
154
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
155
|
+
def load_config(self):
|
156
|
+
config = {
|
157
|
+
key: value for key, value in self.options.items()
|
158
|
+
if key in self.cfg.settings and value is not None
|
159
|
+
}
|
160
|
+
for key, value in config.items():
|
161
|
+
self.cfg.set(key.lower(), value)
|
156
162
|
|
157
|
-
|
158
|
-
|
163
|
+
def load(self):
|
164
|
+
return self.application
|
159
165
|
#38;2;66;59;165m
|
160
166
|
ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF]|[\x00-\x1F\x7F])')
|
161
167
|
ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF])')
|
@@ -455,7 +461,7 @@ def update_command_status(command_id, updates):
|
|
455
461
|
del status['last_read']
|
456
462
|
with open(status_file_path, 'w') as f:
|
457
463
|
json.dump(status, f)
|
458
|
-
|
464
|
+
os.fsync(f)
|
459
465
|
status_cache[command_id] = status
|
460
466
|
|
461
467
|
|
@@ -507,7 +513,6 @@ def script(output_file):
|
|
507
513
|
|
508
514
|
|
509
515
|
def run_command(fromip, user, command, params, command_id, rows, cols):
|
510
|
-
# app.logger.info(f'{fromip} run_command {command_id} {user}: {command} {params}')
|
511
516
|
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}')
|
512
517
|
start_time = datetime.now(timezone.utc).isoformat()
|
513
518
|
if user:
|
@@ -524,41 +529,68 @@ def run_command(fromip, user, command, params, command_id, rows, cols):
|
|
524
529
|
})
|
525
530
|
output_file_path = get_output_file_path(command_id)
|
526
531
|
try:
|
527
|
-
|
528
|
-
|
532
|
+
if platform.system() == 'Windows':
|
533
|
+
# On Windows, use winpty
|
534
|
+
cmdline = f"{sys.executable} -u {command} " + " ".join(shlex.quote(p) for p in params)
|
535
|
+
with open(output_file_path, 'wb', buffering=0) as fd:
|
536
|
+
p = winpty.PTY(cols, rows)
|
537
|
+
p.spawn(cmdline)
|
538
|
+
pid = p.pid
|
539
|
+
update_command_status(command_id, {
|
540
|
+
'pid': pid,
|
541
|
+
})
|
542
|
+
while True:
|
543
|
+
try:
|
544
|
+
if not p.isalive():
|
545
|
+
time.sleep(1)
|
546
|
+
data = p.read(10485760, blocking=False)
|
547
|
+
fd.write(data.encode())
|
548
|
+
if not p.isalive():
|
549
|
+
break
|
550
|
+
time.sleep(0.1)
|
551
|
+
except (EOFError, winpty.WinptyError):
|
552
|
+
break
|
553
|
+
status = p.get_exitstatus()
|
554
|
+
del p
|
555
|
+
print("end", status)
|
556
|
+
else:
|
557
|
+
# On Unix, use pexpect
|
558
|
+
with open(output_file_path, 'wb') as fd:
|
559
|
+
p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(rows, cols))
|
560
|
+
update_command_status(command_id, {
|
561
|
+
'pid': p.pid,
|
562
|
+
})
|
563
|
+
p.logfile = fd
|
564
|
+
p.expect(pexpect.EOF)
|
565
|
+
fd.flush()
|
566
|
+
status = p.wait()
|
567
|
+
|
568
|
+
end_time = datetime.now(timezone.utc).isoformat()
|
569
|
+
# Update the status based on the result
|
570
|
+
if status is None:
|
571
|
+
exit_code = -15
|
529
572
|
update_command_status(command_id, {
|
530
|
-
'
|
573
|
+
'status': 'aborted',
|
574
|
+
'end_time': end_time,
|
575
|
+
'exit_code': exit_code,
|
531
576
|
})
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
end_time = datetime.now(timezone.utc).isoformat()
|
537
|
-
# Update the status based on the result
|
538
|
-
if status is None:
|
539
|
-
exit_code = -15
|
577
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: command aborted')
|
578
|
+
else:
|
579
|
+
exit_code = status
|
580
|
+
if exit_code == 0:
|
540
581
|
update_command_status(command_id, {
|
541
|
-
'status': '
|
582
|
+
'status': 'success',
|
542
583
|
'end_time': end_time,
|
543
584
|
'exit_code': exit_code,
|
544
585
|
})
|
545
|
-
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}:
|
586
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
|
546
587
|
else:
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
})
|
554
|
-
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
|
555
|
-
else:
|
556
|
-
update_command_status(command_id, {
|
557
|
-
'status': 'failed',
|
558
|
-
'end_time': end_time,
|
559
|
-
'exit_code': exit_code,
|
560
|
-
})
|
561
|
-
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
|
588
|
+
update_command_status(command_id, {
|
589
|
+
'status': 'failed',
|
590
|
+
'end_time': end_time,
|
591
|
+
'exit_code': exit_code,
|
592
|
+
})
|
593
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
|
562
594
|
|
563
595
|
except Exception as e:
|
564
596
|
end_time = datetime.now(timezone.utc).isoformat()
|
@@ -661,7 +693,7 @@ app.config['TITLE'] = f"{args.title} API"
|
|
661
693
|
|
662
694
|
|
663
695
|
def get_executable(cmd):
|
664
|
-
if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
|
696
|
+
if os.path.isfile(cmd) and os.access(cmd, os.X_OK) and Path(cmd).suffix not in [".help", ".yaml", ".env", ".swp"]:
|
665
697
|
help_file = f"{cmd}.help"
|
666
698
|
help_text = ""
|
667
699
|
if os.path.exists(help_file) and os.path.isfile(help_file):
|
@@ -688,14 +720,17 @@ def get_executables():
|
|
688
720
|
def stop_command(command_id):
|
689
721
|
log_request(f"stop_command {command_id}")
|
690
722
|
status = read_command_status(command_id)
|
723
|
+
user = session.get('username', '-')
|
691
724
|
if not status or 'pid' not in status:
|
692
725
|
return jsonify({'error': 'Invalid command_id or command not running'}), 400
|
693
726
|
|
694
727
|
pid = status['pid']
|
695
728
|
end_time = datetime.now(timezone.utc).isoformat()
|
696
729
|
try:
|
697
|
-
|
698
|
-
|
730
|
+
try:
|
731
|
+
os.killpg(os.getpgid(pid), signal.SIGTERM) # Send SIGTERM to the process group
|
732
|
+
except:
|
733
|
+
os.kill(pid, signal.SIGINT) # Send SIGTERM to the process
|
699
734
|
except Exception as e:
|
700
735
|
update_command_status(command_id, {
|
701
736
|
'status': 'aborted',
|
@@ -703,6 +738,10 @@ def stop_command(command_id):
|
|
703
738
|
'exit_code': -15,
|
704
739
|
})
|
705
740
|
return jsonify({'error': 'Failed to terminate command'}), 500
|
741
|
+
output_file = get_output_file_path(command_id)
|
742
|
+
with open(output_file, 'a') as f:
|
743
|
+
f.write(f"\n\nCommand aborted by user {user} at {end_time}\n")
|
744
|
+
return jsonify({'message': 'Command aborted'})
|
706
745
|
|
707
746
|
|
708
747
|
@app.before_request
|
@@ -846,6 +885,8 @@ def run_dynamic_command(cmd):
|
|
846
885
|
if isinstance(data_params, dict):
|
847
886
|
params = ""
|
848
887
|
for param in schema_params.keys():
|
888
|
+
if not data_params.get(param, None) and schema_params[param].get("type", None) == "object":
|
889
|
+
data_params[param] = '{}'
|
849
890
|
if not param in data_params:
|
850
891
|
continue
|
851
892
|
value = data_params[param]
|
@@ -1160,6 +1201,15 @@ def main():
|
|
1160
1201
|
pywebexec.terminate()
|
1161
1202
|
sys.exit(res)
|
1162
1203
|
|
1204
|
+
if platform.system() == 'Windows':
|
1205
|
+
# Use waitress on Windows
|
1206
|
+
ssl_context = None
|
1207
|
+
if args.cert:
|
1208
|
+
ssl_context = (args.cert, args.key)
|
1209
|
+
serve(app, host=args.listen, port=args.port, url_scheme='https' if args.cert else 'http', threads=8)
|
1210
|
+
return 0
|
1211
|
+
|
1212
|
+
# Use gunicorn on Unix-like systems
|
1163
1213
|
if args.action == "start":
|
1164
1214
|
return start_gunicorn(daemonized=True, baselog=basef)
|
1165
1215
|
if args.action:
|
@@ -260,12 +260,13 @@ paramsInput.addEventListener('focus', () => {
|
|
260
260
|
const currentCmd = commandInput.value;
|
261
261
|
paramsInput.name = currentCmd;
|
262
262
|
if (gExecutables[currentCmd] && gExecutables[currentCmd].schema && gExecutables[currentCmd].schema.properties && paramsContainer.style.display == 'none') {
|
263
|
+
const schema = gExecutables[currentCmd].schema;
|
263
264
|
jsForm = createSchemaForm($('#schemaForm'), gExecutables[currentCmd].schema, async function (errors, values) {
|
264
265
|
const commandName = commandInput.value;
|
265
266
|
fitAddon.fit();
|
266
267
|
terminal.clear();
|
267
268
|
payload = { params: values, rows: terminal.rows, cols: terminal.cols }
|
268
|
-
if ('parallel' in values) {
|
269
|
+
if (schema.schema_options && schema.schema_options.batch_param && 'parallel' in values) {
|
269
270
|
payload['parallel'] = values['parallel'];
|
270
271
|
payload['delay'] = values['delay'];
|
271
272
|
delete payload['params']['parallel'];
|
@@ -301,7 +302,6 @@ paramsInput.addEventListener('focus', () => {
|
|
301
302
|
input1.focus();
|
302
303
|
}
|
303
304
|
schemaFormPW.addEventListener('submit', (event) => {
|
304
|
-
console.log('Form submitted');
|
305
305
|
paramsContainer.style.display = 'none';
|
306
306
|
event.preventDefault();
|
307
307
|
});
|
@@ -251,7 +251,7 @@ function createSchemaForm($form, schema, onSubmit, schemaName) {
|
|
251
251
|
err.classList.add('alert');
|
252
252
|
err.style.display = 'none';
|
253
253
|
schemaForm.appendChild(err);
|
254
|
-
validateSchemaForm(schemaForm, formDesc, schema,
|
254
|
+
validateSchemaForm(schemaForm, formDesc, schema, jsform.root.getFormValues(), schemaName);
|
255
255
|
schemaForm.querySelectorAll('textarea').forEach(txt => {
|
256
256
|
txt.style.height = "0";
|
257
257
|
setTimeout(() => adjustTxtHeight(txt), 1);
|
pywebexec/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pywebexec
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.3.0
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
6
6
|
Author: Franck Jouvanceau
|
@@ -52,16 +52,18 @@ Classifier: Topic :: System :: Systems Administration
|
|
52
52
|
Requires-Python: >=3.6
|
53
53
|
Description-Content-Type: text/markdown
|
54
54
|
License-File: LICENSE
|
55
|
-
Requires-Dist: python-daemon>=2.3.2
|
55
|
+
Requires-Dist: python-daemon>=2.3.2; platform_system != "Windows"
|
56
56
|
Requires-Dist: cryptography>=40.0.2
|
57
57
|
Requires-Dist: Flask>=2.0.3
|
58
58
|
Requires-Dist: Flask-HTTPAuth>=4.8.0
|
59
59
|
Requires-Dist: pexpect>=4.9.0
|
60
|
-
Requires-Dist: gunicorn>=21.2.0
|
60
|
+
Requires-Dist: gunicorn>=21.2.0; platform_system != "Windows"
|
61
61
|
Requires-Dist: ldap3>=2.9.1
|
62
62
|
Requires-Dist: pyte>=0.8.1
|
63
63
|
Requires-Dist: PyYAML>=6.0.1
|
64
64
|
Requires-Dist: run-para>=1.0.2
|
65
|
+
Requires-Dist: waitress>=3.0.2; platform_system == "Windows"
|
66
|
+
Requires-Dist: pywinpty>=2.0.15; platform_system == "Windows"
|
65
67
|
Dynamic: license-file
|
66
68
|
|
67
69
|
[](https://pypi.org/project/pywebexec/)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=197fHJy0UDBwTTpGCGortZRr-w2kTaD7MxqdbVmTEi0,61
|
2
2
|
pywebexec/host_ip.py,sha256=Ud_HTflWVQ8789aoQ2RZdT1wGI-ccvrwSWGz_c7T3TI,1241
|
3
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
+
pywebexec/pywebexec.py,sha256=q2q4NVgWeMmAegM-iK8J9B175snHyHkW45WYPlHYJpQ,47727
|
4
4
|
pywebexec/swagger.yaml,sha256=I_oLpp7Hqel8SDEEykvpmCT-Gv3ytGlziq9bvQOrtZY,7598
|
5
|
-
pywebexec/version.py,sha256=
|
5
|
+
pywebexec/version.py,sha256=U--yqU7RFo8hQQm8oopUGYLkafj4phNIVfkf5HFEal8,511
|
6
6
|
pywebexec/static/css/form.css,sha256=XC_0ES5yMHYz0S2OHR0RAboQN7fBUmg5ZIq8Qm5rHP0,5806
|
7
7
|
pywebexec/static/css/markdown.css,sha256=br4-iK9wigTs54N2KHtjgZ4KLH0THVSvJo-XZAdMHiE,1970
|
8
8
|
pywebexec/static/css/style.css,sha256=R1VOPNV2ztROKy9Fgf3tvUrtuKagY027tFJ8C866yWU,9991
|
@@ -33,9 +33,9 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
|
|
33
33
|
pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
|
34
34
|
pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
|
35
35
|
pywebexec/static/images/swagger-ui.svg,sha256=FR0yeOVwe4zCYKZAjCGcT_m0Mf25NexIVaSXifIkoU0,2117
|
36
|
-
pywebexec/static/js/executables.js,sha256=
|
36
|
+
pywebexec/static/js/executables.js,sha256=cTgCFHr_F9bFCirtfG_uR32vOY3vNUr4Ih3Wglj5lFc,11988
|
37
37
|
pywebexec/static/js/popup.js,sha256=O3DEWnyb5yGW9tjODYycc-ujWndyAfnJMxulaQeogtc,9700
|
38
|
-
pywebexec/static/js/schemaform.js,sha256=
|
38
|
+
pywebexec/static/js/schemaform.js,sha256=NlFXFKJI53izxPXct3a5XiB1RhWGt0_EIp6o1HfsryU,9624
|
39
39
|
pywebexec/static/js/script.js,sha256=yQP2dWwTObWbg6ROeKFxwYi1NfDd2gxEn2e3oL-FWL8,18379
|
40
40
|
pywebexec/static/js/swagger-form.js,sha256=CLcSHMhk5P4-_2MIRBoJLgEnIj_9keDDSzUugXHZjio,4565
|
41
41
|
pywebexec/static/js/js-yaml/LICENSE,sha256=oHvCRGi5ZUznalR9R6LbKC0HcztxXbTHOpi9Y5YflVA,1084
|
@@ -67,9 +67,9 @@ pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
67
67
|
pywebexec/templates/index.html,sha256=w18O2plH_yS8bqlPsu5hwFFmCj9H2hWLSV8B6ADcSwU,3900
|
68
68
|
pywebexec/templates/popup.html,sha256=3kpMccKD_OLLhJ4Y9KRw6Ny8wQWjVaRrUfV9y5-bDiQ,1580
|
69
69
|
pywebexec/templates/swagger_ui.html,sha256=MAPr-z96VERAecDvX37V8q2Nxph-O0fNDBul1x2w9SI,1147
|
70
|
-
pywebexec-2.
|
71
|
-
pywebexec-2.
|
72
|
-
pywebexec-2.
|
73
|
-
pywebexec-2.
|
74
|
-
pywebexec-2.
|
75
|
-
pywebexec-2.
|
70
|
+
pywebexec-2.3.0.dist-info/licenses/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
71
|
+
pywebexec-2.3.0.dist-info/METADATA,sha256=y1F2WI5mfVSDdt_GznPWBmK07NrToa8qOjSqls1r0kM,13015
|
72
|
+
pywebexec-2.3.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
73
|
+
pywebexec-2.3.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
74
|
+
pywebexec-2.3.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
75
|
+
pywebexec-2.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|