pywebexec 1.1.1__tar.gz → 1.1.11__tar.gz

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.
Files changed (28) hide show
  1. {pywebexec-1.1.1/pywebexec.egg-info → pywebexec-1.1.11}/PKG-INFO +18 -15
  2. {pywebexec-1.1.1 → pywebexec-1.1.11}/README.md +16 -13
  3. {pywebexec-1.1.1 → pywebexec-1.1.11}/pyproject.toml +1 -1
  4. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/pywebexec.py +16 -10
  5. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/css/style.css +40 -11
  6. pywebexec-1.1.11/pywebexec/static/images/running.gif +0 -0
  7. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/js/script.js +6 -5
  8. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/templates/index.html +1 -1
  9. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/version.py +2 -2
  10. {pywebexec-1.1.1 → pywebexec-1.1.11/pywebexec.egg-info}/PKG-INFO +18 -15
  11. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/SOURCES.txt +1 -1
  12. pywebexec-1.1.1/pywebexec/static/images/running.svg +0 -1
  13. {pywebexec-1.1.1 → pywebexec-1.1.11}/.github/workflows/python-publish.yml +0 -0
  14. {pywebexec-1.1.1 → pywebexec-1.1.11}/.gitignore +0 -0
  15. {pywebexec-1.1.1 → pywebexec-1.1.11}/LICENSE +0 -0
  16. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/__init__.py +0 -0
  17. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/aborted.svg +0 -0
  18. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/copy.svg +0 -0
  19. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/copy_ok.svg +0 -0
  20. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/failed.svg +0 -0
  21. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/favicon.svg +0 -0
  22. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/success.svg +0 -0
  23. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/templates/__init__.py +0 -0
  24. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/dependency_links.txt +0 -0
  25. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/entry_points.txt +0 -0
  26. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/requires.txt +0 -0
  27. {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/top_level.txt +0 -0
  28. {pywebexec-1.1.1 → pywebexec-1.1.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.1.1
3
+ Version: 1.1.11
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -30,7 +30,7 @@ License: MIT License
30
30
  Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
31
  Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
32
  Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
- Keywords: http,fileserver,browser,explorer
33
+ Keywords: http,server,remote commands,api,website
34
34
  Classifier: Development Status :: 5 - Production/Stable
35
35
  Classifier: Intended Audience :: System Administrators
36
36
  Classifier: License :: OSI Approved :: MIT License
@@ -63,7 +63,7 @@ Requires-Dist: ldap3>=2.9.1
63
63
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
64
64
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
65
65
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
66
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
66
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
67
67
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
68
68
 
69
69
  # pywebexec
@@ -83,7 +83,9 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
86
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
87
+
88
+ all commands output / statuses are available in the executables directory in subdirectory `.web_status`
87
89
 
88
90
  ## features
89
91
 
@@ -97,13 +99,13 @@ $ pywebexec
97
99
  * Basic Auth
98
100
  * LDAP(S)
99
101
  * Can be started as a daemon (POSIX)
100
- * uses gunicorn to serve http/https
101
- * compatible Linux/MacOS
102
+ * Uses gunicorn to serve http/https
103
+ * Linux/MacOS compatible
102
104
 
103
105
  ## Customize server
104
106
  ```shell
105
- $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080
106
- $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080
107
+ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080 --title myscripts
108
+ $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080 -t myscripts
107
109
  ```
108
110
 
109
111
  ## Basic auth
@@ -117,11 +119,10 @@ Generated password is given if no `--pasword` option
117
119
 
118
120
  * ldap(s) password check / group member
119
121
  ```shell
120
- $ export PYWEBEXEC_LDAP_SERVER=ldap.forumsys.com
121
- $ export PYWEBEXEC_LDAP_USE_SSL=0
122
+ $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
122
123
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
123
124
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
124
- $ export PYWEBEXEC_LDAP_GROUPS=mathematicians,scientists
125
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
125
126
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
126
127
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
127
128
  $ pywebexec
@@ -147,12 +148,14 @@ $ pywebexec start
147
148
  $ pywebexec status
148
149
  $ pywebexec stop
149
150
  ```
150
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
152
 
152
153
  ## Launch command through API
153
154
 
154
155
  ```shell
155
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
156
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
157
+ $ curl http://myhost:8080/command_status/<command_id>
158
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
156
159
  ```
157
160
 
158
161
  ## API reference
@@ -162,7 +165,7 @@ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST
162
165
  |-----------|-----------------------------|--------------------|---------------------|
163
166
  | POST | /run_command | command: str<br>params: array[str] | command_id: uuid<br>message: str |
164
167
  | POST | /stop_command/command_id | | message: str |
165
- | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
168
+ | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
166
169
  | GET | /command_output/command_id | | output: str<br>status: str |
167
- | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
170
+ | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
168
171
  | GET | /executables | | array of str |
@@ -1,7 +1,7 @@
1
1
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
2
2
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
3
3
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
4
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
4
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
5
5
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
6
6
 
7
7
  # pywebexec
@@ -21,7 +21,9 @@ $ pywebexec
21
21
  ```
22
22
 
23
23
  * Launch commands with params/view live output/Status using browser
24
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
24
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
25
+
26
+ all commands output / statuses are available in the executables directory in subdirectory `.web_status`
25
27
 
26
28
  ## features
27
29
 
@@ -35,13 +37,13 @@ $ pywebexec
35
37
  * Basic Auth
36
38
  * LDAP(S)
37
39
  * Can be started as a daemon (POSIX)
38
- * uses gunicorn to serve http/https
39
- * compatible Linux/MacOS
40
+ * Uses gunicorn to serve http/https
41
+ * Linux/MacOS compatible
40
42
 
41
43
  ## Customize server
42
44
  ```shell
43
- $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080
44
- $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080
45
+ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080 --title myscripts
46
+ $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080 -t myscripts
45
47
  ```
46
48
 
47
49
  ## Basic auth
@@ -55,11 +57,10 @@ Generated password is given if no `--pasword` option
55
57
 
56
58
  * ldap(s) password check / group member
57
59
  ```shell
58
- $ export PYWEBEXEC_LDAP_SERVER=ldap.forumsys.com
59
- $ export PYWEBEXEC_LDAP_USE_SSL=0
60
+ $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
60
61
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
61
62
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
62
- $ export PYWEBEXEC_LDAP_GROUPS=mathematicians,scientists
63
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
63
64
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
64
65
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
65
66
  $ pywebexec
@@ -85,12 +86,14 @@ $ pywebexec start
85
86
  $ pywebexec status
86
87
  $ pywebexec stop
87
88
  ```
88
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
89
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
89
90
 
90
91
  ## Launch command through API
91
92
 
92
93
  ```shell
93
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
94
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
95
+ $ curl http://myhost:8080/command_status/<command_id>
96
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
94
97
  ```
95
98
 
96
99
  ## API reference
@@ -100,7 +103,7 @@ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST
100
103
  |-----------|-----------------------------|--------------------|---------------------|
101
104
  | POST | /run_command | command: str<br>params: array[str] | command_id: uuid<br>message: str |
102
105
  | POST | /stop_command/command_id | | message: str |
103
- | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
106
+ | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
104
107
  | GET | /command_output/command_id | | output: str<br>status: str |
105
- | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
108
+ | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
106
109
  | GET | /executables | | array of str |
@@ -20,7 +20,7 @@ dynamic=["version"]
20
20
  readme = "README.md"
21
21
  license = {file = "LICENSE"}
22
22
  requires-python = ">= 3.6"
23
- keywords = ["http", "fileserver", "browser", "explorer"]
23
+ keywords = ["http", "server", "remote commands", "api", "website"]
24
24
  classifiers = [
25
25
  "Development Status :: 5 - Production/Stable",
26
26
  "Intended Audience :: System Administrators",
@@ -34,7 +34,6 @@ app.config['LDAP_GROUPS'] = os.environ.get('PYWEBEXEC_LDAP_GROUPS')
34
34
  app.config['LDAP_BASE_DN'] = os.environ.get('PYWEBEXEC_LDAP_BASE_DN')
35
35
  app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
36
36
  app.config['LDAP_BIND_PASSWORD'] = os.environ.get('PYWEBEXEC_LDAP_BIND_PASSWORD')
37
- app.config['LDAP_USE_SSL'] = int(os.environ.get('PYWEBEXEC_LDAP_USE_SSL', False))
38
37
 
39
38
  # Directory to store the command status and output
40
39
  COMMAND_STATUS_DIR = '.web_status'
@@ -343,7 +342,10 @@ def read_command_status(command_id):
343
342
  if not os.path.exists(status_file_path):
344
343
  return None
345
344
  with open(status_file_path, 'r') as f:
346
- status_data = json.load(f)
345
+ try:
346
+ status_data = json.load(f)
347
+ except json.JSONDecodeError:
348
+ return None
347
349
 
348
350
  # Cache the status if it is not "running"
349
351
  if status_data['status'] != 'running':
@@ -383,6 +385,8 @@ def run_command(command, params, command_id):
383
385
 
384
386
  @app.before_request
385
387
  def check_authentication():
388
+ if not app.config['USER'] and not app.config['LDAP_SERVER']:
389
+ return
386
390
  if 'username' not in session and request.endpoint not in ['login', 'static']:
387
391
  return auth.login_required(lambda: None)()
388
392
 
@@ -401,14 +405,14 @@ def verify_password(username, password):
401
405
  return False
402
406
 
403
407
  def verify_ldap(username, password):
404
- tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) if app.config['LDAP_USE_SSL'] else None
405
- server = Server(app.config['LDAP_SERVER'], use_ssl=app.config['LDAP_USE_SSL'], tls=tls_configuration, get_info=ALL)
408
+ tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) if app.config['LDAP_SERVER'].startswith("ldaps:") else None
409
+ server = Server(app.config['LDAP_SERVER'], tls=tls_configuration, get_info=ALL)
406
410
  user_filter = f"({app.config['LDAP_USER_ID']}={username})"
407
411
  try:
408
412
  # Bind with the bind DN and password
409
- conn = Connection(server, user=app.config['LDAP_BIND_DN'], password=app.config['LDAP_BIND_PASSWORD'], authentication=SIMPLE, auto_bind=True)
413
+ conn = Connection(server, user=app.config['LDAP_BIND_DN'], password=app.config['LDAP_BIND_PASSWORD'], authentication=SIMPLE, auto_bind=True, read_only=True)
410
414
  try:
411
- conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=user_filter)
415
+ conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=user_filter, search_scope=SUBTREE)
412
416
  if len(conn.entries) == 0:
413
417
  print(f"User {username} not found in LDAP.")
414
418
  return False
@@ -417,13 +421,13 @@ def verify_ldap(username, password):
417
421
  conn.unbind()
418
422
 
419
423
  # Bind with the user DN and password to verify credentials
420
- conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE, auto_bind=True)
424
+ conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE, auto_bind=True, read_only=True)
421
425
  try:
422
426
  if not app.config['LDAP_GROUPS'] and conn.result["result"] == 0:
423
427
  return True
424
- group_filter = "".join([f'(ou={group})' for group in app.config['LDAP_GROUPS'].split(",")])
428
+ group_filter = "".join([f'({group})' for group in app.config['LDAP_GROUPS'].split(",")])
425
429
  group_filter = f"(&{group_filter}(|(member={user_dn})(uniqueMember={user_dn})))"
426
- conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=group_filter)
430
+ conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=group_filter, search_scope=SUBTREE)
427
431
  result = len(conn.entries) > 0
428
432
  if not result:
429
433
  print(f"User {username} is not a member of groups {app.config['LDAP_GROUPS']}.")
@@ -519,7 +523,7 @@ def list_commands():
519
523
  params = shlex.join(status['params'])
520
524
  except AttributeError:
521
525
  params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
522
- command = status['command'] + ' ' + params
526
+ command = status.get('command', '-') + ' ' + params
523
527
  commands.append({
524
528
  'command_id': command_id,
525
529
  'status': status['status'],
@@ -540,6 +544,8 @@ def get_command_output(command_id):
540
544
  with open(output_file_path, 'r') as output_file:
541
545
  output = output_file.read()
542
546
  status_data = read_command_status(command_id) or {}
547
+ if request.headers.get('Accept') == 'text/plain':
548
+ return f"{output}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
543
549
  return jsonify({'output': output, 'status': status_data.get("status")})
544
550
  return jsonify({'error': 'Invalid command_id'}), 404
545
551
 
@@ -1,23 +1,46 @@
1
- body { font-family: Arial, sans-serif; }
2
- .table-container { height: 270px; overflow-y: auto; position: relative; }
3
- table { width: 100%; border-collapse: collapse; }
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ }
4
+ .table-container {
5
+ height: 270px;
6
+ overflow-y: auto;
7
+ position: relative;
8
+ border-radius: 10px;
9
+ border: 1px solid #aaa;
10
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.30);
11
+ }
12
+ table {
13
+ width: 100%;
14
+ border-collapse: collapse;
15
+ }
4
16
  th, td {
5
17
  padding: 8px;
6
18
  text-align: left;
7
19
  border-bottom: 1px solid #ddd;
8
20
  white-space: nowrap;
9
21
  }
10
- th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
22
+ th {
23
+ background-color: #444;
24
+ color: #eee;
25
+ position: sticky;
26
+ top: 0;
27
+ z-index: 1;
28
+ }
11
29
  .outcol {
12
30
  width: 100%;
13
31
  }
32
+ select { /* Safari bug */
33
+ font-size: 15px;
34
+ border: #aaa solid 1px;
35
+ border-radius: 5px;
36
+ }
14
37
  .output {
15
38
  white-space: pre-wrap;
16
39
  background: #f0f0f0;
17
40
  padding: 10px;
18
41
  border: 1px solid #ccc;
19
42
  font-family: monospace;
20
- border-radius: 15px;
43
+ border-radius: 10px;
21
44
  overflow-y: auto;
22
45
  }
23
46
  .copy-icon { cursor: pointer; }
@@ -28,7 +51,6 @@ button {
28
51
  -webkit-border-radius: none;
29
52
  appearance: none;
30
53
  border-radius: 15px;
31
- padding: 3px;
32
54
  padding-right: 13px;
33
55
  border: 1px #555 solid;
34
56
  height: 22px;
@@ -49,8 +71,13 @@ form {
49
71
  background-repeat: no-repeat;
50
72
  vertical-align: middle;
51
73
  }
74
+ .title-icon {
75
+ width: 30px;
76
+ height: 30px;
77
+ background-image: url("/static/images/favicon.svg")
78
+ }
52
79
  .status-running {
53
- background-image: url("/static/images/running.svg")
80
+ background-image: url("/static/images/running.gif")
54
81
  }
55
82
  .status-success {
56
83
  background-image: url("/static/images/success.svg")
@@ -62,10 +89,10 @@ form {
62
89
  background-image: url("/static/images/aborted.svg")
63
90
  }
64
91
  .copy_clip {
65
- padding-right: 25px;
92
+ padding-right: 20px;
66
93
  background-repeat: no-repeat;
67
94
  background-position: right top;
68
- background-size: 25px 16px;
95
+ background-size: 20px 12px;
69
96
  white-space: nowrap;
70
97
  }
71
98
  .copy_clip:hover {
@@ -87,7 +114,7 @@ input {
87
114
  height: 15px;
88
115
  font-size: 15px;
89
116
  outline: none;
90
- text-indent: 10px;
117
+ text-indent: 5px;
91
118
  background-color: white;
92
119
  }
93
120
  .currentcommand {
@@ -96,6 +123,7 @@ input {
96
123
  .resizer {
97
124
  width: 100%;
98
125
  height: 5px;
126
+ border-radius: 5px;
99
127
  background: #aaa;
100
128
  cursor: ns-resize;
101
129
  position: absolute;
@@ -105,7 +133,8 @@ input {
105
133
  .resizer-container {
106
134
  position: relative;
107
135
  height: 5px;
108
- margin-bottom: 10px;
136
+ margin: 5px;
137
+ /*margin-bottom: 10px;*/
109
138
  }
110
139
  tr.clickable-row {
111
140
  cursor: pointer;
@@ -33,7 +33,7 @@ async function fetchCommands() {
33
33
  commandRow.onclick = () => viewOutput(command.command_id);
34
34
  commandRow.innerHTML = `
35
35
  <td class="monospace">
36
- <span class="copy_clip" onclick="copyToClipboard('${command.command_id}', this)">${command.command_id.slice(0, 8)}</span>
36
+ ${navigator.clipboard == undefined ? `${command.command_id}` : `<span class="copy_clip" onclick="copyToClipboard('${command.command_id.slice(0, 8)}', this, event)">${command.command_id.slice(0, 8)}</span>`}
37
37
  </td>
38
38
  <td><span class="status-icon status-${command.status}"></span>${command.status}</td>
39
39
  <td>${formatTime(command.start_time)}</td>
@@ -41,8 +41,7 @@ async function fetchCommands() {
41
41
  <td>${command.exit_code}</td>
42
42
  <td>${command.command.replace(/^\.\//, '')}</td>
43
43
  <td>
44
- ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : ` <button onclick="relaunchCommand('${command.command_id}')">Run</button>
45
- `}
44
+ ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}')">Run</button>`}
46
45
  </td>
47
46
  <td class="monospace outcol">${command.last_output_line || ''}</td>
48
47
  `;
@@ -146,12 +145,14 @@ function formatDuration(startTime, endTime) {
146
145
  return `${hours}h ${minutes}m ${seconds}s`;
147
146
  }
148
147
 
149
- function copyToClipboard(text, element) {
148
+ function copyToClipboard(text, element, event) {
149
+ event.stopPropagation();
150
+ event.stopImmediatePropagation();
150
151
  navigator.clipboard.writeText(text).then(() => {
151
152
  element.classList.add('copy_clip_ok');
152
153
  setTimeout(() => {
153
154
  element.classList.remove('copy_clip_ok');
154
- }, 2000);
155
+ }, 1000);
155
156
  });
156
157
  }
157
158
 
@@ -7,7 +7,7 @@
7
7
  <link rel="stylesheet" href="/static/css/style.css">
8
8
  </head>
9
9
  <body>
10
- <h2>{{ title }}</h2>
10
+ <h2><span class="status-icon title-icon"></span>{{ title }}</h2>
11
11
  <form id="launchForm">
12
12
  <label for="commandName">Command</label>
13
13
  <select id="commandName" name="commandName"></select>
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.1.1'
16
- __version_tuple__ = version_tuple = (1, 1, 1)
15
+ __version__ = version = '1.1.11'
16
+ __version_tuple__ = version_tuple = (1, 1, 11)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.1.1
3
+ Version: 1.1.11
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -30,7 +30,7 @@ License: MIT License
30
30
  Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
31
  Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
32
  Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
- Keywords: http,fileserver,browser,explorer
33
+ Keywords: http,server,remote commands,api,website
34
34
  Classifier: Development Status :: 5 - Production/Stable
35
35
  Classifier: Intended Audience :: System Administrators
36
36
  Classifier: License :: OSI Approved :: MIT License
@@ -63,7 +63,7 @@ Requires-Dist: ldap3>=2.9.1
63
63
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
64
64
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
65
65
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
66
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
66
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
67
67
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
68
68
 
69
69
  # pywebexec
@@ -83,7 +83,9 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
86
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
87
+
88
+ all commands output / statuses are available in the executables directory in subdirectory `.web_status`
87
89
 
88
90
  ## features
89
91
 
@@ -97,13 +99,13 @@ $ pywebexec
97
99
  * Basic Auth
98
100
  * LDAP(S)
99
101
  * Can be started as a daemon (POSIX)
100
- * uses gunicorn to serve http/https
101
- * compatible Linux/MacOS
102
+ * Uses gunicorn to serve http/https
103
+ * Linux/MacOS compatible
102
104
 
103
105
  ## Customize server
104
106
  ```shell
105
- $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080
106
- $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080
107
+ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080 --title myscripts
108
+ $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080 -t myscripts
107
109
  ```
108
110
 
109
111
  ## Basic auth
@@ -117,11 +119,10 @@ Generated password is given if no `--pasword` option
117
119
 
118
120
  * ldap(s) password check / group member
119
121
  ```shell
120
- $ export PYWEBEXEC_LDAP_SERVER=ldap.forumsys.com
121
- $ export PYWEBEXEC_LDAP_USE_SSL=0
122
+ $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
122
123
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
123
124
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
124
- $ export PYWEBEXEC_LDAP_GROUPS=mathematicians,scientists
125
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
125
126
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
126
127
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
127
128
  $ pywebexec
@@ -147,12 +148,14 @@ $ pywebexec start
147
148
  $ pywebexec status
148
149
  $ pywebexec stop
149
150
  ```
150
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
152
 
152
153
  ## Launch command through API
153
154
 
154
155
  ```shell
155
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
156
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
157
+ $ curl http://myhost:8080/command_status/<command_id>
158
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
156
159
  ```
157
160
 
158
161
  ## API reference
@@ -162,7 +165,7 @@ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST
162
165
  |-----------|-----------------------------|--------------------|---------------------|
163
166
  | POST | /run_command | command: str<br>params: array[str] | command_id: uuid<br>message: str |
164
167
  | POST | /stop_command/command_id | | message: str |
165
- | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
168
+ | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
166
169
  | GET | /command_output/command_id | | output: str<br>status: str |
167
- | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
170
+ | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int<br>last_output_line: str |
168
171
  | GET | /executables | | array of str |
@@ -19,7 +19,7 @@ pywebexec/static/images/copy.svg
19
19
  pywebexec/static/images/copy_ok.svg
20
20
  pywebexec/static/images/failed.svg
21
21
  pywebexec/static/images/favicon.svg
22
- pywebexec/static/images/running.svg
22
+ pywebexec/static/images/running.gif
23
23
  pywebexec/static/images/success.svg
24
24
  pywebexec/static/js/script.js
25
25
  pywebexec/templates/__init__.py
@@ -1 +0,0 @@
1
- <svg viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M661.333333 170.666667l253.866667 34.133333-209.066667 209.066667zM362.666667 853.333333L108.8 819.2l209.066667-209.066667zM170.666667 362.666667L204.8 108.8l209.066667 209.066667z" fill="#4949d1"></path><path d="M198.4 452.266667l-89.6 17.066666c-2.133333 14.933333-2.133333 27.733333-2.133333 42.666667 0 98.133333 34.133333 192 98.133333 264.533333l64-55.466666C219.733333 663.466667 192 588.8 192 512c0-19.2 2.133333-40.533333 6.4-59.733333zM512 106.666667c-115.2 0-217.6 49.066667-292.266667 125.866666l59.733334 59.733334C339.2 230.4 420.266667 192 512 192c19.2 0 40.533333 2.133333 59.733333 6.4l14.933334-83.2C563.2 108.8 537.6 106.666667 512 106.666667zM825.6 571.733333l89.6-17.066666c2.133333-14.933333 2.133333-27.733333 2.133333-42.666667 0-93.866667-32-185.6-91.733333-258.133333l-66.133333 53.333333c46.933333 57.6 72.533333 130.133333 72.533333 202.666667 0 21.333333-2.133333 42.666667-6.4 61.866666zM744.533333 731.733333C684.8 793.6 603.733333 832 512 832c-19.2 0-40.533333-2.133333-59.733333-6.4l-14.933334 83.2c25.6 4.266667 51.2 6.4 74.666667 6.4 115.2 0 217.6-49.066667 292.266667-125.866667l-59.733334-57.6z" fill="#4949d1"></path><path d="M853.333333 661.333333l-34.133333 253.866667-209.066667-209.066667z" fill="#4949d1"></path></g></svg>
File without changes
File without changes
File without changes