|
|
|
@@ -47,7 +47,10 @@ Pages:
|
|
|
|
|
/metrics/history
|
|
|
|
|
the metrics the metrics service keeps, for a view: hour, day, month or year (see
|
|
|
|
|
metrics_history())
|
|
|
|
|
/logs the last lines of the journal of the router's units and of the files web.log_files lists
|
|
|
|
|
/metrics/settings
|
|
|
|
|
the settings of the metrics service: show and change, applied by vpn-router-setup (see
|
|
|
|
|
metrics_settings())
|
|
|
|
|
/logs the last lines of the journal of the router's units and of the files web.log_files lists
|
|
|
|
|
(see logs())
|
|
|
|
|
/sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()),
|
|
|
|
|
and the files of each: list, download, upload, extract a zip archive, create a folder, rename and
|
|
|
|
@@ -62,7 +65,7 @@ Pages:
|
|
|
|
|
loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts
|
|
|
|
|
towards the limit of /auth calls
|
|
|
|
|
|
|
|
|
|
/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
|
|
|
|
|
/status/full, /config, /metrics/history, /metrics/settings, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
|
|
|
|
|
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
|
|
|
|
|
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
|
|
|
|
|
token of vpn-router-token has the admin claim and names no user; it may call every path but
|
|
|
|
@@ -1351,6 +1354,39 @@ def metrics_history(view):
|
|
|
|
|
return 500, {'error': str(exc.code)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def metrics_shown(settings):
|
|
|
|
|
"""The metrics section as /metrics/settings shows it: its settings but otlp_password, with otlp_password_set."""
|
|
|
|
|
return {**{option: value for option, value in settings.items() if option != 'otlp_password'},
|
|
|
|
|
'otlp_password_set': bool(settings['otlp_password'])}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def metrics_settings(method, body):
|
|
|
|
|
"""Answer a /metrics/settings request: the HTTP status and the JSON document.
|
|
|
|
|
|
|
|
|
|
GET /metrics/settings {"settings"}: the metrics section without otlp_password, with otlp_password_set
|
|
|
|
|
PUT /metrics/settings change any of the settings of the metrics section; an empty or absent otlp_password keeps
|
|
|
|
|
the current secret, and an empty otlp_username removes it
|
|
|
|
|
|
|
|
|
|
A change is applied by restarting vpn-router-setup without waiting for it.
|
|
|
|
|
"""
|
|
|
|
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
|
|
|
|
return 400, {'error': 'the body must be a JSON object'}
|
|
|
|
|
if method in ('GET', 'HEAD'):
|
|
|
|
|
try:
|
|
|
|
|
return 200, {'settings': metrics_shown(vpnrouter.load_config()['metrics'])}
|
|
|
|
|
except SystemExit as exc:
|
|
|
|
|
return 500, {'error': str(exc.code)}
|
|
|
|
|
if method != 'PUT':
|
|
|
|
|
return 404, {'error': f'no route {method} /metrics/settings'}
|
|
|
|
|
body = dict(body or {})
|
|
|
|
|
if not body.get('otlp_password'):
|
|
|
|
|
body.pop('otlp_password', None)
|
|
|
|
|
if body.get('otlp_username') == '':
|
|
|
|
|
body['otlp_password'] = ''
|
|
|
|
|
changed = section_change('metrics', body)
|
|
|
|
|
return changed if isinstance(changed, tuple) else (200, {'settings': metrics_shown(changed)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def changes_admins(connection, method, parts, body):
|
|
|
|
|
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
|
|
|
|
|
removes a member of it."""
|
|
|
|
@@ -1819,7 +1855,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
|
|
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup',
|
|
|
|
|
'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns')
|
|
|
|
|
if path == f'/{name}' or path.startswith(f'/{name}/')), None)
|
|
|
|
|
protected = path in ('/status/full', '/config', '/metrics/history', '/postgresql') or directory is not None
|
|
|
|
|
protected = path in ('/status/full', '/config', '/metrics/history', '/metrics/settings', '/postgresql') or \
|
|
|
|
|
directory is not None
|
|
|
|
|
signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout')
|
|
|
|
|
oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \
|
|
|
|
|
self.server.server_address[0] == vpnrouter_net.LOOPBACK
|
|
|
|
@@ -1861,6 +1898,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
|
|
query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)
|
|
|
|
|
code, document = metrics_history(query.get('range', ['hour'])[0])
|
|
|
|
|
body, kind = json.dumps(document).encode() + b'\n', 'application/json'
|
|
|
|
|
elif path == '/metrics/settings':
|
|
|
|
|
code, document = metrics_settings(self.command, payload)
|
|
|
|
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
|
|
|
|
elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'):
|
|
|
|
|
return self.shell(claims.get('sub'))
|
|
|
|
|
elif directory == 'shell':
|
|
|
|
|