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 CHANGED
@@ -12,17 +12,23 @@ import string
12
12
  from datetime import datetime, timezone, timedelta
13
13
  import time
14
14
  import shlex
15
- from gunicorn.app.base import Application
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
- class PyWebExec(Application):
143
-
144
- def __init__(self, app, options=None):
145
- self.options = options or {}
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
- def load_config(self):
150
- config = {
151
- key: value for key, value in self.options.items()
152
- if key in self.cfg.settings and value is not None
153
- }
154
- for key, value in config.items():
155
- self.cfg.set(key.lower(), value)
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
- def load(self):
158
- return self.application
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
- os.sync()
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
- with open(output_file_path, 'wb') as fd:
528
- p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(rows, cols))
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
- 'pid': p.pid,
583
+ 'status': 'aborted',
584
+ 'end_time': end_time,
585
+ 'exit_code': exit_code,
531
586
  })
532
- p.logfile = fd
533
- p.expect(pexpect.EOF)
534
- fd.flush()
535
- status = p.wait()
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': 'aborted',
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)}: command aborted')
596
+ log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
546
597
  else:
547
- exit_code = status
548
- if exit_code == 0:
549
- update_command_status(command_id, {
550
- 'status': 'success',
551
- 'end_time': end_time,
552
- 'exit_code': exit_code,
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
- os.killpg(os.getpgid(pid), 15) # Send SIGTERM to the process group
698
- return jsonify({'message': 'Command aborted'})
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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.2.9'
21
- __version_tuple__ = version_tuple = (2, 2, 9)
20
+ __version__ = version = '2.3.1'
21
+ __version_tuple__ = version_tuple = (2, 3, 1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywebexec
3
- Version: 2.2.9
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
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](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=R-jp9BiUMJZW7m9N9kQC6dpIueUgFVDo4n9_GFb1rOs,45734
3
+ pywebexec/pywebexec.py,sha256=LD917ZeDVnOSMuLWPkl9JT1baaDvZ_e0Yk7RTs5yBfk,48273
4
4
  pywebexec/swagger.yaml,sha256=I_oLpp7Hqel8SDEEykvpmCT-Gv3ytGlziq9bvQOrtZY,7598
5
- pywebexec/version.py,sha256=vfAUNhf_nRXyidROZBr9-mByYfQRM-Vg2szu6KEhvHY,511
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=jB4QqWi4Qeq1uJflTSqqjsZy-ec9iy5ZmFvfNWMzXq4,11913
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.2.9.dist-info/licenses/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
71
- pywebexec-2.2.9.dist-info/METADATA,sha256=1a8pUASu6I7njLd3y1krziCRCMjotkdEG3NXGjzbgOA,12832
72
- pywebexec-2.2.9.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
73
- pywebexec-2.2.9.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
74
- pywebexec-2.2.9.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
75
- pywebexec-2.2.9.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5