pywebexec 1.9.2__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} +40 -6
- pywebexec/static/js/popup.js +2 -7
- pywebexec/static/js/script.js +11 -15
- pywebexec/swagger.yaml +127 -1
- pywebexec/templates/index.html +2 -1
- pywebexec/version.py +2 -2
- {pywebexec-1.9.2.dist-info → pywebexec-1.9.4.dist-info}/METADATA +20 -8
- {pywebexec-1.9.2.dist-info → pywebexec-1.9.4.dist-info}/RECORD +14 -14
- {pywebexec-1.9.2.dist-info → pywebexec-1.9.4.dist-info}/LICENSE +0 -0
- {pywebexec-1.9.2.dist-info → pywebexec-1.9.4.dist-info}/WHEEL +0 -0
- {pywebexec-1.9.2.dist-info → pywebexec-1.9.4.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.9.2.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`;
|
@@ -216,17 +225,23 @@ window.addEventListener('load', () => {
|
|
216
225
|
|
217
226
|
async function fetchExecutables() {
|
218
227
|
try {
|
219
|
-
const response = await fetch(`/executables
|
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/popup.js
CHANGED
@@ -98,11 +98,6 @@ function autoFit(scroll=true) {
|
|
98
98
|
if (scroll) terminal.scrollToBottom();
|
99
99
|
}
|
100
100
|
|
101
|
-
function getTokenParam() {
|
102
|
-
const urlParams = new URLSearchParams(window.location.search);
|
103
|
-
return urlParams.get('token') ? `?token=${urlParams.get('token')}` : '';
|
104
|
-
}
|
105
|
-
const urlToken = getTokenParam();
|
106
101
|
|
107
102
|
|
108
103
|
function setCommandStatus(status) {
|
@@ -161,14 +156,14 @@ async function viewOutput(command_id) {
|
|
161
156
|
slider.value = 1000;
|
162
157
|
adjustOutputHeight();
|
163
158
|
currentCommandId = command_id;
|
164
|
-
nextOutputLink = `/commands/${command_id}/output
|
159
|
+
nextOutputLink = `/commands/${command_id}/output`;
|
165
160
|
clearInterval(outputInterval);
|
166
161
|
terminal.clear();
|
167
162
|
terminal.reset();
|
168
163
|
fullOutput = '';
|
169
164
|
try {
|
170
165
|
// Updated endpoint below:
|
171
|
-
const response = await fetch(`/commands/${command_id}
|
166
|
+
const response = await fetch(`/commands/${command_id}`);
|
172
167
|
if (!response.ok) {
|
173
168
|
return;
|
174
169
|
}
|
pywebexec/static/js/script.js
CHANGED
@@ -108,12 +108,6 @@ function autoFit(scroll=true) {
|
|
108
108
|
if (scroll) terminal.scrollToBottom();
|
109
109
|
}
|
110
110
|
|
111
|
-
function getTokenParam() {
|
112
|
-
const urlParams = new URLSearchParams(window.location.search);
|
113
|
-
return urlParams.get('token') ? `?token=${urlParams.get('token')}` : '';
|
114
|
-
}
|
115
|
-
const urlToken = getTokenParam();
|
116
|
-
|
117
111
|
|
118
112
|
document.getElementById('launchForm').addEventListener('submit', async (event) => {
|
119
113
|
event.preventDefault();
|
@@ -122,7 +116,7 @@ document.getElementById('launchForm').addEventListener('submit', async (event) =
|
|
122
116
|
fitAddon.fit();
|
123
117
|
terminal.clear();
|
124
118
|
try {
|
125
|
-
const response = await fetch(`/commands
|
119
|
+
const response = await fetch(`/commands`, {
|
126
120
|
method: 'POST',
|
127
121
|
headers: {
|
128
122
|
'Content-Type': 'application/json'
|
@@ -144,12 +138,14 @@ document.getElementById('launchForm').addEventListener('submit', async (event) =
|
|
144
138
|
|
145
139
|
async function fetchCommands(hide=false) {
|
146
140
|
try {
|
147
|
-
const response = await fetch(`/commands
|
141
|
+
const response = await fetch(`/commands`);
|
148
142
|
if (!response.ok) {
|
149
143
|
document.getElementById('dimmer').style.display = 'block';
|
150
144
|
return;
|
151
145
|
}
|
152
|
-
|
146
|
+
// Adapt to the new result structure:
|
147
|
+
const data = await response.json();
|
148
|
+
const commands = data.commands;
|
153
149
|
commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
154
150
|
const commandsTbody = document.getElementById('commands');
|
155
151
|
commandsTbody.innerHTML = '';
|
@@ -252,13 +248,13 @@ async function viewOutput(command_id) {
|
|
252
248
|
outputPercentage.innerText = '100%';
|
253
249
|
adjustOutputHeight();
|
254
250
|
currentCommandId = command_id;
|
255
|
-
nextOutputLink = `/commands/${command_id}/output
|
251
|
+
nextOutputLink = `/commands/${command_id}/output`;
|
256
252
|
clearInterval(outputInterval);
|
257
253
|
terminal.clear();
|
258
254
|
terminal.reset();
|
259
255
|
fullOutput = '';
|
260
256
|
try {
|
261
|
-
const response = await fetch(`/commands/${command_id}
|
257
|
+
const response = await fetch(`/commands/${command_id}`);
|
262
258
|
if (!response.ok) {
|
263
259
|
outputInterval = setInterval(() => fetchOutput(nextOutputLink), 500);
|
264
260
|
}
|
@@ -289,7 +285,7 @@ async function viewOutput(command_id) {
|
|
289
285
|
async function openPopup(command_id, event) {
|
290
286
|
event.stopPropagation();
|
291
287
|
event.stopImmediatePropagation();
|
292
|
-
const popupUrl = `/commands/${command_id}/popup
|
288
|
+
const popupUrl = `/commands/${command_id}/popup`;
|
293
289
|
window.open(popupUrl, '_blank', 'width=1000,height=600');
|
294
290
|
}
|
295
291
|
|
@@ -297,7 +293,7 @@ async function relaunchCommand(command_id, event) {
|
|
297
293
|
event.stopPropagation();
|
298
294
|
event.stopImmediatePropagation();
|
299
295
|
try {
|
300
|
-
const response = await fetch(`/commands/${command_id}
|
296
|
+
const response = await fetch(`/commands/${command_id}`);
|
301
297
|
if (!response.ok) {
|
302
298
|
throw new Error('Failed to fetch command status');
|
303
299
|
}
|
@@ -308,7 +304,7 @@ async function relaunchCommand(command_id, event) {
|
|
308
304
|
}
|
309
305
|
fitAddon.fit();
|
310
306
|
terminal.clear();
|
311
|
-
const relaunchResponse = await fetch(`/commands
|
307
|
+
const relaunchResponse = await fetch(`/commands`, {
|
312
308
|
method: 'POST',
|
313
309
|
headers: {
|
314
310
|
'Content-Type': 'application/json'
|
@@ -336,7 +332,7 @@ async function stopCommand(command_id, event) {
|
|
336
332
|
event.stopPropagation();
|
337
333
|
event.stopImmediatePropagation();
|
338
334
|
try {
|
339
|
-
const response = await fetch(`/commands/${command_id}/stop
|
335
|
+
const response = await fetch(`/commands/${command_id}/stop`, {
|
340
336
|
method: 'PATCH'
|
341
337
|
});
|
342
338
|
if (!response.ok) {
|
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/
|
27
|
-
pywebexec/static/js/popup.js,sha256=
|
28
|
-
pywebexec/static/js/script.js,sha256=
|
26
|
+
pywebexec/static/js/executables.js,sha256=jwb5TcbWmEyhbfp5Xc3re_zr3hvOV-2u6zPzObQnn3g,9542
|
27
|
+
pywebexec/static/js/popup.js,sha256=0fr3pp4j9D2fXEVnHyQrx2bPWFHfgbb336dbewgH1d8,9023
|
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
|