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 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
- row = rows - 1
206
- while row > 0:
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 125
219
- rows = rows or 24
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 not os.path.exists(get_done_file_path(command_id)):
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=user_filter, search_scope=SUBTREE)
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 not app.config['LDAP_GROUPS'] and conn.result["result"] == 0:
711
+ if conn.result["result"] == 0:
707
712
  return True
708
- group_filter = "".join([f'({group})' for group in app.config['LDAP_GROUPS'].split(",")])
709
- group_filter = f"(&{group_filter}(|(member={user_dn})(uniqueMember={user_dn})))"
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
  }
@@ -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
- .pause-resume-button {
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>
@@ -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
- terminal.resize(data.cols, terminal.rows);
108
- } else fitAddon.fit();
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
- fitAddon.fit();
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
- fitAddon.fit();
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
- fitAddon.fit();
272
+ autoFit();
239
273
  });
@@ -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
- terminal.resize(data.cols, terminal.rows);
205
- } else fitAddon.fit();
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
- fitAddon.fit();
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
- fitAddon.fit();
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
- fitAddon.fit();
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;
@@ -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>
@@ -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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.7.1'
16
- __version_tuple__ = version_tuple = (1, 7, 1)
15
+ __version__ = version = '1.7.3'
16
+ __version_tuple__ = version_tuple = (1, 7, 3)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.7.1
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=yPag3ijHH1mzI6k6DjyvBms7FNAQ3UH_IPyeCHU-AO8,33323
3
- pywebexec/version.py,sha256=kn-QYzzAhfbnfKK6EpE9gJz8TDZkEk52evaid1DHkG4,411
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=MJHUBpjWL4sLxM7a7DxypmPKaFJQbmA_ESNXsbLviNI,8201
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=I5TLYUm5s2p12a0VfFONOxxzhXLBmnpVzM-B5W-GPis,8294
23
- pywebexec/static/js/script.js,sha256=EPofQO0ec2bwIr2YdzzNY3dcXipW2_Mi2j34DIYvJeE,17096
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=5h8kLyzAhbvUDU9sEwrGIvD6FxAMcDZLXlN-ldlO8KU,2880
35
- pywebexec/templates/popup.html,sha256=3ZqQcE9mYs-RXv0Lfb24zntOlvR137ZYI9mtCZNVAo0,1407
36
- pywebexec-1.7.1.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
37
- pywebexec-1.7.1.dist-info/METADATA,sha256=2qLB_f1jdH1RY5hctK3TZcD8_EofCTtsfLp0uYix0Is,8000
38
- pywebexec-1.7.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
- pywebexec-1.7.1.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
40
- pywebexec-1.7.1.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
41
- pywebexec-1.7.1.dist-info/RECORD,,
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,,