Improve Gateway authentication and static HTTP fidelity
This commit is contained in:
1 parent
42561bfa34
commit
4ab9411aca
8 files changed
+167
-58
No files matched your search
@@ -55,7 +55,7 @@ English/German language JSON and gzip endpoints carry the default 14.1-29.63 per
|
||||
Translations are synthetic subsets, not original firmware files. If changing HONEYPOT_VERSION, also
|
||||
set HONEYPOT_ASSET_MTIME to that firmware's documented timestamp; metadata matching does not prove
|
||||
full firmware emulation. Gzip assets are returned with Content-Encoding and Last-Modified.
|
||||
Authentication XML includes credential requirements and a postback route; it grants no real access.
|
||||
Authentication XML includes credential requirements, state context, a postback route and error labels after credential submissions; it grants no real access. The browser requests the initial challenge and consumes the returned XML. Gateway CSS and JavaScript use local appliance-style paths; assets support ETag/Last-Modified revalidation and HEAD, without issuing session cookies. CSP reports are captured before a204 response; logout clears the cookie and redirects locally.
|
||||
`request_indicators` hunts header/body log markers and encoding hints without claiming a confirmed CVE.
|
||||
Bodies are inspected up to64KiB for these hints; complete accepted bodies remain captured independently.
|
||||
Run `python3 test_profile.py` for passive identification, gzip decoding/metadata, XML and capture checks.
|
||||
@@ -139,7 +139,7 @@ vulnerability exists, a payload executes, or a CVE is conclusively identified.
|
||||
| Gateway login pages | `/`, `/vpn/`, `/vpn/index.html`, `/vpn/tmindex.html`, `/logon/LogonPoint/index.html`, `/logon/LogonPoint/tmindex.html`, `/logon/LogonPoint/custom.html`, `/menu/neo/`. German login UI, canonical Citrix Gateway title, NSC_ cookie and Gateway-shaped response headers. |
|
||||
| Gateway static fingerprints | Explicit rdx/CTXS JavaScript routes, login.js/init.js, original favicon/logo/background assets and AccessGateway.ico alias. Only routes in STATIC are supported; arbitrary files under these directories return404. |
|
||||
| Language/version fingerprints | English/German JSON and `.json.gz` under `/vpn/js/rdx/core/lang/` and `/vpn/js/rdx/core/lang-ext/`; gzip filename, build timestamp, Content-Encoding and Last-Modified. Synthetic translation subset with the default14.1-29.63 metadata. |
|
||||
| Authentication XML | `/nf/auth/*` returns credential requirements, state context and local postback. This is a repeated login challenge, not LDAP, SAML, MFA or successful real authentication. |
|
||||
| Authentication XML | `/nf/auth/getAuthenticationRequirements.do` and `/nf/auth/doAuthentication.do` return credential requirements, state context and local postback; credential submissions add an error label. Unknown authentication endpoints return404. This is a repeated login challenge, not LDAP, SAML, MFA or successful real authentication. |
|
||||
| NITRO authentication | `/nitro/v1/config/login` and logout with synthetic session tokens. Resource access accepts a matching fake token, nonempty X-NITRO-USER/X-NITRO-PASS, or a Basic authorization prefix. These are decoy rules, not real user/account validation. |
|
||||
| NITRO resources | `/nitro/v1/config/<resource>[/<name>]` and `/nitro/v1/stat/<resource>[/<name>]`;1,493 config and176 statistics names in [nitro-resources.json](nitro-resources.json). Common system/network/Gateway/AAA/SSL/LB/binding records are populated; other recognized resources return empty collections or generic counters. |
|
||||
| NITRO operations | Config reads, fake POST/PUT/DELETE, enable/disable/rename, inert administrative-action acknowledgements, attrs, literal filter/args, counts, pagesize and pageno. Unknown resources, invalid input, unsupported methods and unauthenticated access return simulator errors. |
|
||||
|
||||
+57
-16
@@ -7,6 +7,7 @@ import collections
|
||||
import gzip
|
||||
import email.parser
|
||||
import email.policy
|
||||
import email.utils
|
||||
import hashlib
|
||||
import html
|
||||
import http.server
|
||||
@@ -51,21 +52,19 @@ STATIC = {
|
||||
'/logon/LogonPoint/receiver/images/common/icon_vpn.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
'/vpn/images/AccessGateway.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
'/favicon.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
# Static NetScaler Gateway scripts scanners look for; symbols match the real rdx/CTXS namespace.
|
||||
'/vpn/js/rdx.js': (b"window.CTXS=window.CTXS||{};window.rdx=window.rdx||{};"
|
||||
b"rdx.base_view=function(){};rdx.utils={get_view:function(){}};\n", JS),
|
||||
'/vpn/js/rdx/core/rdx.js': (b"window.rdx=window.rdx||{};rdx.base_view=rdx.base_view||function(){};"
|
||||
b"rdx.utils={get_view:function(){}};\n", JS),
|
||||
'/vpn/js/gateway_login_view.js': (b"rdx.gateway_login_view=function(){return new rdx.base_view();};\n", JS),
|
||||
'/vpn/js/gateway_login_form_view.js': (b"rdx.gateway_login_form_view=function(){return rdx.utils.get_view();};\n", JS),
|
||||
'/logon/LogonPoint/init.js': (b"var _ctxstxt_CitrixCopyright='Copyright (c) Citrix Systems, Inc.';"
|
||||
b"window.CTXS=window.CTXS||{};CTXS.init=function(){};\n", JS),
|
||||
'/logon/LogonPoint/receiver/js/ctxs.core.min.js': (b"window.CTXS=window.CTXS||{};CTXS.core={version:'1.0'};\n", JS),
|
||||
'/vpn/js/rdx/core/css/rdx.css': ((UI / 'rdx.css').read_bytes(), 'text/css; charset=utf-8'),
|
||||
'/vpn/js/rdx.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
'/vpn/js/rdx/core/rdx.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
'/vpn/js/gateway_login_view.js': (b"rdx.gateway_login_view=function(){return new rdx.base_view(document.querySelector('main'));};\n", JS),
|
||||
'/vpn/js/gateway_login_form_view.js': (b"rdx.gateway_login_form_view=function(){return new rdx.base_view(document.getElementById('login-form'));};\n", JS),
|
||||
'/logon/LogonPoint/init.js': (b"window.CTXS=window.CTXS||{};CTXS.init=function(){return rdx.utils.get_view('login-form');};\n", JS),
|
||||
'/logon/LogonPoint/receiver/js/ctxs.core.min.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
}
|
||||
|
||||
# Synthetic translations; metadata matches the default 14.1-29.63 persona.
|
||||
# Source: Fox-IT citrix-netscaler-triage version timestamp catalog.
|
||||
ASSET_MTIME = env_int('HONEYPOT_ASSET_MTIME', 1723114781)
|
||||
UI_MTIME = int(max(p.stat().st_mtime for p in UI.iterdir() if p.is_file()))
|
||||
for language in ('en', 'de'):
|
||||
translation = {'LogOn': 'Log On' if language == 'en' else 'Anmelden',
|
||||
'UserName': 'User name' if language == 'en' else 'Benutzername',
|
||||
@@ -681,13 +680,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
data, mime = STATIC[url.path]
|
||||
if url.path.endswith('.gz'):
|
||||
extra_headers.append(('Content-Encoding', 'gzip'))
|
||||
extra_headers.append(('Last-Modified', self.date_time_string(ASSET_MTIME)))
|
||||
elif url.path.startswith('/nf/auth/'):
|
||||
asset_mtime = ASSET_MTIME if '/lang/' in url.path or '/lang-ext/' in url.path else UI_MTIME
|
||||
extra_headers.append(('Last-Modified', self.date_time_string(asset_mtime)))
|
||||
elif url.path in ('/nf/auth/getAuthenticationRequirements.do', '/nf/auth/doAuthentication.do'):
|
||||
data = ('<AuthenticateResponse xmlns="http://citrix.com/authentication/response/1">'
|
||||
'<Status>success</Status><Result>more-info</Result>'
|
||||
'<StateContext>' + html.escape(state.sid) + '</StateContext>'
|
||||
'<AuthenticationRequirements><PostBack>/nf/auth/doAuthentication.do</PostBack>'
|
||||
'<Requirements>'
|
||||
'<Requirements>' + (
|
||||
'<Requirement><Credential><Type>none</Type></Credential>'
|
||||
'<Label><Text>Incorrect user name or password.</Text><Type>error</Type></Label>'
|
||||
'<Input/></Requirement>' if url.path == '/nf/auth/doAuthentication.do'
|
||||
and self.command == 'POST' and auth_fields.intersection({'login', 'passwd'}) else '') +
|
||||
'<Requirement><Credential><ID>login</ID><Type>username</Type></Credential>'
|
||||
'<Label><Text>User name:</Text><Type>plain</Type></Label>'
|
||||
'<Input><Text><InitialValue/><Constraint>.+</Constraint></Text></Input></Requirement>'
|
||||
@@ -699,6 +703,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
'</Requirements></AuthenticationRequirements>'
|
||||
'</AuthenticateResponse>').encode()
|
||||
mime = 'application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8'
|
||||
elif url.path == '/nscsp_violation/report_uri':
|
||||
code, data, mime = 204, b'', 'text/plain; charset=utf-8'
|
||||
elif url.path in ('/cgi/logout', '/vpn/logout.html'):
|
||||
code, data = 302, b''
|
||||
extra_headers = [('Location', '/vpn/index.html'),
|
||||
('Set-Cookie', 'NSC_AAAC=; Path=/; Max-Age=0; Secure; HttpOnly')]
|
||||
elif url.path.startswith('/nitro/'):
|
||||
code, result, extra_headers = nitro_response(state, self.command, url, body, self.headers)
|
||||
data, mime = json.dumps(result).encode(), 'application/json'
|
||||
@@ -715,22 +725,53 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.event('virtual_file_http_read', path=key, content=data)
|
||||
else:
|
||||
code, data = 404, b'Not Found'
|
||||
# Capture and ingest precede protocol responses, including rejected methods.
|
||||
if url.path in STATIC or url.path in LOGIN_PATHS:
|
||||
allowed = ('GET', 'HEAD')
|
||||
elif url.path == '/nf/auth/doAuthentication.do' or url.path == '/nscsp_violation/report_uri':
|
||||
allowed = ('POST',)
|
||||
elif url.path == '/nf/auth/getAuthenticationRequirements.do':
|
||||
allowed = ('GET', 'HEAD', 'POST')
|
||||
else:
|
||||
allowed = None
|
||||
if allowed and self.command not in allowed:
|
||||
code, data, mime = 405, b'Method Not Allowed', 'text/plain; charset=utf-8'
|
||||
extra_headers = [('Allow', ', '.join(allowed))]
|
||||
if url.path in STATIC and code == 200:
|
||||
etag = '"' + hashlib.sha256(data).hexdigest() + '"'
|
||||
extra_headers.append(('ETag', etag))
|
||||
matches = self.headers.get('If-None-Match')
|
||||
not_modified = matches is not None and any(
|
||||
tag.strip().removeprefix('W/') in ('*', etag) for tag in matches.split(','))
|
||||
if matches is None and self.headers.get('If-Modified-Since'):
|
||||
try:
|
||||
not_modified = email.utils.parsedate_to_datetime(
|
||||
self.headers['If-Modified-Since']).timestamp() >= asset_mtime
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
pass
|
||||
if not_modified:
|
||||
code, data = 304, b''
|
||||
if vm_read_path:
|
||||
result = engine.backend.read(vm_read_path, self.context)
|
||||
if result is not None:
|
||||
code, data, mime = 200, result, 'text/plain; charset=utf-8'
|
||||
# Headers shaped like the real NetScaler Gateway AAA response; HTTP/1.1 keep-alive is default.
|
||||
headers = [('Content-Type', mime), ('Content-Length', str(len(data))),
|
||||
('Cache-Control', 'no-cache, no-store, must-revalidate'),
|
||||
headers = [('Content-Type', mime),
|
||||
('Cache-Control', 'public, max-age=0, must-revalidate' if url.path in STATIC
|
||||
and code in (200, 304) else 'no-cache, no-store, must-revalidate'),
|
||||
('Pragma', 'no-cache'), ('Expires', '0'),
|
||||
('X-Frame-Options', 'SAMEORIGIN'), ('X-Content-Type-Options', 'nosniff'),
|
||||
('X-XSS-Protection', '1; mode=block'),
|
||||
('Set-Cookie', f'NSC_AAAC={state.sid}; Path=/; Secure; HttpOnly'),
|
||||
('Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data:; font-src 'self'; frame-ancestors 'self'; "
|
||||
"connect-src 'self' citrixng:// com.citrix.nsgclient:// nsgcepa://; "
|
||||
"report-uri /nscsp_violation/report_uri")]
|
||||
if code not in (204, 304):
|
||||
headers.append(('Content-Length', str(len(data))))
|
||||
if url.path not in STATIC and url.path != '/nscsp_violation/report_uri' and not any(
|
||||
key == 'Set-Cookie' for key, _ in extra_headers):
|
||||
headers.append(('Set-Cookie', f'NSC_AAAC={state.sid}; Path=/; Secure; HttpOnly'))
|
||||
headers.extend(extra_headers)
|
||||
self.event('response', status=code, headers=headers, content=data,
|
||||
body_suppressed=self.command == 'HEAD')
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ def main():
|
||||
wire_digest = hashlib.sha256(raw).hexdigest()
|
||||
# Wait for finally-recorded evidence after the response has closed.
|
||||
assert (pathlib.Path(workspace.name) / 'artifacts' / (wire_digest + '.bin')).read_bytes() == raw
|
||||
assert b'200' in raw_request(b'CUSTOM / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n').split(b'\r\n')[0]
|
||||
assert b'405' in raw_request(b'CUSTOM / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n').split(b'\r\n')[0]
|
||||
assert b'400' in raw_request(b'not-http\r\n')
|
||||
partial = b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 10\r\n\r\nabc'
|
||||
assert b'400' in raw_request(partial)
|
||||
|
||||
@@ -43,6 +43,40 @@ try:
|
||||
ns = {'c': 'http://citrix.com/authentication/response/1'}
|
||||
assert root.find('c:AuthenticationRequirements/c:PostBack', ns).text == '/nf/auth/doAuthentication.do'
|
||||
assert len(root.findall('.//c:Requirement', ns)) == 3
|
||||
status, headers, xml = request('/nf/auth/doAuthentication.do', 'POST',
|
||||
b'login=profile-check&passwd=synthetic',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'})
|
||||
root = ET.fromstring(xml)
|
||||
labels = root.findall('.//c:Label', ns)
|
||||
assert any(label.find('c:Type', ns).text == 'error' for label in labels)
|
||||
assert root.find('c:Result', ns).text == 'more-info' # Never grant authentication.
|
||||
assert request('/nf/auth/not-a-real-endpoint.do')[0] == 404
|
||||
assert request('/nf/auth/doAuthentication.do')[0] == 405
|
||||
assert b'<script>' not in html and b'<style>' not in html # Compatible with our CSP.
|
||||
for path in ('/vpn/js/rdx/core/css/rdx.css', '/vpn/js/rdx.js', '/logon/LogonPoint/login.js'):
|
||||
status, headers, asset = request(path)
|
||||
assert status == 200 and asset and 'Set-Cookie' not in headers
|
||||
assert 'no-store' not in headers['Cache-Control']
|
||||
assert request(path, headers={'If-None-Match': headers['ETag']})[0] == 304
|
||||
assert request(path, headers={'If-None-Match': 'W/' + headers['ETag']})[0] == 304
|
||||
assert request(path, headers={'If-Modified-Since': headers['Last-Modified']})[0] == 304
|
||||
assert request(path, headers={'If-Modified-Since': 'invalid'})[0] == 200
|
||||
assert request(path, headers={'If-None-Match': '"different"',
|
||||
'If-Modified-Since': headers['Last-Modified']})[0] == 200
|
||||
head = request(path, 'HEAD')
|
||||
assert head[0] == 200 and head[2] == b'' and int(head[1]['Content-Length']) == len(asset)
|
||||
before = len(events)
|
||||
status, rejected_headers, _ = request(path, 'POST', b'capture-only-check-method')
|
||||
assert status == 405 and rejected_headers['Allow'] == 'GET, HEAD'
|
||||
assert any(e['event'] == 'request_body' and e['evidence']['bytes'] == 25 for e in events[before:])
|
||||
before = len(events)
|
||||
assert request('/nscsp_violation/report_uri', 'POST', b'{}',
|
||||
{'Content-Type': 'application/csp-report'})[0] == 204
|
||||
assert any(e['event'] == 'request_body' and e['evidence']['bytes'] == 2 for e in events[before:])
|
||||
assert request('/nscsp_violation/report_uri')[0] == 405
|
||||
status, headers, body = request('/cgi/logout')
|
||||
assert status == 302 and headers['Location'] == '/vpn/index.html' and body == b''
|
||||
assert 'Max-Age=0' in headers['Set-Cookie']
|
||||
# Inert markers, no exploit syntax or commands. Original intake still correlated.
|
||||
status, _, _ = request('/unknown', 'POST', b'base64=inert-marker',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded', 'X_Test': 'NSPPE inert-marker'})
|
||||
|
||||
+2
-33
@@ -7,42 +7,10 @@
|
||||
<title class="_ctxstxt_NetscalerGateway">Citrix Gateway</title>
|
||||
<link rel="icon" href="/logon/LogonPoint/receiver/images/common/icon_vpn.ico" sizes="16x16 32x32 48x48 64x64" type="image/vnd.microsoft.icon">
|
||||
<link rel="shortcut icon" href="/logon/LogonPoint/receiver/images/common/icon_vpn.ico" type="image/vnd.microsoft.icon">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { min-height: 100%; margin: 0; }
|
||||
body { background: #302c41 url('/logon/LogonPoint/receiver/images/common/ReceiverFullScreenBackground.jpg') center / cover fixed;
|
||||
font-family: "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; color: #fff; }
|
||||
main { min-height: 100vh; display: flex; align-items: center; }
|
||||
.band { width: 100%; min-height: 350px; background: rgba(58, 54, 65, .86); display: flex; align-items: center; }
|
||||
.layout { width: min(1086px, calc(100% - 64px)); margin: auto; display: grid; grid-template-columns: 258px 570px; align-items: center; }
|
||||
.logo { width: 160px; height: auto; }
|
||||
h1 { font-size: 18px; font-weight: 400; margin: 0 0 18px; }
|
||||
.row { display: grid; grid-template-columns: 185px 1fr; align-items: center; margin-bottom: 6px; }
|
||||
label { color: #bdbbc2; font-size: 16px; }
|
||||
input { width: 100%; height: 44px; border: 1px solid #ddd; background: white; color: #111; border-radius: 0; padding: 8px 15px; font: inherit; }
|
||||
input:focus-visible { outline: 2px solid #79cce2; outline-offset: 2px; }
|
||||
button { display: block; width: calc(100% - 185px); margin: 26px 0 0 185px; height: 45px; border: 0; border-radius: 0;
|
||||
background: #49a0bb; color: white; font: 600 16px "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; cursor: pointer; }
|
||||
button:hover { background: #398aa4; }
|
||||
button:focus-visible { outline: 2px solid white; outline-offset: 3px; }
|
||||
button:disabled { cursor: wait; opacity: .75; }
|
||||
#message { margin: 12px 0 0 185px; font-size: 14px; color: #fff; }
|
||||
#message:empty { display: none; }
|
||||
@media (max-width: 950px) {
|
||||
.layout { width: min(570px, calc(100% - 48px)); display: block; padding: 40px 0; }
|
||||
.logo { margin-bottom: 28px; }
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.row { display: block; margin-bottom: 16px; }
|
||||
label { display: block; margin-bottom: 7px; }
|
||||
button { width: 100%; margin-left: 0; }
|
||||
#message { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/vpn/js/rdx/core/css/rdx.css">
|
||||
<script src="/vpn/js/rdx.js"></script>
|
||||
<script src="/vpn/js/gateway_login_view.js"></script>
|
||||
<script src="/logon/LogonPoint/login.js" defer></script>
|
||||
<script>var _ctxstxt_CitrixCopyright = 'Copyright (c) Citrix Systems, Inc.';</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
@@ -53,6 +21,7 @@
|
||||
<h1>Melden Sie sich an</h1>
|
||||
<div class="row"><label for="login">Benutzername:</label><input id="login" name="login" required autofocus autocomplete="off" spellcheck="false"></div>
|
||||
<div class="row"><label for="passwd">Kennwort:</label><input id="passwd" name="passwd" type="password" required autocomplete="off"></div>
|
||||
<input type="hidden" name="StateContext" value="">
|
||||
<input type="hidden" name="savecredentials" value="false">
|
||||
<button id="nsg-x1-logon-button" name="nsg-x1-logon-button" value="Log On" type="submit">Anmelden</button>
|
||||
<p id="message" role="status" aria-live="polite"></p>
|
||||
|
||||
+18
-6
@@ -2,19 +2,31 @@
|
||||
const form = document.getElementById('login-form');
|
||||
const button = document.getElementById('nsg-x1-logon-button');
|
||||
const message = document.getElementById('message');
|
||||
async function authenticate(path, body) {
|
||||
const response = await fetch(path, {
|
||||
method: 'POST', body, credentials: 'same-origin', redirect: 'error',
|
||||
headers: { Accept: 'application/vnd.citrix.authenticateresponse-1+xml' },
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
if (!response.ok) throw new Error('Request failed');
|
||||
const result = rdx.utils.authentication(await response.text());
|
||||
form.elements.StateContext.value = result.state;
|
||||
// Only the local authentication endpoint can receive form credentials.
|
||||
if (result.postback === '/nf/auth/doAuthentication.do') form.action = result.postback;
|
||||
return result;
|
||||
}
|
||||
const ready = authenticate('/nf/auth/getAuthenticationRequirements.do', new URLSearchParams())
|
||||
.catch(() => null);
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
button.disabled = true;
|
||||
message.textContent = '';
|
||||
try {
|
||||
await ready;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
body.set('nsg-x1-logon-button', 'Log On');
|
||||
const response = await fetch('/nf/auth/doAuthentication.do', {
|
||||
method: 'POST', body, credentials: 'same-origin', redirect: 'error',
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
if (!response.ok) throw new Error('Request failed');
|
||||
message.textContent = 'Anmeldung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.';
|
||||
const result = await authenticate('/nf/auth/doAuthentication.do', body);
|
||||
message.textContent = result.errors.join(' ') || 'Anmeldung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.';
|
||||
} catch {
|
||||
message.textContent = 'Die Anmeldung ist derzeit nicht möglich. Versuchen Sie es erneut.';
|
||||
} finally {
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, body { min-height: 100%; margin: 0; }
|
||||
body { background: #302c41 url('/logon/LogonPoint/receiver/images/common/ReceiverFullScreenBackground.jpg') center / cover fixed;
|
||||
font-family: "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; color: #fff; }
|
||||
main { min-height: 100vh; display: flex; align-items: center; }
|
||||
.band { width: 100%; min-height: 350px; background: rgba(58, 54, 65, .86); display: flex; align-items: center; }
|
||||
.layout { width: min(1086px, calc(100% - 64px)); margin: auto; display: grid; grid-template-columns: 258px 570px; align-items: center; }
|
||||
.logo { width: 160px; height: auto; }
|
||||
h1 { font-size: 18px; font-weight: 400; margin: 0 0 18px; }
|
||||
.row { display: grid; grid-template-columns: 185px 1fr; align-items: center; margin-bottom: 6px; }
|
||||
label { color: #bdbbc2; font-size: 16px; }
|
||||
input { width: 100%; height: 44px; border: 1px solid #ddd; background: white; color: #111; border-radius: 0; padding: 8px 15px; font: inherit; }
|
||||
input:focus-visible { outline: 2px solid #79cce2; outline-offset: 2px; }
|
||||
button { display: block; width: calc(100% - 185px); margin: 26px 0 0 185px; height: 45px; border: 0; border-radius: 0;
|
||||
background: #49a0bb; color: white; font: 600 16px "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; cursor: pointer; }
|
||||
button:hover { background: #398aa4; }
|
||||
button:focus-visible { outline: 2px solid white; outline-offset: 3px; }
|
||||
button:disabled { cursor: wait; opacity: .75; }
|
||||
#message { margin: 12px 0 0 185px; font-size: 14px; color: #fff; }
|
||||
#message:empty { display: none; }
|
||||
@media (max-width: 950px) {
|
||||
.layout { width: min(570px, calc(100% - 48px)); display: block; padding: 40px 0; }
|
||||
.logo { margin-bottom: 28px; }
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.row { display: block; margin-bottom: 16px; }
|
||||
label { display: block; margin-bottom: 7px; }
|
||||
button { width: 100%; margin-left: 0; }
|
||||
#message { margin-left: 0; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
var _ctxstxt_CitrixCopyright = 'Copyright (c) Citrix Systems, Inc.';
|
||||
window.CTXS = window.CTXS || {};
|
||||
window.rdx = window.rdx || {};
|
||||
rdx.base_view = function (element) { this.element = element; };
|
||||
rdx.base_view.prototype.show = function () { if (this.element) this.element.hidden = false; };
|
||||
rdx.base_view.prototype.hide = function () { if (this.element) this.element.hidden = true; };
|
||||
rdx.utils = {
|
||||
get_view: function (id) { return new rdx.base_view(document.getElementById(id || 'login-form')); },
|
||||
authentication: function (xml) {
|
||||
const document = new DOMParser().parseFromString(xml, 'application/xml');
|
||||
const ns = 'http://citrix.com/authentication/response/1';
|
||||
if (document.getElementsByTagName('parsererror').length ||
|
||||
document.documentElement.localName !== 'AuthenticateResponse' ||
|
||||
document.documentElement.namespaceURI !== ns) throw new Error('Invalid authentication response');
|
||||
const text = name => document.getElementsByTagNameNS(ns, name)[0]?.textContent || '';
|
||||
const errors = Array.from(document.getElementsByTagNameNS(ns, 'Label'))
|
||||
.filter(label => label.getElementsByTagNameNS(ns, 'Type')[0]?.textContent === 'error')
|
||||
.map(label => label.getElementsByTagNameNS(ns, 'Text')[0]?.textContent || '');
|
||||
return { state: text('StateContext'), postback: text('PostBack'), errors };
|
||||
}
|
||||
};
|
||||
CTXS.core = { getView: rdx.utils.get_view };
|
||||
Reference in new issue
Block a user