pywebexec 2.2.9__py3-none-any.whl → 2.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 +113 -53
- pywebexec/static/js/executables.js +2 -2
- pywebexec/version.py +2 -2
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.dist-info}/METADATA +5 -3
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.dist-info}/RECORD +9 -9
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.dist-info}/WHEEL +1 -1
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.dist-info}/entry_points.txt +0 -0
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {pywebexec-2.2.9.dist-info → pywebexec-2.3.1.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
|
+
from winpty import PtyProcess, WinptyError
|
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
|
@@ -66,6 +72,7 @@ CONFDIR += "/.pywebexec"
|
|
66
72
|
term_command_id = str(uuid.uuid4())
|
67
73
|
tty_cols = 125
|
68
74
|
tty_rows = 30
|
75
|
+
os.environ["PYWEBEXEC"] = "true"
|
69
76
|
|
70
77
|
# In-memory cache for command statuses
|
71
78
|
status_cache = {}
|
@@ -138,24 +145,24 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
138
145
|
|
139
146
|
return cert_pem, key_pem
|
140
147
|
|
148
|
+
if platform.system() != 'Windows':
|
149
|
+
class PyWebExec(Application):
|
141
150
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
self.application = app
|
147
|
-
super().__init__()
|
151
|
+
def __init__(self, app, options=None):
|
152
|
+
self.options = options or {}
|
153
|
+
self.application = app
|
154
|
+
super().__init__()
|
148
155
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
+
def load_config(self):
|
157
|
+
config = {
|
158
|
+
key: value for key, value in self.options.items()
|
159
|
+
if key in self.cfg.settings and value is not None
|
160
|
+
}
|
161
|
+
for key, value in config.items():
|
162
|
+
self.cfg.set(key.lower(), value)
|
156
163
|
|
157
|
-
|
158
|
-
|
164
|
+
def load(self):
|
165
|
+
return self.application
|
159
166
|
#38;2;66;59;165m
|
160
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]|[\x00-\x1F\x7F])')
|
161
168
|
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])')
|
@@ -358,6 +365,7 @@ def parseargs():
|
|
358
365
|
parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
|
359
366
|
parser.add_argument("-g", "--gencert", action="store_true", help="https server self signed cert")
|
360
367
|
parser.add_argument("-T", "--tokenurl", action="store_true", help="generate safe url to access")
|
368
|
+
parser.add_argument("-n", "--notty", action="store_true", help="no span commands in tty")
|
361
369
|
parser.add_argument("-C", "--cols", type=int, default=tty_cols, help="terminal columns")
|
362
370
|
parser.add_argument("-R", "--rows", type=int, default=tty_rows, help="terminal rows")
|
363
371
|
parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status/shareterm/term",
|
@@ -455,7 +463,7 @@ def update_command_status(command_id, updates):
|
|
455
463
|
del status['last_read']
|
456
464
|
with open(status_file_path, 'w') as f:
|
457
465
|
json.dump(status, f)
|
458
|
-
|
466
|
+
os.fsync(f)
|
459
467
|
status_cache[command_id] = status
|
460
468
|
|
461
469
|
|
@@ -507,7 +515,6 @@ def script(output_file):
|
|
507
515
|
|
508
516
|
|
509
517
|
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
518
|
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}')
|
512
519
|
start_time = datetime.now(timezone.utc).isoformat()
|
513
520
|
if user:
|
@@ -524,41 +531,76 @@ def run_command(fromip, user, command, params, command_id, rows, cols):
|
|
524
531
|
})
|
525
532
|
output_file_path = get_output_file_path(command_id)
|
526
533
|
try:
|
527
|
-
|
528
|
-
|
534
|
+
if args.notty:
|
535
|
+
os.environ["PYTHONIOENCODING"] = "utf-8"
|
536
|
+
os.environ["PYTHONLEGACYWINDOWSSTDIO"] = "utf-8"
|
537
|
+
with open(output_file_path, 'wb', buffering=0) as fd:
|
538
|
+
p = subprocess.Popen([sys.executable, "-u", command, *params], stdout=fd, stderr=fd, bufsize=1, text=False)
|
539
|
+
pid = p.pid
|
540
|
+
update_command_status(command_id, {
|
541
|
+
'pid': pid,
|
542
|
+
})
|
543
|
+
p.wait()
|
544
|
+
status = p.returncode
|
545
|
+
elif platform.system() == 'Windows':
|
546
|
+
# On Windows, use winpty
|
547
|
+
with open(output_file_path, 'wb', buffering=0) as fd:
|
548
|
+
p = PtyProcess.spawn([sys.executable, "-u", command, *params], dimensions=(rows, cols))
|
549
|
+
pid = p.pid
|
550
|
+
update_command_status(command_id, {
|
551
|
+
'pid': pid,
|
552
|
+
})
|
553
|
+
while True:
|
554
|
+
try:
|
555
|
+
if not p.isalive():
|
556
|
+
time.sleep(1)
|
557
|
+
data = p.read(10485760)
|
558
|
+
fd.write(data.encode())
|
559
|
+
if not p.isalive():
|
560
|
+
break
|
561
|
+
time.sleep(0.1)
|
562
|
+
except (EOFError, WinptyError):
|
563
|
+
break
|
564
|
+
status = p.exitstatus
|
565
|
+
p.close()
|
566
|
+
else:
|
567
|
+
# On Unix, use pexpect
|
568
|
+
with open(output_file_path, 'wb') as fd:
|
569
|
+
p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(rows, cols))
|
570
|
+
update_command_status(command_id, {
|
571
|
+
'pid': p.pid,
|
572
|
+
})
|
573
|
+
p.logfile = fd
|
574
|
+
p.expect(pexpect.EOF)
|
575
|
+
fd.flush()
|
576
|
+
status = p.wait()
|
577
|
+
|
578
|
+
end_time = datetime.now(timezone.utc).isoformat()
|
579
|
+
# Update the status based on the result
|
580
|
+
if status is None:
|
581
|
+
exit_code = -15
|
529
582
|
update_command_status(command_id, {
|
530
|
-
'
|
583
|
+
'status': 'aborted',
|
584
|
+
'end_time': end_time,
|
585
|
+
'exit_code': exit_code,
|
531
586
|
})
|
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
|
587
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: command aborted')
|
588
|
+
else:
|
589
|
+
exit_code = status
|
590
|
+
if exit_code == 0:
|
540
591
|
update_command_status(command_id, {
|
541
|
-
'status': '
|
592
|
+
'status': 'success',
|
542
593
|
'end_time': end_time,
|
543
594
|
'exit_code': exit_code,
|
544
595
|
})
|
545
|
-
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}:
|
596
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
|
546
597
|
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}')
|
598
|
+
update_command_status(command_id, {
|
599
|
+
'status': 'failed',
|
600
|
+
'end_time': end_time,
|
601
|
+
'exit_code': exit_code,
|
602
|
+
})
|
603
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
|
562
604
|
|
563
605
|
except Exception as e:
|
564
606
|
end_time = datetime.now(timezone.utc).isoformat()
|
@@ -661,7 +703,7 @@ app.config['TITLE'] = f"{args.title} API"
|
|
661
703
|
|
662
704
|
|
663
705
|
def get_executable(cmd):
|
664
|
-
if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
|
706
|
+
if os.path.isfile(cmd) and os.access(cmd, os.X_OK) and Path(cmd).suffix not in [".help", ".yaml", ".env", ".swp"]:
|
665
707
|
help_file = f"{cmd}.help"
|
666
708
|
help_text = ""
|
667
709
|
if os.path.exists(help_file) and os.path.isfile(help_file):
|
@@ -688,14 +730,17 @@ def get_executables():
|
|
688
730
|
def stop_command(command_id):
|
689
731
|
log_request(f"stop_command {command_id}")
|
690
732
|
status = read_command_status(command_id)
|
733
|
+
user = session.get('username', '-')
|
691
734
|
if not status or 'pid' not in status:
|
692
735
|
return jsonify({'error': 'Invalid command_id or command not running'}), 400
|
693
736
|
|
694
737
|
pid = status['pid']
|
695
738
|
end_time = datetime.now(timezone.utc).isoformat()
|
696
739
|
try:
|
697
|
-
|
698
|
-
|
740
|
+
try:
|
741
|
+
os.killpg(os.getpgid(pid), signal.SIGTERM) # Send SIGTERM to the process group
|
742
|
+
except:
|
743
|
+
os.kill(pid, signal.SIGINT) # Send SIGTERM to the process
|
699
744
|
except Exception as e:
|
700
745
|
update_command_status(command_id, {
|
701
746
|
'status': 'aborted',
|
@@ -703,6 +748,10 @@ def stop_command(command_id):
|
|
703
748
|
'exit_code': -15,
|
704
749
|
})
|
705
750
|
return jsonify({'error': 'Failed to terminate command'}), 500
|
751
|
+
output_file = get_output_file_path(command_id)
|
752
|
+
with open(output_file, 'a') as f:
|
753
|
+
f.write(f"\n\nCommand aborted by user {user} at {end_time}\n")
|
754
|
+
return jsonify({'message': 'Command aborted'})
|
706
755
|
|
707
756
|
|
708
757
|
@app.before_request
|
@@ -846,6 +895,8 @@ def run_dynamic_command(cmd):
|
|
846
895
|
if isinstance(data_params, dict):
|
847
896
|
params = ""
|
848
897
|
for param in schema_params.keys():
|
898
|
+
if not data_params.get(param, None) and schema_params[param].get("type", None) == "object":
|
899
|
+
data_params[param] = '{}'
|
849
900
|
if not param in data_params:
|
850
901
|
continue
|
851
902
|
value = data_params[param]
|
@@ -1160,6 +1211,15 @@ def main():
|
|
1160
1211
|
pywebexec.terminate()
|
1161
1212
|
sys.exit(res)
|
1162
1213
|
|
1214
|
+
if platform.system() == 'Windows':
|
1215
|
+
# Use waitress on Windows
|
1216
|
+
ssl_context = None
|
1217
|
+
if args.cert:
|
1218
|
+
ssl_context = (args.cert, args.key)
|
1219
|
+
serve(app, host=args.listen, port=args.port, url_scheme='https' if args.cert else 'http', threads=8)
|
1220
|
+
return 0
|
1221
|
+
|
1222
|
+
# Use gunicorn on Unix-like systems
|
1163
1223
|
if args.action == "start":
|
1164
1224
|
return start_gunicorn(daemonized=True, baselog=basef)
|
1165
1225
|
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
|
});
|
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.1
|
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=LD917ZeDVnOSMuLWPkl9JT1baaDvZ_e0Yk7RTs5yBfk,48273
|
4
4
|
pywebexec/swagger.yaml,sha256=I_oLpp7Hqel8SDEEykvpmCT-Gv3ytGlziq9bvQOrtZY,7598
|
5
|
-
pywebexec/version.py,sha256=
|
5
|
+
pywebexec/version.py,sha256=4lLWfgycoQE7rafXKcKQeSzbG6DAo6_kH0qn9J_0diQ,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,7 +33,7 @@ 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
38
|
pywebexec/static/js/schemaform.js,sha256=NlFXFKJI53izxPXct3a5XiB1RhWGt0_EIp6o1HfsryU,9624
|
39
39
|
pywebexec/static/js/script.js,sha256=yQP2dWwTObWbg6ROeKFxwYi1NfDd2gxEn2e3oL-FWL8,18379
|
@@ -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.1.dist-info/licenses/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
71
|
+
pywebexec-2.3.1.dist-info/METADATA,sha256=7CcAZIQPHq5Y_txEFyDwBzSRE1o4TPyJYHgZtSeQvuM,13015
|
72
|
+
pywebexec-2.3.1.dist-info/WHEEL,sha256=L0N565qmK-3nM2eBoMNFszYJ_MTx03_tQ0CQu1bHLYo,91
|
73
|
+
pywebexec-2.3.1.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
74
|
+
pywebexec-2.3.1.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
75
|
+
pywebexec-2.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|