pywebexec 1.9.3__py3-none-any.whl → 1.9.4__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 +112 -11
- pywebexec/static/css/style.css +12 -0
- pywebexec/static/js/{commands.js → executables.js} +39 -5
- pywebexec/static/js/script.js +3 -1
- pywebexec/swagger.yaml +127 -1
- pywebexec/templates/index.html +2 -1
- pywebexec/version.py +2 -2
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/METADATA +20 -8
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/RECORD +13 -13
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/LICENSE +0 -0
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/WHEEL +0 -0
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.9.3.dist-info → pywebexec-1.9.4.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -29,7 +29,8 @@ from pathlib import Path
|
|
29
29
|
import pyte
|
30
30
|
from . import host_ip
|
31
31
|
from flask_swagger_ui import get_swaggerui_blueprint # new import
|
32
|
-
import yaml
|
32
|
+
import yaml
|
33
|
+
import html
|
33
34
|
|
34
35
|
if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
35
36
|
from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
|
@@ -167,12 +168,22 @@ def strip_ansi_control_chars(text):
|
|
167
168
|
def decode_line(line: bytes) -> str:
|
168
169
|
"""try decode line exception on binary"""
|
169
170
|
try:
|
170
|
-
return
|
171
|
+
return get_visible_line(line.decode())
|
172
|
+
except UnicodeDecodeError:
|
173
|
+
return ""
|
174
|
+
|
175
|
+
def get_visible_output(output, cols, rows):
|
176
|
+
"""pyte vt100 render to get last line"""
|
177
|
+
try:
|
178
|
+
screen = pyte.Screen(cols, rows)
|
179
|
+
stream = pyte.Stream(screen)
|
180
|
+
stream.feed(output)
|
181
|
+
return "\n".join(screen.display).strip()
|
171
182
|
except UnicodeDecodeError:
|
172
183
|
return ""
|
173
184
|
|
174
185
|
|
175
|
-
def
|
186
|
+
def get_visible_line(line, cols, rows):
|
176
187
|
"""pyte vt100 render to get last line"""
|
177
188
|
try:
|
178
189
|
screen = pyte.Screen(cols, rows)
|
@@ -197,7 +208,7 @@ def get_last_line(file_path, cols=None, rows=None, maxsize=2048):
|
|
197
208
|
fd.seek(-maxsize, os.SEEK_END)
|
198
209
|
except OSError:
|
199
210
|
fd.seek(0)
|
200
|
-
return
|
211
|
+
return get_visible_line(fd.read(), cols, rows)
|
201
212
|
|
202
213
|
|
203
214
|
def start_gunicorn(daemonized=False, baselog=None):
|
@@ -619,6 +630,19 @@ def log_error(fromip, user, message):
|
|
619
630
|
def log_request(message):
|
620
631
|
log_info(request.remote_addr, session.get('username', '-'), message)
|
621
632
|
|
633
|
+
def get_executables():
|
634
|
+
executables_list = []
|
635
|
+
for f in os.listdir('.'):
|
636
|
+
if os.path.isfile(f) and os.access(f, os.X_OK):
|
637
|
+
help_file = f"{f}.help"
|
638
|
+
help_text = ""
|
639
|
+
if os.path.exists(help_file) and os.path.isfile(help_file):
|
640
|
+
with open(help_file, 'r') as hf:
|
641
|
+
help_text = hf.read()
|
642
|
+
executables_list.append({"command": f, "help": help_text})
|
643
|
+
return executables_list
|
644
|
+
|
645
|
+
|
622
646
|
@app.route('/commands/<command_id>/stop', methods=['PATCH'])
|
623
647
|
def stop_command(command_id):
|
624
648
|
log_request(f"stop_command {command_id}")
|
@@ -744,6 +768,37 @@ def run_command_endpoint():
|
|
744
768
|
|
745
769
|
return jsonify({'message': 'Command is running', 'command_id': command_id})
|
746
770
|
|
771
|
+
@app.route('/commands/<cmd>', methods=['POST'])
|
772
|
+
def run_dynamic_command(cmd):
|
773
|
+
# Validate that 'cmd' is an executable in the current directory
|
774
|
+
cmd_path = os.path.join(".", os.path.basename(cmd))
|
775
|
+
if not os.path.isfile(cmd_path) or not os.access(cmd_path, os.X_OK):
|
776
|
+
return jsonify({'error': 'Command not found or not executable'}), 400
|
777
|
+
try:
|
778
|
+
data = request.json
|
779
|
+
except Exception as e:
|
780
|
+
data = {}
|
781
|
+
params = data.get('params', [])
|
782
|
+
rows = data.get('rows', tty_rows) or tty_rows
|
783
|
+
cols = data.get('cols', tty_cols) or tty_cols
|
784
|
+
try:
|
785
|
+
params = shlex.split(' '.join(params)) if isinstance(params, list) else []
|
786
|
+
except Exception as e:
|
787
|
+
return jsonify({'error': str(e)}), 400
|
788
|
+
user = session.get('username', '-')
|
789
|
+
command_id = str(uuid.uuid4())
|
790
|
+
update_command_status(command_id, {
|
791
|
+
'status': 'running',
|
792
|
+
'command': cmd,
|
793
|
+
'params': params,
|
794
|
+
'user': user,
|
795
|
+
'from': request.remote_addr,
|
796
|
+
})
|
797
|
+
Path(get_output_file_path(command_id)).touch()
|
798
|
+
thread = threading.Thread(target=run_command, args=(request.remote_addr, user, cmd_path, params, command_id, rows, cols))
|
799
|
+
thread.start()
|
800
|
+
return jsonify({'message': 'Command is running', 'command_id': command_id})
|
801
|
+
|
747
802
|
@app.route('/commands/<command_id>', methods=['GET'])
|
748
803
|
def get_command_status(command_id):
|
749
804
|
status = read_command_status(command_id)
|
@@ -757,15 +812,15 @@ def index():
|
|
757
812
|
|
758
813
|
@app.route('/commands', methods=['GET'])
|
759
814
|
def list_commands():
|
760
|
-
# Sort commands by start_time in descending order
|
761
815
|
commands = read_commands()
|
762
816
|
commands.sort(key=lambda x: x['start_time'], reverse=True)
|
763
|
-
return jsonify(commands)
|
817
|
+
return jsonify({"commands": commands})
|
764
818
|
|
765
819
|
@app.route('/commands/<command_id>/output', methods=['GET'])
|
766
820
|
def get_command_output(command_id):
|
767
821
|
offset = int(request.args.get('offset', 0))
|
768
822
|
maxsize = int(request.args.get('maxsize', 10485760))
|
823
|
+
maxlines = int(request.args.get('maxlines', 5000))
|
769
824
|
output_file_path = get_output_file_path(command_id)
|
770
825
|
if os.path.exists(output_file_path):
|
771
826
|
size = os.path.getsize(output_file_path)
|
@@ -790,7 +845,7 @@ def get_command_output(command_id):
|
|
790
845
|
}
|
791
846
|
}
|
792
847
|
if request.headers.get('Accept') == 'text/plain':
|
793
|
-
return f"{output}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
|
848
|
+
return f"{get_visible_output(output, status_data.get('cols'), maxlines)}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
|
794
849
|
return jsonify(response)
|
795
850
|
return jsonify({'error': 'Invalid command_id'}), 404
|
796
851
|
|
@@ -820,9 +875,8 @@ def get_command_output_raw(command_id):
|
|
820
875
|
|
821
876
|
@app.route('/executables', methods=['GET'])
|
822
877
|
def list_executables():
|
823
|
-
|
824
|
-
|
825
|
-
return jsonify(executables)
|
878
|
+
executables_list = get_executables()
|
879
|
+
return jsonify({"executables": executables_list})
|
826
880
|
|
827
881
|
@app.route('/commands/<command_id>/popup')
|
828
882
|
def popup(command_id):
|
@@ -854,7 +908,54 @@ def swagger_yaml():
|
|
854
908
|
with open(swagger_path, 'r') as f:
|
855
909
|
swagger_spec_str = f.read()
|
856
910
|
swagger_spec = yaml.safe_load(swagger_spec_str)
|
857
|
-
#
|
911
|
+
# Update existing POST /commands enum if present
|
912
|
+
executables = get_executables()
|
913
|
+
post_cmd = swagger_spec.get('paths', {}).get('/commands', {}).get('post')
|
914
|
+
if post_cmd:
|
915
|
+
params_list = post_cmd.get('parameters', [])
|
916
|
+
for param in params_list:
|
917
|
+
if param.get('in') == 'body' and 'schema' in param:
|
918
|
+
props = param['schema'].get('properties', {})
|
919
|
+
if 'command' in props:
|
920
|
+
props['command']['enum'] = [e['command'] for e in executables]
|
921
|
+
# Add dynamic paths for each
|
922
|
+
# Add dynamic paths for each executable:
|
923
|
+
for exe in executables:
|
924
|
+
dynamic_path = "/commands/" + exe["command"]
|
925
|
+
swagger_spec.setdefault("paths", {})[dynamic_path] = {
|
926
|
+
"post": {
|
927
|
+
"summary": f"Run command {exe["command"]}",
|
928
|
+
"description": html.escape(exe["help"]),
|
929
|
+
"consumes": ["application/json"],
|
930
|
+
"produces": ["application/json"],
|
931
|
+
"parameters": [
|
932
|
+
{
|
933
|
+
"in": "body",
|
934
|
+
"name": "commandRequest",
|
935
|
+
"schema": {
|
936
|
+
"type": "object",
|
937
|
+
"properties": {
|
938
|
+
"params": {"type": "array", "items": {"type": "string"}, "default": []},
|
939
|
+
"rows": {"type": "integer", "default": tty_rows},
|
940
|
+
"cols": {"type": "integer", "default": tty_cols},
|
941
|
+
}
|
942
|
+
}
|
943
|
+
}
|
944
|
+
],
|
945
|
+
"responses": {
|
946
|
+
"200": {
|
947
|
+
"description": "Command started",
|
948
|
+
"schema": {
|
949
|
+
"type": "object",
|
950
|
+
"properties": {
|
951
|
+
"message": {"type": "string"},
|
952
|
+
"command_id": {"type": "string"}
|
953
|
+
}
|
954
|
+
}
|
955
|
+
}
|
956
|
+
}
|
957
|
+
}
|
958
|
+
}
|
858
959
|
swagger_spec['info']['title'] = app.config.get('TITLE', 'PyWebExec API')
|
859
960
|
swagger_spec_str = yaml.dump(swagger_spec)
|
860
961
|
return Response(swagger_spec_str, mimetype='application/yaml')
|
pywebexec/static/css/style.css
CHANGED
@@ -99,6 +99,18 @@ form {
|
|
99
99
|
vertical-align: bottom;
|
100
100
|
padding-left: 35px;
|
101
101
|
}
|
102
|
+
#paramsHelp {
|
103
|
+
position: absolute;
|
104
|
+
background-color: #e0e0ff;
|
105
|
+
border: 1px solid #ccc;
|
106
|
+
padding: 4px 8px;
|
107
|
+
font-size: 0.9em;
|
108
|
+
display: none;
|
109
|
+
z-index: 1000;
|
110
|
+
white-space: pre;
|
111
|
+
border-radius: 5px;
|
112
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
113
|
+
}
|
102
114
|
#thStatus {
|
103
115
|
cursor: pointer;
|
104
116
|
}
|
@@ -5,6 +5,8 @@ let commandListDiv = document.getElementById('commandList');
|
|
5
5
|
let showCommandListButton = document.getElementById('showCommandListButton');
|
6
6
|
let isHandlingKeydown = false;
|
7
7
|
let firstVisibleItem = 0;
|
8
|
+
let gExecutables = {};
|
9
|
+
let helpDiv = document.getElementById('paramsHelp');
|
8
10
|
|
9
11
|
function unfilterCommands() {
|
10
12
|
const items = commandListDiv.children;
|
@@ -43,6 +45,13 @@ function setCommandListPosition() {
|
|
43
45
|
commandListDiv.style.top = `${rect.bottom}px`;
|
44
46
|
}
|
45
47
|
|
48
|
+
// Update helpDiv position relative to paramsInput
|
49
|
+
function setHelpDivPosition() {
|
50
|
+
const rect = paramsInput.getBoundingClientRect();
|
51
|
+
helpDiv.style.left = `${rect.left}px`;
|
52
|
+
helpDiv.style.top = `${rect.bottom + 2}px`;
|
53
|
+
}
|
54
|
+
|
46
55
|
function adjustInputWidth(input) {
|
47
56
|
input.style.width = 'auto';
|
48
57
|
input.style.width = `${input.scrollWidth}px`;
|
@@ -218,15 +227,21 @@ async function fetchExecutables() {
|
|
218
227
|
try {
|
219
228
|
const response = await fetch(`/executables`);
|
220
229
|
if (!response.ok) {
|
221
|
-
throw new Error('Failed to fetch
|
230
|
+
throw new Error('Failed to fetch executables');
|
222
231
|
}
|
223
|
-
const
|
232
|
+
const data = await response.json();
|
233
|
+
// Build mapping from executable name to its object
|
234
|
+
gExecutables = {};
|
224
235
|
commandListDiv.innerHTML = '';
|
225
|
-
executables.forEach(
|
236
|
+
data.executables.forEach(exeObj => {
|
237
|
+
gExecutables[exeObj.command] = exeObj;
|
226
238
|
const div = document.createElement('div');
|
227
239
|
div.className = 'command-item';
|
228
|
-
div.textContent =
|
229
|
-
div.tabIndex = 0;
|
240
|
+
div.textContent = exeObj.command;
|
241
|
+
div.tabIndex = 0;
|
242
|
+
if (exeObj.help) {
|
243
|
+
div.title = exeObj.help;
|
244
|
+
}
|
230
245
|
commandListDiv.appendChild(div);
|
231
246
|
});
|
232
247
|
} catch (error) {
|
@@ -240,3 +255,22 @@ async function fetchExecutables() {
|
|
240
255
|
document.getElementById('launchForm').style.display = 'none';
|
241
256
|
|
242
257
|
}
|
258
|
+
|
259
|
+
paramsInput.addEventListener('focus', () => {
|
260
|
+
const currentCmd = commandInput.value;
|
261
|
+
if (gExecutables[currentCmd] && gExecutables[currentCmd].help) {
|
262
|
+
//helpDiv.innerHTML = gExecutables[currentCmd].help.replace(/\n/g, '<br>');
|
263
|
+
helpDiv.innerText = gExecutables[currentCmd].help;
|
264
|
+
setHelpDivPosition();
|
265
|
+
helpDiv.style.display = 'block';
|
266
|
+
} else {
|
267
|
+
helpDiv.style.display = 'none';
|
268
|
+
}
|
269
|
+
});
|
270
|
+
|
271
|
+
paramsInput.addEventListener('blur', () => {
|
272
|
+
helpDiv.style.display = 'none';
|
273
|
+
});
|
274
|
+
|
275
|
+
window.addEventListener('resize', setHelpDivPosition);
|
276
|
+
window.addEventListener('scroll', setHelpDivPosition);
|
pywebexec/static/js/script.js
CHANGED
@@ -143,7 +143,9 @@ async function fetchCommands(hide=false) {
|
|
143
143
|
document.getElementById('dimmer').style.display = 'block';
|
144
144
|
return;
|
145
145
|
}
|
146
|
-
|
146
|
+
// Adapt to the new result structure:
|
147
|
+
const data = await response.json();
|
148
|
+
const commands = data.commands;
|
147
149
|
commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
148
150
|
const commandsTbody = document.getElementById('commands');
|
149
151
|
commandsTbody.innerHTML = '';
|
pywebexec/swagger.yaml
CHANGED
@@ -9,6 +9,32 @@ paths:
|
|
9
9
|
responses:
|
10
10
|
"200":
|
11
11
|
description: "List of all commands status"
|
12
|
+
schema:
|
13
|
+
type: object
|
14
|
+
properties:
|
15
|
+
commands:
|
16
|
+
type: array
|
17
|
+
items:
|
18
|
+
type: object
|
19
|
+
properties:
|
20
|
+
command_id:
|
21
|
+
type: string
|
22
|
+
command:
|
23
|
+
type: string
|
24
|
+
status:
|
25
|
+
type: string
|
26
|
+
start_time:
|
27
|
+
type: string
|
28
|
+
format: date-time
|
29
|
+
end_time:
|
30
|
+
type: string
|
31
|
+
format: date-time
|
32
|
+
exit_code:
|
33
|
+
type: integer
|
34
|
+
last_output_line:
|
35
|
+
type: string
|
36
|
+
user:
|
37
|
+
type: string
|
12
38
|
post:
|
13
39
|
summary: "Run a command"
|
14
40
|
consumes:
|
@@ -23,17 +49,31 @@ paths:
|
|
23
49
|
properties:
|
24
50
|
command:
|
25
51
|
type: string
|
52
|
+
# Enum will be added dynamically by the APIs
|
53
|
+
default: commandName
|
26
54
|
params:
|
27
55
|
type: array
|
28
56
|
items:
|
29
57
|
type: string
|
58
|
+
default: []
|
30
59
|
rows:
|
31
60
|
type: integer
|
61
|
+
default: 24
|
32
62
|
cols:
|
33
63
|
type: integer
|
64
|
+
default: 125
|
65
|
+
required:
|
66
|
+
- command
|
34
67
|
responses:
|
35
68
|
"200":
|
36
69
|
description: "Command started"
|
70
|
+
schema:
|
71
|
+
type: object
|
72
|
+
properties:
|
73
|
+
command_id:
|
74
|
+
type: string
|
75
|
+
message:
|
76
|
+
type: string
|
37
77
|
/commands/{command_id}:
|
38
78
|
get:
|
39
79
|
summary: "Get command status"
|
@@ -45,6 +85,40 @@ paths:
|
|
45
85
|
responses:
|
46
86
|
"200":
|
47
87
|
description: "Command status returned"
|
88
|
+
schema:
|
89
|
+
type: object
|
90
|
+
properties:
|
91
|
+
command_id:
|
92
|
+
type: string
|
93
|
+
command:
|
94
|
+
type: string
|
95
|
+
params:
|
96
|
+
type: array
|
97
|
+
items:
|
98
|
+
type: string
|
99
|
+
status:
|
100
|
+
type: string
|
101
|
+
start_time:
|
102
|
+
type: string
|
103
|
+
format: date-time
|
104
|
+
end_time:
|
105
|
+
type: string
|
106
|
+
format: date-time
|
107
|
+
exit_code:
|
108
|
+
type: integer
|
109
|
+
last_output_line:
|
110
|
+
type: string
|
111
|
+
cols:
|
112
|
+
type: integer
|
113
|
+
rows:
|
114
|
+
type: integer
|
115
|
+
user:
|
116
|
+
type: string
|
117
|
+
from:
|
118
|
+
type: string
|
119
|
+
pid:
|
120
|
+
type: integer
|
121
|
+
|
48
122
|
/commands/{command_id}/output:
|
49
123
|
get:
|
50
124
|
summary: "Get command output"
|
@@ -61,9 +135,35 @@ paths:
|
|
61
135
|
name: maxsize
|
62
136
|
type: integer
|
63
137
|
default: 10485760
|
138
|
+
- in: query
|
139
|
+
name: maxlines
|
140
|
+
type: integer
|
141
|
+
default: 5000
|
142
|
+
consumes:
|
143
|
+
- application/json
|
144
|
+
produces:
|
145
|
+
- application/json
|
146
|
+
- text/plain
|
64
147
|
responses:
|
65
148
|
"200":
|
66
149
|
description: "Command output returned"
|
150
|
+
schema:
|
151
|
+
type: object
|
152
|
+
properties:
|
153
|
+
output:
|
154
|
+
type: string
|
155
|
+
status:
|
156
|
+
type: string
|
157
|
+
rows:
|
158
|
+
type: integer
|
159
|
+
cols:
|
160
|
+
type: integer
|
161
|
+
links:
|
162
|
+
type: object
|
163
|
+
properties:
|
164
|
+
next:
|
165
|
+
type: string
|
166
|
+
format: uri
|
67
167
|
/commands/{command_id}/stop:
|
68
168
|
patch:
|
69
169
|
summary: "Stop a running command"
|
@@ -75,9 +175,35 @@ paths:
|
|
75
175
|
responses:
|
76
176
|
"200":
|
77
177
|
description: "Command stopped successfully"
|
178
|
+
schema:
|
179
|
+
type: object
|
180
|
+
properties:
|
181
|
+
message:
|
182
|
+
type: string
|
78
183
|
/executables:
|
79
184
|
get:
|
80
185
|
summary: "List available executable commands"
|
186
|
+
produces:
|
187
|
+
- application/json
|
81
188
|
responses:
|
82
189
|
"200":
|
83
|
-
description: "List of executables returned as an array of executable
|
190
|
+
description: "List of executables returned as an array of executable objects"
|
191
|
+
schema:
|
192
|
+
type: object
|
193
|
+
properties:
|
194
|
+
executables:
|
195
|
+
type: array
|
196
|
+
items:
|
197
|
+
type: object
|
198
|
+
properties:
|
199
|
+
command:
|
200
|
+
type: string
|
201
|
+
help:
|
202
|
+
type: string
|
203
|
+
examples:
|
204
|
+
application/json:
|
205
|
+
executables:
|
206
|
+
- command: ls
|
207
|
+
help: "List directory contents\nparams:\n <ls_params>"
|
208
|
+
- command: cat
|
209
|
+
help: "Concatenate and display files\nparams:\n <cat_params>"
|
pywebexec/templates/index.html
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
</div>
|
22
22
|
<div id="commandList" class="command-list"></div>
|
23
23
|
<input type="text" id="params" name="params" oninput="this.style.width = ((this.value.length + 1) * 8) + 'px';">
|
24
|
+
<pre id="paramsHelp"></pre>
|
24
25
|
</div>
|
25
26
|
<button type="submit">Run</button>
|
26
27
|
</form>
|
@@ -60,6 +61,6 @@
|
|
60
61
|
<script src="/static/js/xterm/addon-unicode-graphemes.js"></script>
|
61
62
|
<script src="/static/js/xterm/addon-fit.js"></script>
|
62
63
|
<script type="text/javascript" src="/static/js/script.js"></script>
|
63
|
-
<script type="text/javascript" src="/static/js/
|
64
|
+
<script type="text/javascript" src="/static/js/executables.js"></script>
|
64
65
|
</body>
|
65
66
|
</html>
|
pywebexec/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: pywebexec
|
3
|
-
Version: 1.9.
|
3
|
+
Version: 1.9.4
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
6
6
|
Author: Franck Jouvanceau
|
@@ -63,13 +63,13 @@ Requires-Dist: ldap3>=2.9.1
|
|
63
63
|
Requires-Dist: pyte>=0.8.1
|
64
64
|
|
65
65
|
[](https://pypi.org/project/pywebexec/)
|
66
|
-

|
67
67
|
[](https://shields.io/)
|
68
68
|
[](https://pepy.tech/projects/pywebexec)
|
69
69
|
[](https://shields.io/)
|
70
70
|
|
71
71
|
# pywebexec
|
72
|
-
Simple Python HTTP(S) API/Web Command Launcher and Terminal sharing
|
72
|
+
Simple Python HTTP(S) API/Web Server Command Launcher and Terminal sharing
|
73
73
|
|
74
74
|
## Install
|
75
75
|
```
|
@@ -79,6 +79,8 @@ $ pip install pywebexec
|
|
79
79
|
## Quick start
|
80
80
|
|
81
81
|
* share terminal
|
82
|
+
* start http server and spawn a new terminal shared on 0.0.0.0 port 8080 (defaults)
|
83
|
+
* exiting terminal stops server/share
|
82
84
|
```shell
|
83
85
|
$ pywebexec shareterm
|
84
86
|
```
|
@@ -100,6 +102,7 @@ all commands output / statuses are available in the executables directory in sub
|
|
100
102
|
## features
|
101
103
|
|
102
104
|
* Serve executables in a directory
|
105
|
+
* full API driven with dynamic swagger UI
|
103
106
|
* Launch commands with params from web browser or API call
|
104
107
|
* multiple share terminal output
|
105
108
|
* Follow live output
|
@@ -190,20 +193,29 @@ $ pywebexec stop
|
|
190
193
|
## Launch command through API
|
191
194
|
|
192
195
|
```shell
|
193
|
-
$ curl http://myhost:8080/
|
194
|
-
$ curl http://myhost:8080/
|
195
|
-
$ curl http://myhost:8080/
|
196
|
+
$ curl http://myhost:8080/commands/myscript -H 'Content-Type: application/json' -X POST -d '{"params":["param1", ...]}'
|
197
|
+
$ curl http://myhost:8080/commands/<command_id>
|
198
|
+
$ curl http://myhost:8080/commands/<command_id>/output -H "Accept: text/plain"
|
196
199
|
```
|
197
200
|
|
201
|
+
## Add help to commands
|
202
|
+
|
203
|
+
For each exposed command, you can add a help message by creating a file named `<command>.help` in the same directory as the command.
|
204
|
+
The help message is displayed:
|
205
|
+
* in the web interface as tooltip when focused on param input field,
|
206
|
+
* in the response when calling the API `/executables`
|
207
|
+
* in the swagger-ui in the `/commands/<command>` route.
|
208
|
+
|
198
209
|
## API reference
|
199
210
|
|
200
211
|
|
201
212
|
| method | route | params/payload | returns
|
202
213
|
|-----------|-----------------------------|--------------------|---------------------|
|
203
|
-
| GET | /executables | |
|
204
|
-
| GET | /commands | |
|
214
|
+
| GET | /executables | | executables: [<br> {command: str,help: str},<br>] |
|
215
|
+
| GET | /commands | | commands: [<br> {<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<br> },<br>] |
|
205
216
|
| GET | /commands/{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 |
|
206
217
|
| GET | /commands/{id}/output | offset: int | output: str<br>status: str<br>links: { next: str } |
|
207
218
|
| GET | /commands/{id}/output_raw | offset: int | output: stream raw output until end of command<br>curl -Ns http://srv/commands/{id}/output_raw|
|
208
219
|
| POST | /commands | command: str<br>params: array[str]<br>rows: int<br>cols: int | command_id: uuid<br>message: str |
|
220
|
+
| POST | /commands/{cmd} | params: array[str]<br>rows: int<br>cols: int | command_id: uuid<br>message: str |
|
209
221
|
| PATCH | /commands/{id}/stop | | message: str |
|
@@ -1,9 +1,9 @@
|
|
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=
|
4
|
-
pywebexec/swagger.yaml,sha256
|
5
|
-
pywebexec/version.py,sha256=
|
6
|
-
pywebexec/static/css/style.css,sha256=
|
3
|
+
pywebexec/pywebexec.py,sha256=ghCPH_SXooljVK3le_SOgWoT-cz5DMFmJZMNwWaRwDQ,37918
|
4
|
+
pywebexec/swagger.yaml,sha256=w5Jl1S9yKhXZ5oaUunX8DJBToUGb9vsxvJE-K4KMNNA,5504
|
5
|
+
pywebexec/version.py,sha256=x_dqnWSuZ5m1DFc2qnANNI5KdLpU51P41kIYXxibols,511
|
6
|
+
pywebexec/static/css/style.css,sha256=p06KbEMfowCC_WoWaJf5PwxEt2I0QR2aWX8oP1Y06Mo,8525
|
7
7
|
pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
|
8
8
|
pywebexec/static/fonts/CommitMonoNerdFontMono-Regular.ttf,sha256=v6nZdSx5cs_TIic8Fujrjzg9u9glWjorDIr7RlwNceM,2370228
|
9
9
|
pywebexec/static/fonts/LICENSE,sha256=gsBdbFPfoMkCWYXBnjcYEAILdO0sYdUdNw8qirJQbVI,4395
|
@@ -23,9 +23,9 @@ pywebexec/static/images/popup.svg,sha256=0Bl9A_v5cBsMPn6FnOlVWlAQKgd2zqiWQbhjcL9
|
|
23
23
|
pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65EiuwtvK4A,607
|
24
24
|
pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
|
25
25
|
pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
|
26
|
-
pywebexec/static/js/
|
26
|
+
pywebexec/static/js/executables.js,sha256=jwb5TcbWmEyhbfp5Xc3re_zr3hvOV-2u6zPzObQnn3g,9542
|
27
27
|
pywebexec/static/js/popup.js,sha256=0fr3pp4j9D2fXEVnHyQrx2bPWFHfgbb336dbewgH1d8,9023
|
28
|
-
pywebexec/static/js/script.js,sha256=
|
28
|
+
pywebexec/static/js/script.js,sha256=EpDwM1CyvHgsfkMSAkYmS9nU0sKOEXpQ6L3xhDIabEY,17933
|
29
29
|
pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
|
30
30
|
pywebexec/static/js/xterm/addon-canvas.js,sha256=ez6QTVvsmLVNJmdJlM-ZQ5bErwlxAQ_9DUmDIptl2TM,94607
|
31
31
|
pywebexec/static/js/xterm/addon-canvas.js.map,sha256=ECBA4B-BqUpdFeRzlsEWLSQnudnhLP-yPQJ8_hKquMo,379537
|
@@ -38,11 +38,11 @@ pywebexec/static/js/xterm/addon-unicode11.js.map,sha256=paDj5KKtTIUGedQn2x7CaUTD
|
|
38
38
|
pywebexec/static/js/xterm/xterm.js,sha256=H5kaw7Syg-v5bmCuI6AKUnZd06Lkb6b92p8aqwMvdJU,289441
|
39
39
|
pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCfN0U_OI6Zw,1118392
|
40
40
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
-
pywebexec/templates/index.html,sha256=
|
41
|
+
pywebexec/templates/index.html,sha256=hx27of3TqTYWIdRQ0Jl1Z9WPsRG7I6rmZTf4zNC_9Xw,3149
|
42
42
|
pywebexec/templates/popup.html,sha256=3kpMccKD_OLLhJ4Y9KRw6Ny8wQWjVaRrUfV9y5-bDiQ,1580
|
43
|
-
pywebexec-1.9.
|
44
|
-
pywebexec-1.9.
|
45
|
-
pywebexec-1.9.
|
46
|
-
pywebexec-1.9.
|
47
|
-
pywebexec-1.9.
|
48
|
-
pywebexec-1.9.
|
43
|
+
pywebexec-1.9.4.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
44
|
+
pywebexec-1.9.4.dist-info/METADATA,sha256=sZuqjtYkJxHczyq4KZETEo_vDcN8ALHEVq3AG17coR0,9074
|
45
|
+
pywebexec-1.9.4.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
46
|
+
pywebexec-1.9.4.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
47
|
+
pywebexec-1.9.4.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
48
|
+
pywebexec-1.9.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|