fix: a shell opens without the password and code again while the user's last use of a shell is within web.shell_auth_timeout minutes.
Release / release (push) Successful in 13s

This commit is contained in:
slawek committed 2026-10-26 07:56:23 +01:00
1 parent a1f314d8c1
commit 91cc64d6a6
6 files changed
+49 -12

No files matched your search

+22 -6
View File
@@ -38,8 +38,9 @@ Pages:
the DER certificate and CRL of the root (root) and of each issuing CA, which the
certificates name in their AIA and CRL distribution points; no authentication
/shell while web.shell is true, a login shell of the user's console account on a terminal, for
a member of admins whose console login is on: POST signs the
user in again for a one-time ticket (see shell_ticket()), GET with it is a WebSocket
a member of admins whose console login is on: POST signs the user in again, unless
the user used a shell within web.shell_auth_timeout minutes, for a one-time ticket
(see shell_ticket()), GET with it is a WebSocket
that carries the terminal (see Handler.shell())
/auth sign-in with a user's password and TOTP code, refresh and sign-out (see auth())
/metrics/history
@@ -189,6 +190,9 @@ _auth_lock = threading.Lock()
_shell_tickets = {}
_shell_lock = threading.Lock()
#: The last use of a shell of each user, a sign-in of POST /shell or a keystroke: user -> monotonic time.
_shell_used = {}
def read_json(path):
"""The object in a JSON file, or {} when the file is missing or holds no object."""
@@ -1105,19 +1109,28 @@ def shell_ticket(name, body):
"""Answer POST /shell: the HTTP status and the JSON document.
The user named by the token signs in again with {"password", "code"?}; a wrong password or code is 403 and counts
as a failed login. The answer is {"ticket", "expires_in"}: a ticket that opens one shell (GET /shell) for the
same user within SHELL_TICKET_SECONDS. 403 while web.shell is false or the user's console login is not on.
as a failed login. Without a password, the user is not asked to sign in while its last use of a shell (see
_shell_used) is less than web.shell_auth_timeout minutes ago. The answer is {"ticket", "expires_in"}: a ticket that
opens one shell (GET /shell) for the same user within SHELL_TICKET_SECONDS, or a null ticket when the user must
sign in. 403 while web.shell is false or the user's console login is not on.
"""
if not isinstance(name, str):
return 403, {'error': 'the shell needs a token of a signed-in user'}
if not isinstance(body, dict):
return 400, {'error': 'the body must be a JSON object'}
try:
if not vpnrouter.load_config()['web']['shell']:
options = vpnrouter.load_config()['web']
if not options['shell']:
return 403, {'error': 'the shell is disabled; vpn-config-edit set web.shell true enables it'}
if console_account(name) is None:
return 403, {'error': f'console login is not enabled for {name}; vpn-router-user console sets it'}
vpnrouter_users.login(name, body.get('password'), body.get('code'))
if 'password' not in body:
with _shell_lock:
used = _shell_used.get(name)
if used is None or time.monotonic() - used >= options['shell_auth_timeout'] * 60:
return 200, {'ticket': None, 'expires_in': 0}
else:
vpnrouter_users.login(name, body.get('password'), body.get('code'))
except vpnrouter_users.Failure as failure:
if failure.status == 5:
return 403, {'error': 'the password or the code is wrong'}
@@ -1132,6 +1145,7 @@ def shell_ticket(name, body):
for unused in [key for key, (_, expires) in _shell_tickets.items() if expires <= now]:
del _shell_tickets[unused]
_shell_tickets[ticket] = (name, now + SHELL_TICKET_SECONDS)
_shell_used[name] = now
return 200, {'ticket': ticket, 'expires_in': SHELL_TICKET_SECONDS}
@@ -1336,6 +1350,8 @@ class Handler(BaseHTTPRequestHandler):
if opcode == BINARY:
os.write(master, payload)
typed[0] = time.monotonic()
with _shell_lock:
_shell_used[name] = typed[0]
elif opcode == TEXT:
size = json.loads(payload)
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', int(size['rows']), int(size['cols']), 0, 0))