@ -7,6 +7,8 @@ var timeCheck = false;
var ntpTimeCheck = false ;
var ntpTimeCheck = false ;
var domainCheck = false ;
var domainCheck = false ;
var httpsCheck = false ;
var httpsCheck = false ;
var websocketCheck = false ;
var httpResponseCheck = false ;
// ================================
// ================================
// Date & Time Check
// Date & Time Check
@ -76,18 +78,15 @@ async function generateSupportString(event, dj) {
event . preventDefault ( ) ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
event . stopPropagation ( ) ;
let supportString = "### Your environment (Generated via diagnostics page)\n ";
let supportString = "### Your environment (Generated via diagnostics page)\n \n ";
supportString += ` * Vaultwarden version: v ${ dj . current _release } \n ` ;
supportString += ` * Vaultwarden version: v ${ dj . current _release } \n ` ;
supportString += ` * Web-vault version: v ${ dj . web _vault _version } \n ` ;
supportString += ` * Web-vault version: v ${ dj . web _vault _version } \n ` ;
supportString += ` * OS/Arch: ${ dj . host _os } / ${ dj . host _arch } \n ` ;
supportString += ` * OS/Arch: ${ dj . host _os } / ${ dj . host _arch } \n ` ;
supportString += ` * Running within a container: ${ dj . running _within _container } (Base: ${ dj . container _base _image } ) \n ` ;
supportString += ` * Running within a container: ${ dj . running _within _container } (Base: ${ dj . container _base _image } ) \n ` ;
supportString += "* Environment settings overridden: " ;
supportString += ` * Database type: ${ dj . db _type } \n ` ;
if ( dj . overrides != "" ) {
supportString += ` * Database version: ${ dj . db _version } \n ` ;
supportString += "true\n" ;
supportString += ` * Environment settings overridden!: ${ dj . overrides !== "" } \n ` ;
} else {
supportString += "false\n" ;
}
supportString += ` * Uses a reverse proxy: ${ dj . ip _header _exists } \n ` ;
supportString += ` * Uses a reverse proxy: ${ dj . ip _header _exists } \n ` ;
if ( dj . ip _header _exists ) {
if ( dj . ip _header _exists ) {
supportString += ` * IP Header check: ${ dj . ip _header _match } ( ${ dj . ip _header _name } ) \n ` ;
supportString += ` * IP Header check: ${ dj . ip _header _match } ( ${ dj . ip _header _name } ) \n ` ;
@ -99,11 +98,12 @@ async function generateSupportString(event, dj) {
supportString += ` * Server/NTP Time Check: ${ ntpTimeCheck } \n ` ;
supportString += ` * Server/NTP Time Check: ${ ntpTimeCheck } \n ` ;
supportString += ` * Domain Configuration Check: ${ domainCheck } \n ` ;
supportString += ` * Domain Configuration Check: ${ domainCheck } \n ` ;
supportString += ` * HTTPS Check: ${ httpsCheck } \n ` ;
supportString += ` * HTTPS Check: ${ httpsCheck } \n ` ;
supportString += ` * Database type: ${ dj . db _type } \n ` ;
if ( dj . enable _websocket ) {
supportString += ` * Database version: ${ dj . db _version } \n ` ;
supportString += ` * Websocket Check: ${ websocketCheck } \n ` ;
supportString += "* Clients used: \n" ;
} else {
supportString += "* Reverse proxy and version: \n" ;
supportString += "* Websocket Check: disabled\n" ;
supportString += "* Other relevant information: \n" ;
}
supportString += ` * HTTP Response Checks: ${ httpResponseCheck } \n ` ;
const jsonResponse = await fetch ( ` ${ BASE _URL } /admin/diagnostics/config ` , {
const jsonResponse = await fetch ( ` ${ BASE _URL } /admin/diagnostics/config ` , {
"headers" : { "Accept" : "application/json" }
"headers" : { "Accept" : "application/json" }
@ -113,10 +113,30 @@ async function generateSupportString(event, dj) {
throw new Error ( jsonResponse ) ;
throw new Error ( jsonResponse ) ;
}
}
const configJson = await jsonResponse . json ( ) ;
const configJson = await jsonResponse . json ( ) ;
supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n" ;
// Start Config and Details section within a details block which is collapsed by default
supportString += "\n### Config & Details (Generated via diagnostics page)\n\n" ;
supportString += "<details><summary>Show Config & Details</summary>\n" ;
// Add overrides if they exists
if ( dj . overrides != "" ) {
supportString += ` \n **Environment settings which are overridden:** ${ dj . overrides } \n ` ;
supportString += ` \n **Environment settings which are overridden:** ${ dj . overrides } \n ` ;
supportString += "\n\n```json\n" + JSON . stringify ( configJson , undefined , 2 ) + "\n```\n</details>\n" ;
}
// Add http response check messages if they exists
if ( httpResponseCheck === false ) {
supportString += "\n**Failed HTTP Checks:**\n" ;
// We use `innerText` here since that will convert <br> into new-lines
supportString += "\n```yaml\n" + document . getElementById ( "http-response-errors" ) . innerText . trim ( ) + "\n```\n" ;
}
// Add the current config in json form
supportString += "\n**Config:**\n" ;
supportString += "\n```json\n" + JSON . stringify ( configJson , undefined , 2 ) + "\n```\n" ;
supportString += "\n</details>\n" ;
// Add the support string to the textbox so it can be viewed and copied
document . getElementById ( "support-string" ) . textContent = supportString ;
document . getElementById ( "support-string" ) . textContent = supportString ;
document . getElementById ( "support-string" ) . classList . remove ( "d-none" ) ;
document . getElementById ( "support-string" ) . classList . remove ( "d-none" ) ;
document . getElementById ( "copy-support" ) . classList . remove ( "d-none" ) ;
document . getElementById ( "copy-support" ) . classList . remove ( "d-none" ) ;
@ -199,6 +219,162 @@ function checkDns(dns_resolved) {
}
}
}
}
async function fetchCheckUrl ( url ) {
try {
const response = await fetch ( url ) ;
return { headers : response . headers , status : response . status , text : await response . text ( ) } ;
} catch ( error ) {
console . error ( ` Error fetching ${ url } : ${ error } ` ) ;
return { error } ;
}
}
function checkSecurityHeaders ( headers , omit ) {
let securityHeaders = {
"x-frame-options" : [ "SAMEORIGIN" ] ,
"x-content-type-options" : [ "nosniff" ] ,
"referrer-policy" : [ "same-origin" ] ,
"x-xss-protection" : [ "0" ] ,
"x-robots-tag" : [ "noindex" , "nofollow" ] ,
"content-security-policy" : [
"default-src 'self'" ,
"base-uri 'self'" ,
"form-action 'self'" ,
"object-src 'self' blob:" ,
"script-src 'self' 'wasm-unsafe-eval'" ,
"style-src 'self' 'unsafe-inline'" ,
"child-src 'self' https://*.duosecurity.com https://*.duofederal.com" ,
"frame-src 'self' https://*.duosecurity.com https://*.duofederal.com" ,
"frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://*" ,
"img-src 'self' data: https://haveibeenpwned.com" ,
"connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory https://app.simplelogin.io/api/ https://app.addy.io/api/ https://api.fastmail.com/ https://api.forwardemail.net" ,
]
} ;
let messages = [ ] ;
for ( let header in securityHeaders ) {
// Skip some headers for specific endpoints if needed
if ( typeof omit === "object" && omit . includes ( header ) === true ) {
continue ;
}
// If the header exists, check if the contents matches what we expect it to be
let headerValue = headers . get ( header ) ;
if ( headerValue !== null ) {
securityHeaders [ header ] . forEach ( ( expectedValue ) => {
if ( headerValue . indexOf ( expectedValue ) === - 1 ) {
messages . push ( ` ' ${ header } ' does not contain ' ${ expectedValue } ' ` ) ;
}
} ) ;
} else {
messages . push ( ` ' ${ header } ' is missing! ` ) ;
}
}
return messages ;
}
async function checkHttpResponse ( ) {
const [ apiConfig , webauthnConnector , notFound , notFoundApi , badRequest , unauthorized , forbidden ] = await Promise . all ( [
fetchCheckUrl ( ` ${ BASE _URL } /api/config ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /webauthn-connector.html ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /admin/does-not-exist ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /admin/diagnostics/http?code=404 ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /admin/diagnostics/http?code=400 ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /admin/diagnostics/http?code=401 ` ) ,
fetchCheckUrl ( ` ${ BASE _URL } /admin/diagnostics/http?code=403 ` ) ,
] ) ;
const respErrorElm = document . getElementById ( "http-response-errors" ) ;
// Check and validate the default API header responses
let apiErrors = checkSecurityHeaders ( apiConfig . headers ) ;
if ( apiErrors . length >= 1 ) {
respErrorElm . innerHTML += "<b>API calls:</b><br>" ;
apiErrors . forEach ( ( errMsg ) => {
respErrorElm . innerHTML += ` <b>Header:</b> ${ errMsg } <br> ` ;
} ) ;
}
// Check the special `-connector.html` headers, these should have some headers omitted.
const omitConnectorHeaders = [ "x-frame-options" , "content-security-policy" ] ;
let connectorErrors = checkSecurityHeaders ( webauthnConnector . headers , omitConnectorHeaders ) ;
omitConnectorHeaders . forEach ( ( header ) => {
if ( webauthnConnector . headers . get ( header ) !== null ) {
connectorErrors . push ( ` ' ${ header } ' is present while it should not ` ) ;
}
} ) ;
if ( connectorErrors . length >= 1 ) {
respErrorElm . innerHTML += "<b>2FA Connector calls:</b><br>" ;
connectorErrors . forEach ( ( errMsg ) => {
respErrorElm . innerHTML += ` <b>Header:</b> ${ errMsg } <br> ` ;
} ) ;
}
// Check specific error code responses if they are not re-written by a reverse proxy
let responseErrors = [ ] ;
if ( notFound . status !== 404 || notFound . text . indexOf ( "return to the web-vault" ) === - 1 ) {
responseErrors . push ( "404 (Not Found) HTML is invalid" ) ;
}
if ( notFoundApi . status !== 404 || notFoundApi . text . indexOf ( "\"message\":\"Testing error 404 response\"," ) === - 1 ) {
responseErrors . push ( "404 (Not Found) JSON is invalid" ) ;
}
if ( badRequest . status !== 400 || badRequest . text . indexOf ( "\"message\":\"Testing error 400 response\"," ) === - 1 ) {
responseErrors . push ( "400 (Bad Request) is invalid" ) ;
}
if ( unauthorized . status !== 401 || unauthorized . text . indexOf ( "\"message\":\"Testing error 401 response\"," ) === - 1 ) {
responseErrors . push ( "401 (Unauthorized) is invalid" ) ;
}
if ( forbidden . status !== 403 || forbidden . text . indexOf ( "\"message\":\"Testing error 403 response\"," ) === - 1 ) {
responseErrors . push ( "403 (Forbidden) is invalid" ) ;
}
if ( responseErrors . length >= 1 ) {
respErrorElm . innerHTML += "<b>HTTP error responses:</b><br>" ;
responseErrors . forEach ( ( errMsg ) => {
respErrorElm . innerHTML += ` <b>Response to:</b> ${ errMsg } <br> ` ;
} ) ;
}
if ( responseErrors . length >= 1 || connectorErrors . length >= 1 || apiErrors . length >= 1 ) {
document . getElementById ( "http-response-warning" ) . classList . remove ( "d-none" ) ;
} else {
httpResponseCheck = true ;
document . getElementById ( "http-response-success" ) . classList . remove ( "d-none" ) ;
}
}
async function fetchWsUrl ( wsUrl ) {
return new Promise ( ( resolve , reject ) => {
try {
const ws = new WebSocket ( wsUrl ) ;
ws . onopen = ( ) => {
ws . close ( ) ;
resolve ( true ) ;
} ;
ws . onerror = ( ) => {
reject ( false ) ;
} ;
} catch ( _ ) {
reject ( false ) ;
}
} ) ;
}
async function checkWebsocketConnection ( ) {
// Test Websocket connections via the anonymous (login with device) connection
const isConnected = await fetchWsUrl ( ` ${ BASE _URL } /notifications/anonymous-hub?token=admin-diagnostics ` ) . catch ( ( ) => false ) ;
if ( isConnected ) {
websocketCheck = true ;
document . getElementById ( "websocket-success" ) . classList . remove ( "d-none" ) ;
} else {
document . getElementById ( "websocket-error" ) . classList . remove ( "d-none" ) ;
}
}
function init ( dj ) {
function init ( dj ) {
// Time check
// Time check
document . getElementById ( "time-browser-string" ) . textContent = browserUTC ;
document . getElementById ( "time-browser-string" ) . textContent = browserUTC ;
@ -225,6 +401,12 @@ function init(dj) {
// DNS Check
// DNS Check
checkDns ( dj . dns _resolved ) ;
checkDns ( dj . dns _resolved ) ;
checkHttpResponse ( ) ;
if ( dj . enable _websocket ) {
checkWebsocketConnection ( ) ;
}
}
}
// onLoad events
// onLoad events