pywebexec 1.7.1__py3-none-any.whl → 1.7.3__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 +27 -26
- pywebexec/static/css/style.css +15 -10
- pywebexec/static/images/fit-tty.svg +1 -0
- pywebexec/static/images/fit-win.svg +1 -0
- pywebexec/static/js/popup.js +41 -7
- pywebexec/static/js/script.js +42 -6
- pywebexec/templates/index.html +1 -0
- pywebexec/templates/popup.html +1 -0
- pywebexec/version.py +2 -2
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/METADATA +3 -3
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/RECORD +15 -13
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/LICENSE +0 -0
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/WHEEL +0 -0
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.7.1.dist-info → pywebexec-1.7.3.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -36,7 +36,7 @@ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Add SameSite attribute to sessi
|
|
36
36
|
auth = HTTPBasicAuth()
|
37
37
|
|
38
38
|
app.config['LDAP_SERVER'] = os.environ.get('PYWEBEXEC_LDAP_SERVER')
|
39
|
-
app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid")
|
39
|
+
app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid") # sAMAccountName
|
40
40
|
app.config['LDAP_GROUPS'] = os.environ.get('PYWEBEXEC_LDAP_GROUPS')
|
41
41
|
app.config['LDAP_BASE_DN'] = os.environ.get('PYWEBEXEC_LDAP_BASE_DN')
|
42
42
|
app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
|
@@ -56,6 +56,8 @@ if os.path.isdir(f"{CONFDIR}/.config"):
|
|
56
56
|
CONFDIR += '/.config'
|
57
57
|
CONFDIR += "/.pywebexec"
|
58
58
|
term_command_id = str(uuid.uuid4())
|
59
|
+
tty_cols = 125
|
60
|
+
tty_rows = 30
|
59
61
|
|
60
62
|
# In-memory cache for command statuses
|
61
63
|
status_cache = {}
|
@@ -181,6 +183,8 @@ class PyWebExec(Application):
|
|
181
183
|
return self.application
|
182
184
|
#38;2;66;59;165m
|
183
185
|
ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF]|[\x00-\x1F\x7F])')
|
186
|
+
ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF])')
|
187
|
+
ANSI_ESCAPE = re.compile(br'(\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF])')
|
184
188
|
|
185
189
|
def strip_ansi_control_chars(text):
|
186
190
|
"""Remove ANSI and control characters from the text."""
|
@@ -202,12 +206,10 @@ def get_visible_output(line, cols, rows):
|
|
202
206
|
stream = pyte.ByteStream(screen)
|
203
207
|
stream.feed(line)
|
204
208
|
visible_line = ""
|
205
|
-
|
206
|
-
|
207
|
-
visible_line = screen.display[row].strip(" ")
|
209
|
+
for visible_line in reversed(screen.display):
|
210
|
+
visible_line = visible_line.strip(" ")
|
208
211
|
if visible_line:
|
209
212
|
return visible_line
|
210
|
-
row -= 1
|
211
213
|
except:
|
212
214
|
return ""
|
213
215
|
return ""
|
@@ -215,8 +217,8 @@ def get_visible_output(line, cols, rows):
|
|
215
217
|
|
216
218
|
def get_last_line(file_path, cols=None, rows=None, maxsize=2048):
|
217
219
|
"""Retrieve last non empty line after vt100 interpretation"""
|
218
|
-
cols = cols or
|
219
|
-
rows = rows or
|
220
|
+
cols = cols or tty_cols
|
221
|
+
rows = rows or tty_rows
|
220
222
|
with open(file_path, 'rb') as fd:
|
221
223
|
try:
|
222
224
|
fd.seek(-maxsize, os.SEEK_END)
|
@@ -429,9 +431,6 @@ def get_status_file_path(command_id):
|
|
429
431
|
def get_output_file_path(command_id):
|
430
432
|
return os.path.join(COMMAND_STATUS_DIR, f'{command_id}_output.txt')
|
431
433
|
|
432
|
-
def get_done_file_path(command_id):
|
433
|
-
return os.path.join(COMMAND_STATUS_DIR, f'{command_id}.done')
|
434
|
-
|
435
434
|
def update_command_status(command_id, updates):
|
436
435
|
status_file_path = get_status_file_path(command_id)
|
437
436
|
status = read_command_status(command_id) or {}
|
@@ -443,8 +442,6 @@ def update_command_status(command_id, updates):
|
|
443
442
|
status_cache[command_id] = status
|
444
443
|
with open(status_file_path, 'w') as f:
|
445
444
|
json.dump(status, f)
|
446
|
-
if status.get('status') != 'running':
|
447
|
-
open(get_done_file_path(command_id), 'a').close()
|
448
445
|
|
449
446
|
|
450
447
|
def read_command_status(command_id):
|
@@ -455,7 +452,7 @@ def read_command_status(command_id):
|
|
455
452
|
status = status_data.get('status')
|
456
453
|
if status and status != "running":
|
457
454
|
return status_data
|
458
|
-
if command_id in status_cache and
|
455
|
+
if command_id in status_cache and status_cache.get('last_read',0)>datetime.now().timestamp()-0.5:
|
459
456
|
return status_data
|
460
457
|
status_file_path = get_status_file_path(command_id)
|
461
458
|
if not os.path.exists(status_file_path):
|
@@ -465,6 +462,7 @@ def read_command_status(command_id):
|
|
465
462
|
status_data.update(json.load(f))
|
466
463
|
except json.JSONDecodeError:
|
467
464
|
return None
|
465
|
+
status_data['last_read'] = datetime.now().timestamp()
|
468
466
|
status_cache[command_id] = status_data
|
469
467
|
return status_data
|
470
468
|
|
@@ -501,18 +499,19 @@ def run_command(fromip, user, command, params, command_id):
|
|
501
499
|
'command': command,
|
502
500
|
'params': params,
|
503
501
|
'start_time': start_time,
|
504
|
-
'user': user
|
502
|
+
'user': user,
|
503
|
+
'cols': tty_cols,
|
504
|
+
'rows': tty_rows,
|
505
505
|
})
|
506
506
|
output_file_path = get_output_file_path(command_id)
|
507
507
|
try:
|
508
508
|
with open(output_file_path, 'wb') as fd:
|
509
|
-
p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None)
|
509
|
+
p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(tty_rows, tty_cols))
|
510
510
|
update_command_status(command_id, {
|
511
511
|
'status': 'running',
|
512
512
|
'pid': p.pid,
|
513
513
|
'user': user
|
514
514
|
})
|
515
|
-
p.setwinsize(24, 125)
|
516
515
|
p.logfile = fd
|
517
516
|
p.expect(pexpect.EOF)
|
518
517
|
fd.flush()
|
@@ -594,6 +593,7 @@ def read_commands():
|
|
594
593
|
'start_time': status.get('start_time', 'N/A'),
|
595
594
|
'end_time': status.get('end_time', 'N/A'),
|
596
595
|
'command': command,
|
596
|
+
'user': status.get('user'),
|
597
597
|
'exit_code': status.get('exit_code', 'N/A'),
|
598
598
|
'last_output_line': status.get('last_output_line'),
|
599
599
|
})
|
@@ -688,13 +688,18 @@ def verify_ldap(username, password):
|
|
688
688
|
tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) if app.config['LDAP_SERVER'].startswith("ldaps:") else None
|
689
689
|
server = Server(app.config['LDAP_SERVER'], tls=tls_configuration, get_info=ALL)
|
690
690
|
user_filter = f"({app.config['LDAP_USER_ID']}={username})"
|
691
|
+
group_filter = ""
|
692
|
+
if app.config["LDAP_GROUPS"]:
|
693
|
+
group_filter = "".join(f"(memberOf={group})" for group in app.config['LDAP_GROUPS'].split(" "))
|
694
|
+
group_filter = f"(|{group_filter})"
|
695
|
+
ldap_filter = f"(&(objectClass=person){user_filter}{group_filter})"
|
691
696
|
try:
|
692
697
|
# Bind with the bind DN and password
|
693
698
|
conn = Connection(server, user=app.config['LDAP_BIND_DN'], password=app.config['LDAP_BIND_PASSWORD'], authentication=SIMPLE, auto_bind=True, read_only=True)
|
694
699
|
try:
|
695
|
-
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=
|
700
|
+
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=ldap_filter, search_scope=SUBTREE)
|
696
701
|
if len(conn.entries) == 0:
|
697
|
-
print(f"User {username} not found in LDAP.")
|
702
|
+
print(f"User {username} not found in LDAP in allowed groups.")
|
698
703
|
return False
|
699
704
|
user_dn = conn.entries[0].entry_dn
|
700
705
|
finally:
|
@@ -703,15 +708,10 @@ def verify_ldap(username, password):
|
|
703
708
|
# Bind with the user DN and password to verify credentials
|
704
709
|
conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE, auto_bind=True, read_only=True)
|
705
710
|
try:
|
706
|
-
if
|
711
|
+
if conn.result["result"] == 0:
|
707
712
|
return True
|
708
|
-
|
709
|
-
|
710
|
-
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=group_filter, search_scope=SUBTREE)
|
711
|
-
result = len(conn.entries) > 0
|
712
|
-
if not result:
|
713
|
-
print(f"User {username} is not a member of groups {app.config['LDAP_GROUPS']}.")
|
714
|
-
return result
|
713
|
+
print(f"{username}: Password mismatch")
|
714
|
+
return False
|
715
715
|
finally:
|
716
716
|
conn.unbind()
|
717
717
|
except Exception as e:
|
@@ -798,6 +798,7 @@ def get_command_output(command_id):
|
|
798
798
|
'output': output[-maxsize:],
|
799
799
|
'status': status_data.get("status"),
|
800
800
|
'cols': status_data.get("cols"),
|
801
|
+
'rows': status_data.get("rows"),
|
801
802
|
'links': {
|
802
803
|
'next': f'{request.url_root}command_output/{command_id}?offset={new_offset}&maxsize={maxsize}{token_param}'
|
803
804
|
}
|
pywebexec/static/css/style.css
CHANGED
@@ -48,7 +48,6 @@ select { /* Safari bug */
|
|
48
48
|
min-width: 150px;
|
49
49
|
}
|
50
50
|
.output {
|
51
|
-
white-space: pre-wrap;
|
52
51
|
background: #111;
|
53
52
|
padding: 10px;
|
54
53
|
border: 1px solid #ccc;
|
@@ -329,20 +328,18 @@ span {
|
|
329
328
|
|
330
329
|
.pause {
|
331
330
|
background-image: url("/static/images/pause.svg");
|
332
|
-
/*background-size: contain;*/
|
333
|
-
background-repeat: no-repeat;
|
334
|
-
background-position: center;
|
335
|
-
background-size: 24px 24px;
|
336
331
|
}
|
337
332
|
.resume {
|
338
333
|
background-image: url("/static/images/resume.svg");
|
339
|
-
/*background-size: contain;*/
|
340
|
-
background-repeat: no-repeat;
|
341
|
-
background-position: center;
|
342
|
-
background-size: 24px 24px;
|
343
334
|
}
|
344
335
|
|
345
|
-
.
|
336
|
+
.fit-window {
|
337
|
+
background-image: url("/static/images/fit-win.svg");
|
338
|
+
}
|
339
|
+
.fit-tty {
|
340
|
+
background-image: url("/static/images/fit-tty.svg");
|
341
|
+
}
|
342
|
+
.pause-resume-button, .fit-button {
|
346
343
|
background-color: transparent;
|
347
344
|
border: none;
|
348
345
|
/*padding: 0;*/
|
@@ -350,6 +347,10 @@ span {
|
|
350
347
|
height: 24px;
|
351
348
|
cursor: pointer;
|
352
349
|
flex-shrink: 0;
|
350
|
+
background-repeat: no-repeat;
|
351
|
+
background-position: center;
|
352
|
+
background-size: 24px 24px;
|
353
|
+
|
353
354
|
}
|
354
355
|
|
355
356
|
|
@@ -389,4 +390,8 @@ span {
|
|
389
390
|
border-radius: 17px;
|
390
391
|
min-width: 17px;
|
391
392
|
text-align: center;
|
393
|
+
}
|
394
|
+
|
395
|
+
.xterm-accessibility {
|
396
|
+
display: none;
|
392
397
|
}
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg height="200px" width="200px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-42.63 -42.63 558.91 558.91" xml:space="preserve" 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"> <circle style="fill:#404040;" cx="236.827" cy="236.827" r="236.827"></circle> <g> <path style="fill:#FFFFFF;" d="M421.487,220.971c-32.306,0-64.612,0-96.919,0c10.033-10.033,20.07-20.067,30.104-30.1 c12.782-12.778-7.128-32.538-19.939-19.726c-17.999,17.995-35.993,35.99-53.992,53.984c-5.422,5.419-5.277,14.453,0.105,19.835 c17.999,17.999,35.993,35.993,53.992,53.992c12.782,12.782,32.542-7.128,19.726-19.939c-9.996-9.996-19.992-19.992-29.988-29.988 c32.235,0,64.47,0,96.705,0C439.396,249.026,439.557,220.971,421.487,220.971z"></path> <path style="fill:#FFFFFF;" d="M193.011,225.021c-17.999-17.999-35.993-35.99-53.992-53.984 c-12.782-12.782-32.542,7.128-19.726,19.939c10,10,19.999,19.995,29.995,29.995c-32.239,0-64.474,0-96.713,0 c-18.111,0-18.272,28.054-0.202,28.054c32.302,0,64.601,0,96.904,0c-10.03,10.033-20.063,20.059-30.092,30.092 c-12.782,12.782,7.128,32.542,19.939,19.726c17.999-17.999,35.993-35.993,53.992-53.992 C198.541,239.434,198.396,230.399,193.011,225.021z"></path> <circle style="fill:#FFFFFF;" cx="236.827" cy="236.79" r="24.098"></circle> </g> </g></svg>
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg height="200px" width="200px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-42.63 -42.63 558.91 558.91" xml:space="preserve" 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"> <circle style="fill:#404040;" cx="236.827" cy="236.827" r="236.827"></circle> <g> <path style="fill:#FFFFFF;" d="M431.251,225.029c-17.999-17.999-35.993-36.001-53.992-53.996 c-12.778-12.782-32.542,7.128-19.726,19.939c9.996,10,19.992,19.995,29.991,29.995c-32.235,0-64.474,0-96.709,0 c-18.111,0-18.272,28.054-0.202,28.054c32.306,0,64.612,0,96.919,0c-10.033,10.033-20.07,20.067-30.104,30.1 c-12.782,12.778,7.128,32.538,19.939,19.726c17.999-17.995,35.993-35.99,53.992-53.984 C436.782,239.437,436.633,230.406,431.251,225.029z"></path> <path style="fill:#FFFFFF;" d="M183.247,220.964c-32.302,0-64.605,0-96.907,0c10.033-10.033,20.063-20.067,30.096-30.1 c12.782-12.782-7.128-32.542-19.939-19.726c-17.999,17.999-35.993,35.997-53.992,53.996c-5.422,5.422-5.277,14.453,0.105,19.835 c17.999,17.999,35.993,35.99,53.992,53.984c12.782,12.782,32.542-7.128,19.726-19.939c-10-10-19.999-19.996-29.995-29.995 c32.239,0,64.474,0,96.713,0C201.155,249.018,201.316,220.964,183.247,220.964z"></path> <circle style="fill:#FFFFFF;" cx="236.827" cy="236.79" r="24.098"></circle> </g> </g></svg>
|
pywebexec/static/js/popup.js
CHANGED
@@ -75,8 +75,30 @@ let fullOutput = '';
|
|
75
75
|
let outputLength = 0;
|
76
76
|
let slider = null;
|
77
77
|
let isPaused = false;
|
78
|
+
let cols = 0;
|
79
|
+
let rows = 0;
|
80
|
+
let fitWindow = false;
|
81
|
+
|
78
82
|
const toggleButton = document.getElementById('toggleFetch');
|
79
83
|
const pausedMessage = document.getElementById('pausedMessage');
|
84
|
+
const toggleFitButton = document.getElementById('toggleFit');
|
85
|
+
|
86
|
+
function autoFit(scroll=true) {
|
87
|
+
// Scroll output div to bottom
|
88
|
+
const outputDiv = document.getElementById('output');
|
89
|
+
outputDiv.scrollTop = terminal.element.clientHeight - outputDiv.clientHeight + 20;
|
90
|
+
if (cols && !fitWindow) {
|
91
|
+
let fit = fitAddon.proposeDimensions();
|
92
|
+
if (fit.rows < rows) {
|
93
|
+
terminal.resize(cols, rows);
|
94
|
+
} else {
|
95
|
+
terminal.resize(cols, fit.rows);
|
96
|
+
}
|
97
|
+
} else {
|
98
|
+
fitAddon.fit();
|
99
|
+
}
|
100
|
+
if (scroll) terminal.scrollToBottom();
|
101
|
+
}
|
80
102
|
|
81
103
|
function getTokenParam() {
|
82
104
|
const urlParams = new URLSearchParams(window.location.search);
|
@@ -104,8 +126,10 @@ async function fetchOutput(url) {
|
|
104
126
|
clearInterval(outputInterval);
|
105
127
|
} else {
|
106
128
|
if (data.cols) {
|
107
|
-
|
108
|
-
|
129
|
+
cols = data.cols;
|
130
|
+
rows = data.rows;
|
131
|
+
autoFit(false);
|
132
|
+
}
|
109
133
|
percentage = slider.value;
|
110
134
|
fullOutput += data.output;
|
111
135
|
if (fullOutput.length > maxSize)
|
@@ -178,8 +202,7 @@ function adjustOutputHeight() {
|
|
178
202
|
const outputTop = outputDiv.getBoundingClientRect().top;
|
179
203
|
const maxHeight = windowHeight - outputTop - 60; // Adjusted for slider height
|
180
204
|
outputDiv.style.height = `${maxHeight}px`;
|
181
|
-
|
182
|
-
sliderUpdateOutput();
|
205
|
+
autoFit();
|
183
206
|
}
|
184
207
|
|
185
208
|
function sliderUpdateOutput() {
|
@@ -214,9 +237,20 @@ function toggleFetchOutput() {
|
|
214
237
|
}
|
215
238
|
isPaused = !isPaused;
|
216
239
|
}
|
240
|
+
function toggleFit() {
|
241
|
+
fitWindow = ! fitWindow;
|
242
|
+
if (fitWindow) {
|
243
|
+
toggleFitButton.classList.add('fit-tty');
|
244
|
+
toggleFitButton.setAttribute('title', 'terminal fit tty');
|
245
|
+
} else {
|
246
|
+
toggleFitButton.classList.remove('fit-tty');
|
247
|
+
toggleFitButton.setAttribute('title', 'terminal fit window');
|
248
|
+
}
|
249
|
+
autoFit();
|
250
|
+
}
|
217
251
|
|
218
252
|
toggleButton.addEventListener('click', toggleFetchOutput);
|
219
|
-
|
253
|
+
toggleFitButton.addEventListener('click', toggleFit);
|
220
254
|
window.addEventListener('resize', adjustOutputHeight);
|
221
255
|
window.addEventListener('load', () => {
|
222
256
|
slider = document.getElementById('outputSlider');
|
@@ -229,11 +263,11 @@ window.addEventListener('load', () => {
|
|
229
263
|
document.getElementById('decreaseFontSize').addEventListener('click', () => {
|
230
264
|
fontSize = Math.max(8, fontSize - 1);
|
231
265
|
terminal.options.fontSize = fontSize;
|
232
|
-
|
266
|
+
autoFit();
|
233
267
|
});
|
234
268
|
|
235
269
|
document.getElementById('increaseFontSize').addEventListener('click', () => {
|
236
270
|
fontSize = Math.min(32, fontSize + 1);
|
237
271
|
terminal.options.fontSize = fontSize;
|
238
|
-
|
272
|
+
autoFit();
|
239
273
|
});
|
pywebexec/static/js/script.js
CHANGED
@@ -11,6 +11,9 @@ let fontSize = 14;
|
|
11
11
|
let isPaused = false;
|
12
12
|
let showRunningOnly = false;
|
13
13
|
let hiddenCommandIds = [];
|
14
|
+
let fitWindow = false;
|
15
|
+
let cols = 0;
|
16
|
+
let rows = 0;
|
14
17
|
|
15
18
|
function initTerminal()
|
16
19
|
{
|
@@ -97,6 +100,23 @@ terminal.onSelectionChange(() => {
|
|
97
100
|
}
|
98
101
|
});
|
99
102
|
|
103
|
+
function autoFit(scroll=true) {
|
104
|
+
// Scroll output div to bottom
|
105
|
+
const outputDiv = document.getElementById('output');
|
106
|
+
outputDiv.scrollTop = terminal.element.clientHeight - outputDiv.clientHeight + 20;
|
107
|
+
if (cols && !fitWindow) {
|
108
|
+
let fit = fitAddon.proposeDimensions();
|
109
|
+
if (fit.rows < rows) {
|
110
|
+
terminal.resize(cols, rows);
|
111
|
+
} else {
|
112
|
+
terminal.resize(cols, fit.rows);
|
113
|
+
}
|
114
|
+
} else {
|
115
|
+
fitAddon.fit();
|
116
|
+
}
|
117
|
+
if (scroll) terminal.scrollToBottom();
|
118
|
+
}
|
119
|
+
|
100
120
|
function getTokenParam() {
|
101
121
|
const urlParams = new URLSearchParams(window.location.search);
|
102
122
|
return urlParams.get('token') ? `?token=${urlParams.get('token')}` : '';
|
@@ -166,7 +186,7 @@ async function fetchCommands(hide=false) {
|
|
166
186
|
<td>
|
167
187
|
${command.command.startsWith('term') ? '' : command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}', event)">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}', event)">Run</button>`}
|
168
188
|
</td>
|
169
|
-
<td class="system-font">${command.command.replace(/^\.\//, '')}</td>
|
189
|
+
<td class="system-font" title="${command.user == '-' ? '' : command.user}">${command.command.replace(/^\.\//, '')}</td>
|
170
190
|
<td class="monospace outcol">
|
171
191
|
<button class="popup-button" onclick="openPopup('${command.command_id}', event)"></button>
|
172
192
|
${command.last_output_line || ''}
|
@@ -201,8 +221,10 @@ async function fetchOutput(url) {
|
|
201
221
|
clearInterval(outputInterval);
|
202
222
|
} else {
|
203
223
|
if (data.cols) {
|
204
|
-
|
205
|
-
|
224
|
+
cols = data.cols;
|
225
|
+
rows = data.rows;
|
226
|
+
autoFit(scroll=false);
|
227
|
+
}
|
206
228
|
fullOutput += data.output;
|
207
229
|
if (fullOutput.length > maxSize)
|
208
230
|
fullOutput = fullOutput.slice(-maxSize);
|
@@ -370,7 +392,7 @@ function adjustOutputHeight() {
|
|
370
392
|
const outputTop = outputDiv.getBoundingClientRect().top;
|
371
393
|
const maxHeight = windowHeight - outputTop - 60; // Adjusted for slider height
|
372
394
|
outputDiv.style.height = `${maxHeight}px`;
|
373
|
-
|
395
|
+
autoFit();
|
374
396
|
}
|
375
397
|
|
376
398
|
function initResizer() {
|
@@ -412,17 +434,19 @@ slider.addEventListener('input', sliderUpdateOutput);
|
|
412
434
|
document.getElementById('decreaseFontSize').addEventListener('click', () => {
|
413
435
|
fontSize = Math.max(8, fontSize - 1);
|
414
436
|
terminal.options.fontSize = fontSize;
|
415
|
-
|
437
|
+
autoFit();
|
416
438
|
});
|
417
439
|
|
418
440
|
document.getElementById('increaseFontSize').addEventListener('click', () => {
|
419
441
|
fontSize = Math.min(32, fontSize + 1);
|
420
442
|
terminal.options.fontSize = fontSize;
|
421
|
-
|
443
|
+
autoFit();
|
422
444
|
});
|
423
445
|
|
424
446
|
const toggleButton = document.getElementById('toggleFetch');
|
425
447
|
const pausedMessage = document.getElementById('pausedMessage');
|
448
|
+
const toggleFitButton = document.getElementById('toggleFit');
|
449
|
+
|
426
450
|
|
427
451
|
function toggleFetchOutput() {
|
428
452
|
if (isPaused) {
|
@@ -446,8 +470,20 @@ function toggleFetchOutput() {
|
|
446
470
|
}
|
447
471
|
isPaused = !isPaused;
|
448
472
|
}
|
473
|
+
function toggleFit() {
|
474
|
+
fitWindow = ! fitWindow;
|
475
|
+
if (fitWindow) {
|
476
|
+
toggleFitButton.classList.add('fit-tty');
|
477
|
+
toggleFitButton.setAttribute('title', 'terminal fit tty');
|
478
|
+
} else {
|
479
|
+
toggleFitButton.classList.remove('fit-tty');
|
480
|
+
toggleFitButton.setAttribute('title', 'terminal fit window');
|
481
|
+
}
|
482
|
+
autoFit();
|
483
|
+
}
|
449
484
|
|
450
485
|
toggleButton.addEventListener('click', toggleFetchOutput);
|
486
|
+
toggleFitButton.addEventListener('click', toggleFit);
|
451
487
|
|
452
488
|
document.getElementById('thStatus').addEventListener('click', () => {
|
453
489
|
showRunningOnly = !showRunningOnly;
|
pywebexec/templates/index.html
CHANGED
@@ -52,6 +52,7 @@
|
|
52
52
|
<button id="toggleFetch" class="pause-resume-button pause"></button>
|
53
53
|
<button id="decreaseFontSize" class="font-size-button font-decrease"></button>
|
54
54
|
<button id="increaseFontSize" class="font-size-button font-increase"></button>
|
55
|
+
<button id="toggleFit" class="fit-button fit-window" title="terminal fit window"></button>
|
55
56
|
</div>
|
56
57
|
<script src="/static/js/xterm/xterm.js"></script>
|
57
58
|
<script src="/static/js/xterm/addon-canvas.js"></script>
|
pywebexec/templates/popup.html
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
<button id="toggleFetch" class="pause-resume-button pause"></button>
|
21
21
|
<button id="decreaseFontSize" class="font-size-button font-decrease"></button>
|
22
22
|
<button id="increaseFontSize" class="font-size-button font-increase"></button>
|
23
|
+
<button id="toggleFit" class="fit-button fit-window" title="terminal fit window"></button>
|
23
24
|
</div>
|
24
25
|
<script src="/static/js/xterm/xterm.js"></script>
|
25
26
|
<script src="/static/js/xterm/addon-canvas.js"></script>
|
pywebexec/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: pywebexec
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.3
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
6
6
|
Author: Franck Jouvanceau
|
@@ -157,9 +157,9 @@ Generated password is given if no `--pasword` option
|
|
157
157
|
$ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
|
158
158
|
$ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
|
159
159
|
$ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
|
160
|
-
$ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
|
161
|
-
$ export PYWEBEXEC_LDAP_USER_ID="uid"
|
162
160
|
$ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
|
161
|
+
$ export PYWEBEXEC_LDAP_USER_ID="uid" # sAMAccountName for AD
|
162
|
+
$ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,dc=example,dc=com ou=scientists,dc=example,dc=com"
|
163
163
|
$ pywebexec
|
164
164
|
```
|
165
165
|
## HTTPS server
|
@@ -1,8 +1,8 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
|
2
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
-
pywebexec/version.py,sha256=
|
2
|
+
pywebexec/pywebexec.py,sha256=YlPtbHqISlpQN5S0gPVk0gRbvuJtg1rXrF2SPzgUXjA,33456
|
3
|
+
pywebexec/version.py,sha256=Lv0gR-NbC-8DxwfmwXEmOzSq6Hgx6MH4xF1fYh_opXo,411
|
4
4
|
pywebexec/static/css/Consolas NF.ttf,sha256=DJEOzF0eqZ-kxu3Gs_VE8X0NJqiobBzmxWDGpdgGRxI,1313900
|
5
|
-
pywebexec/static/css/style.css,sha256=
|
5
|
+
pywebexec/static/css/style.css,sha256=3s7QgbCh4wb7kfZ7Pjo-B6o3lDIBogZ-3j6AfaPdpzU,8209
|
6
6
|
pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
|
7
7
|
pywebexec/static/images/aborted.svg,sha256=2nuvSwGBIZGWtlM1DrBO3qiSq1reDbcZDAj9rJXBnjY,380
|
8
8
|
pywebexec/static/images/copy.svg,sha256=d9OwtGh5GzzZHzYcDrLfNxZYLth1Q64x7bRyYxu4Px0,622
|
@@ -10,6 +10,8 @@ pywebexec/static/images/copy_ok.svg,sha256=mEqUVUhSq8xaJK2msQkxRawnz_KwlCZ-tok8Q
|
|
10
10
|
pywebexec/static/images/down-arrow.svg,sha256=4TclEmntMvKk_F_ADXgTpGtviYo826EDmmZiGE7HQBI,121
|
11
11
|
pywebexec/static/images/failed.svg,sha256=rSTBWOtiz7slGDobeU_vaLaMM8CoPeul_tQgWVBcodo,1438
|
12
12
|
pywebexec/static/images/favicon.svg,sha256=9gSN5Oak1zTWhTCyutlupPBKUxcbdoVt7dvhk8xvEug,1224
|
13
|
+
pywebexec/static/images/fit-tty.svg,sha256=gyRB9cqvXFSUOzpBq_Cr0Hv8nIpfr5ca74uUXjkLR-A,1438
|
14
|
+
pywebexec/static/images/fit-win.svg,sha256=_JNK-ew7mc8QIFG2D2TPNm0W2wN2d3Oa6d2d8iwbCFQ,1435
|
13
15
|
pywebexec/static/images/font-decrease.svg,sha256=89TJXQpfZ-XDJ2qC0c7cbLH-tWC-4AJQr_VOgsp06Gg,1782
|
14
16
|
pywebexec/static/images/font-increase.svg,sha256=u-EBxUzmOhKx4UH96fb4gLZ0l6Jkew1HLhcSqz7Z1ak,1869
|
15
17
|
pywebexec/static/images/norun.svg,sha256=_9PyQklBOfUwFak1gVrm-76tYM825urAFhj-zMBec20,555
|
@@ -19,8 +21,8 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
|
|
19
21
|
pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
|
20
22
|
pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
|
21
23
|
pywebexec/static/js/commands.js,sha256=h2fkd9qpypLBxvhEEbay23nwuqUwcKJA0vHugcyL8pU,7961
|
22
|
-
pywebexec/static/js/popup.js,sha256=
|
23
|
-
pywebexec/static/js/script.js,sha256=
|
24
|
+
pywebexec/static/js/popup.js,sha256=UPKtjPLIR5KaAV2i1iWUGwcCTX1tT3vnOIMc8VKQqag,9311
|
25
|
+
pywebexec/static/js/script.js,sha256=yO2PEsyO1F5w3lSCJyKkAtp21Tb7I3MQMPibxvdmFOc,18198
|
24
26
|
pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
|
25
27
|
pywebexec/static/js/xterm/addon-canvas.js,sha256=ez6QTVvsmLVNJmdJlM-ZQ5bErwlxAQ_9DUmDIptl2TM,94607
|
26
28
|
pywebexec/static/js/xterm/addon-canvas.js.map,sha256=ECBA4B-BqUpdFeRzlsEWLSQnudnhLP-yPQJ8_hKquMo,379537
|
@@ -31,11 +33,11 @@ pywebexec/static/js/xterm/addon-unicode11.js.map,sha256=paDj5KKtTIUGedQn2x7CaUTD
|
|
31
33
|
pywebexec/static/js/xterm/xterm.js,sha256=H5kaw7Syg-v5bmCuI6AKUnZd06Lkb6b92p8aqwMvdJU,289441
|
32
34
|
pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCfN0U_OI6Zw,1118392
|
33
35
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
|
-
pywebexec/templates/index.html,sha256=
|
35
|
-
pywebexec/templates/popup.html,sha256=
|
36
|
-
pywebexec-1.7.
|
37
|
-
pywebexec-1.7.
|
38
|
-
pywebexec-1.7.
|
39
|
-
pywebexec-1.7.
|
40
|
-
pywebexec-1.7.
|
41
|
-
pywebexec-1.7.
|
36
|
+
pywebexec/templates/index.html,sha256=2fEN8cggHBEd8-RamDFpnekVJtIbRembFSw0-1YEptc,2979
|
37
|
+
pywebexec/templates/popup.html,sha256=GT2jY7oOxpCaBaRl924QJWdFBmfSOP952T13d37R_pY,1506
|
38
|
+
pywebexec-1.7.3.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
39
|
+
pywebexec-1.7.3.dist-info/METADATA,sha256=-JC0TqKo6F5B43R4TKYjxzoyXvL986W-djlTshi6dUM,8060
|
40
|
+
pywebexec-1.7.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
41
|
+
pywebexec-1.7.3.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
42
|
+
pywebexec-1.7.3.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
43
|
+
pywebexec-1.7.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|