pywebexec 1.7.2__py3-none-any.whl → 1.7.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 +27 -24
- pywebexec/static/js/popup.js +2 -1
- pywebexec/static/js/script.js +2 -2
- pywebexec/version.py +2 -2
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/METADATA +5 -4
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/RECORD +10 -10
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/LICENSE +0 -0
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/WHEEL +0 -0
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.7.2.dist-info → pywebexec-1.7.4.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -33,10 +33,12 @@ if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
|
33
33
|
app = Flask(__name__)
|
34
34
|
app.secret_key = os.urandom(24) # Secret key for session management
|
35
35
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Add SameSite attribute to session cookies
|
36
|
+
app.config['SESSION_COOKIE_SECURE'] = True
|
37
|
+
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
36
38
|
auth = HTTPBasicAuth()
|
37
39
|
|
38
40
|
app.config['LDAP_SERVER'] = os.environ.get('PYWEBEXEC_LDAP_SERVER')
|
39
|
-
app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid")
|
41
|
+
app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid") # sAMAccountName
|
40
42
|
app.config['LDAP_GROUPS'] = os.environ.get('PYWEBEXEC_LDAP_GROUPS')
|
41
43
|
app.config['LDAP_BASE_DN'] = os.environ.get('PYWEBEXEC_LDAP_BASE_DN')
|
42
44
|
app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
|
@@ -112,7 +114,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
112
114
|
from cryptography.hazmat.backends import default_backend
|
113
115
|
from cryptography.hazmat.primitives import serialization
|
114
116
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
115
|
-
|
117
|
+
|
116
118
|
# Generate our key
|
117
119
|
if key is None:
|
118
120
|
key = rsa.generate_private_key(
|
@@ -120,16 +122,16 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
120
122
|
key_size=2048,
|
121
123
|
backend=default_backend(),
|
122
124
|
)
|
123
|
-
|
125
|
+
|
124
126
|
name = x509.Name([
|
125
127
|
x509.NameAttribute(NameOID.COMMON_NAME, hostname)
|
126
128
|
])
|
127
|
-
|
128
|
-
# best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
|
129
|
+
|
130
|
+
# best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
|
129
131
|
alt_names = [x509.DNSName(hostname)]
|
130
132
|
alt_names.append(x509.DNSName("localhost"))
|
131
|
-
|
132
|
-
# allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
|
133
|
+
|
134
|
+
# allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
|
133
135
|
if ip_addresses:
|
134
136
|
for addr in ip_addresses:
|
135
137
|
# openssl wants DNSnames for ips...
|
@@ -138,7 +140,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
138
140
|
# note: older versions of cryptography do not understand ip_address objects
|
139
141
|
alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
|
140
142
|
san = x509.SubjectAlternativeName(alt_names)
|
141
|
-
|
143
|
+
|
142
144
|
# path_len=0 means this cert can only sign itself, not other certs.
|
143
145
|
basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
|
144
146
|
now = datetime.now(timezone.utc)
|
@@ -225,7 +227,7 @@ def get_last_line(file_path, cols=None, rows=None, maxsize=2048):
|
|
225
227
|
except OSError:
|
226
228
|
fd.seek(0)
|
227
229
|
return get_visible_output(fd.read(), cols, rows)
|
228
|
-
|
230
|
+
|
229
231
|
|
230
232
|
def start_gunicorn(daemonized=False, baselog=None):
|
231
233
|
check_processes()
|
@@ -304,7 +306,7 @@ def daemon_d(action, pidfilepath, silent=False, hostname=None, args=None):
|
|
304
306
|
def start_term():
|
305
307
|
os.environ["PYWEBEXEC"] = " (shared)"
|
306
308
|
os.chdir(CWD)
|
307
|
-
start_time = datetime.now().isoformat()
|
309
|
+
start_time = datetime.now(timezone.utc).isoformat()
|
308
310
|
user = pwd.getpwuid(os.getuid())[0]
|
309
311
|
print(f"Starting terminal session for {user} : {term_command_id}")
|
310
312
|
update_command_status(term_command_id, {
|
@@ -316,7 +318,7 @@ def start_term():
|
|
316
318
|
})
|
317
319
|
output_file_path = get_output_file_path(term_command_id)
|
318
320
|
res = script(output_file_path)
|
319
|
-
end_time = datetime.now().isoformat()
|
321
|
+
end_time = datetime.now(timezone.utc).isoformat()
|
320
322
|
update_command_status(term_command_id, {
|
321
323
|
'status': 'success',
|
322
324
|
'end_time': end_time,
|
@@ -488,7 +490,7 @@ def script(output_file):
|
|
488
490
|
sigwinch_passthrough(None, None)
|
489
491
|
signal.signal(signal.SIGWINCH, sigwinch_passthrough)
|
490
492
|
p.interact()
|
491
|
-
|
493
|
+
|
492
494
|
|
493
495
|
def run_command(fromip, user, command, params, command_id):
|
494
496
|
# app.logger.info(f'{fromip} run_command {command_id} {user}: {command} {params}')
|
@@ -593,6 +595,7 @@ def read_commands():
|
|
593
595
|
'start_time': status.get('start_time', 'N/A'),
|
594
596
|
'end_time': status.get('end_time', 'N/A'),
|
595
597
|
'command': command,
|
598
|
+
'user': status.get('user'),
|
596
599
|
'exit_code': status.get('exit_code', 'N/A'),
|
597
600
|
'last_output_line': status.get('last_output_line'),
|
598
601
|
})
|
@@ -662,7 +665,7 @@ def check_authentication():
|
|
662
665
|
if request.args.get('token') == token:
|
663
666
|
return
|
664
667
|
return jsonify({'error': 'Forbidden'}), 403
|
665
|
-
|
668
|
+
|
666
669
|
if not app.config['USER'] and not app.config['LDAP_SERVER']:
|
667
670
|
return
|
668
671
|
|
@@ -687,30 +690,30 @@ def verify_ldap(username, password):
|
|
687
690
|
tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) if app.config['LDAP_SERVER'].startswith("ldaps:") else None
|
688
691
|
server = Server(app.config['LDAP_SERVER'], tls=tls_configuration, get_info=ALL)
|
689
692
|
user_filter = f"({app.config['LDAP_USER_ID']}={username})"
|
693
|
+
group_filter = ""
|
694
|
+
if app.config["LDAP_GROUPS"]:
|
695
|
+
group_filter = "".join(f"(memberOf={group})" for group in app.config['LDAP_GROUPS'].split(" "))
|
696
|
+
group_filter = f"(|{group_filter})"
|
697
|
+
ldap_filter = f"(&(objectClass=person){user_filter}{group_filter})"
|
690
698
|
try:
|
691
699
|
# Bind with the bind DN and password
|
692
700
|
conn = Connection(server, user=app.config['LDAP_BIND_DN'], password=app.config['LDAP_BIND_PASSWORD'], authentication=SIMPLE, auto_bind=True, read_only=True)
|
693
701
|
try:
|
694
|
-
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=
|
702
|
+
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=ldap_filter, search_scope=SUBTREE)
|
695
703
|
if len(conn.entries) == 0:
|
696
|
-
print(f"User {username} not found in LDAP.")
|
704
|
+
print(f"User {username} not found in LDAP in allowed groups.")
|
697
705
|
return False
|
698
706
|
user_dn = conn.entries[0].entry_dn
|
699
707
|
finally:
|
700
708
|
conn.unbind()
|
701
|
-
|
709
|
+
|
702
710
|
# Bind with the user DN and password to verify credentials
|
703
711
|
conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE, auto_bind=True, read_only=True)
|
704
712
|
try:
|
705
|
-
if
|
713
|
+
if conn.result["result"] == 0:
|
706
714
|
return True
|
707
|
-
|
708
|
-
|
709
|
-
conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=group_filter, search_scope=SUBTREE)
|
710
|
-
result = len(conn.entries) > 0
|
711
|
-
if not result:
|
712
|
-
print(f"User {username} is not a member of groups {app.config['LDAP_GROUPS']}.")
|
713
|
-
return result
|
715
|
+
print(f"{username}: Password mismatch")
|
716
|
+
return False
|
714
717
|
finally:
|
715
718
|
conn.unbind()
|
716
719
|
except Exception as e:
|
pywebexec/static/js/popup.js
CHANGED
@@ -117,7 +117,7 @@ async function fetchOutput(url) {
|
|
117
117
|
try {
|
118
118
|
const response = await fetch(url);
|
119
119
|
if (!response.ok) {
|
120
|
-
document.getElementById('dimmer').style.display = '
|
120
|
+
document.getElementById('dimmer').style.display = 'block';
|
121
121
|
return;
|
122
122
|
}
|
123
123
|
const data = await response.json();
|
@@ -151,6 +151,7 @@ async function fetchOutput(url) {
|
|
151
151
|
toggleButton.style.display = 'block';
|
152
152
|
setCommandStatus(data.status)
|
153
153
|
}
|
154
|
+
document.getElementById('dimmer').style.display = 'none';
|
154
155
|
}
|
155
156
|
} catch (error) {
|
156
157
|
document.getElementById('dimmer').style.display = 'block';
|
pywebexec/static/js/script.js
CHANGED
@@ -186,7 +186,7 @@ async function fetchCommands(hide=false) {
|
|
186
186
|
<td>
|
187
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>`}
|
188
188
|
</td>
|
189
|
-
<td class="system-font">${command.command.replace(/^\.\//, '')}</td>
|
189
|
+
<td class="system-font" title="${command.user == '-' ? '' : command.user}">${command.command.replace(/^\.\//, '')}</td>
|
190
190
|
<td class="monospace outcol">
|
191
191
|
<button class="popup-button" onclick="openPopup('${command.command_id}', event)"></button>
|
192
192
|
${command.last_output_line || ''}
|
@@ -361,7 +361,7 @@ async function stopCommand(command_id, event) {
|
|
361
361
|
function formatTime(time) {
|
362
362
|
if (!time || time === 'N/A') return 'N/A';
|
363
363
|
const date = new Date(time);
|
364
|
-
return date.
|
364
|
+
return date.toLocaleString().slice(0, 16).replace('T', ' ');
|
365
365
|
}
|
366
366
|
|
367
367
|
function formatDuration(startTime, endTime) {
|
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.4
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
6
6
|
Author: Franck Jouvanceau
|
@@ -153,13 +153,14 @@ $ pywebexec -u myuser [-P mypass]
|
|
153
153
|
Generated password is given if no `--pasword` option
|
154
154
|
|
155
155
|
* ldap(s) password check / group member
|
156
|
+
ldap server must accept memberOf attribute for group members
|
156
157
|
```shell
|
157
|
-
$ export PYWEBEXEC_LDAP_SERVER=
|
158
|
+
$ export PYWEBEXEC_LDAP_SERVER=ldaps://ldap.mydomain.com:389
|
158
159
|
$ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
|
159
160
|
$ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
|
160
|
-
$ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
|
161
|
-
$ export PYWEBEXEC_LDAP_USER_ID="uid"
|
162
161
|
$ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
|
162
|
+
$ export PYWEBEXEC_LDAP_USER_ID="uid" # sAMAccountName for AD
|
163
|
+
$ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,dc=example,dc=com ou=scientists,dc=example,dc=com"
|
163
164
|
$ pywebexec
|
164
165
|
```
|
165
166
|
## HTTPS server
|
@@ -1,6 +1,6 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
|
2
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
-
pywebexec/version.py,sha256=
|
2
|
+
pywebexec/pywebexec.py,sha256=VBR6KmRiHNCd0ZvGEu_5sTqzVrUEPBhM8Hi6knS1QcA,33526
|
3
|
+
pywebexec/version.py,sha256=RXCVfb--6hzsLq9eHm4YOs9d-j0v74-HRGs2DFWnpjQ,411
|
4
4
|
pywebexec/static/css/Consolas NF.ttf,sha256=DJEOzF0eqZ-kxu3Gs_VE8X0NJqiobBzmxWDGpdgGRxI,1313900
|
5
5
|
pywebexec/static/css/style.css,sha256=3s7QgbCh4wb7kfZ7Pjo-B6o3lDIBogZ-3j6AfaPdpzU,8209
|
6
6
|
pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
|
@@ -21,8 +21,8 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
|
|
21
21
|
pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
|
22
22
|
pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
|
23
23
|
pywebexec/static/js/commands.js,sha256=h2fkd9qpypLBxvhEEbay23nwuqUwcKJA0vHugcyL8pU,7961
|
24
|
-
pywebexec/static/js/popup.js,sha256=
|
25
|
-
pywebexec/static/js/script.js,sha256=
|
24
|
+
pywebexec/static/js/popup.js,sha256=oVkwnOKHae11omBiTZs-00NVeS9HGNEiUo71JSwOqn4,9382
|
25
|
+
pywebexec/static/js/script.js,sha256=hT50gLRs-GRQoxDNJN__X3UQxk003M1ZPlvmzj8BZtc,18201
|
26
26
|
pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
|
27
27
|
pywebexec/static/js/xterm/addon-canvas.js,sha256=ez6QTVvsmLVNJmdJlM-ZQ5bErwlxAQ_9DUmDIptl2TM,94607
|
28
28
|
pywebexec/static/js/xterm/addon-canvas.js.map,sha256=ECBA4B-BqUpdFeRzlsEWLSQnudnhLP-yPQJ8_hKquMo,379537
|
@@ -35,9 +35,9 @@ pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCf
|
|
35
35
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
36
|
pywebexec/templates/index.html,sha256=2fEN8cggHBEd8-RamDFpnekVJtIbRembFSw0-1YEptc,2979
|
37
37
|
pywebexec/templates/popup.html,sha256=GT2jY7oOxpCaBaRl924QJWdFBmfSOP952T13d37R_pY,1506
|
38
|
-
pywebexec-1.7.
|
39
|
-
pywebexec-1.7.
|
40
|
-
pywebexec-1.7.
|
41
|
-
pywebexec-1.7.
|
42
|
-
pywebexec-1.7.
|
43
|
-
pywebexec-1.7.
|
38
|
+
pywebexec-1.7.4.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
39
|
+
pywebexec-1.7.4.dist-info/METADATA,sha256=Cui8J6P2uQXqzD43UuHSypY2oQCrp3palZ34daD_Gfg,8122
|
40
|
+
pywebexec-1.7.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
41
|
+
pywebexec-1.7.4.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
42
|
+
pywebexec-1.7.4.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
43
|
+
pywebexec-1.7.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|