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.
- {pywebexec-1.1.1/pywebexec.egg-info → pywebexec-1.1.11}/PKG-INFO +18 -15
- {pywebexec-1.1.1 → pywebexec-1.1.11}/README.md +16 -13
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pyproject.toml +1 -1
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/pywebexec.py +16 -10
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/css/style.css +40 -11
- pywebexec-1.1.11/pywebexec/static/images/running.gif +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/js/script.js +6 -5
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/templates/index.html +1 -1
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/version.py +2 -2
- {pywebexec-1.1.1 → pywebexec-1.1.11/pywebexec.egg-info}/PKG-INFO +18 -15
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/SOURCES.txt +1 -1
- pywebexec-1.1.1/pywebexec/static/images/running.svg +0 -1
- {pywebexec-1.1.1 → pywebexec-1.1.11}/.github/workflows/python-publish.yml +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/.gitignore +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/LICENSE +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/__init__.py +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/aborted.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/copy.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/copy_ok.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/failed.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/favicon.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/static/images/success.svg +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec/templates/__init__.py +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/dependency_links.txt +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/entry_points.txt +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/requires.txt +0 -0
- {pywebexec-1.1.1 → pywebexec-1.1.11}/pywebexec.egg-info/top_level.txt +0 -0
- {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.
|
|
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,
|
|
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
|
[](https://pypi.org/project/pywebexec/)
|
|
64
64
|

|
|
65
65
|
[](https://shields.io/)
|
|
66
|
-
[](https://pepy.tech/
|
|
66
|
+
[](https://pepy.tech/projects/pywebexec)
|
|
67
67
|
[](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
|
-

|
|
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
|
-
*
|
|
101
|
-
*
|
|
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
|
|
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", "
|
|
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
|
[](https://pypi.org/project/pywebexec/)
|
|
2
2
|

|
|
3
3
|
[](https://shields.io/)
|
|
4
|
-
[](https://pepy.tech/
|
|
4
|
+
[](https://pepy.tech/projects/pywebexec)
|
|
5
5
|
[](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
|
-

|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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['
|
|
405
|
-
server = Server(app.config['LDAP_SERVER'],
|
|
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'(
|
|
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
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
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:
|
|
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.
|
|
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:
|
|
92
|
+
padding-right: 20px;
|
|
66
93
|
background-repeat: no-repeat;
|
|
67
94
|
background-position: right top;
|
|
68
|
-
background-size:
|
|
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:
|
|
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
|
|
136
|
+
margin: 5px;
|
|
137
|
+
/*margin-bottom: 10px;*/
|
|
109
138
|
}
|
|
110
139
|
tr.clickable-row {
|
|
111
140
|
cursor: pointer;
|
|
Binary file
|
|
@@ -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
|
-
|
|
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>` :
|
|
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
|
-
},
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pywebexec
|
|
3
|
-
Version: 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,
|
|
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
|
[](https://pypi.org/project/pywebexec/)
|
|
64
64
|

|
|
65
65
|
[](https://shields.io/)
|
|
66
|
-
[](https://pepy.tech/
|
|
66
|
+
[](https://pepy.tech/projects/pywebexec)
|
|
67
67
|
[](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
|
-

|
|
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
|
-
*
|
|
101
|
-
*
|
|
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
|
|
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", "
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|