|
|
@ -7,6 +7,8 @@ var timeCheck = false; |
|
|
|
var ntpTimeCheck = false; |
|
|
|
var domainCheck = false; |
|
|
|
var httpsCheck = false; |
|
|
|
var websocketCheck = false; |
|
|
|
var httpResponseCheck = false; |
|
|
|
|
|
|
|
// ================================
|
|
|
|
// Date & Time Check
|
|
|
@ -76,18 +78,15 @@ async function generateSupportString(event, dj) { |
|
|
|
event.preventDefault(); |
|
|
|
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 += `* Web-vault version: v${dj.web_vault_version}\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 += "* Environment settings overridden: "; |
|
|
|
if (dj.overrides != "") { |
|
|
|
supportString += "true\n"; |
|
|
|
} else { |
|
|
|
supportString += "false\n"; |
|
|
|
} |
|
|
|
supportString += `* Database type: ${dj.db_type}\n`; |
|
|
|
supportString += `* Database version: ${dj.db_version}\n`; |
|
|
|
supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`; |
|
|
|
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; |
|
|
|
if (dj.ip_header_exists) { |
|
|
|
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 += `* Domain Configuration Check: ${domainCheck}\n`; |
|
|
|
supportString += `* HTTPS Check: ${httpsCheck}\n`; |
|
|
|
supportString += `* Database type: ${dj.db_type}\n`; |
|
|
|
supportString += `* Database version: ${dj.db_version}\n`; |
|
|
|
supportString += "* Clients used: \n"; |
|
|
|
supportString += "* Reverse proxy and version: \n"; |
|
|
|
supportString += "* Other relevant information: \n"; |
|
|
|
if (dj.enable_websocket) { |
|
|
|
supportString += `* Websocket Check: ${websocketCheck}\n`; |
|
|
|
} else { |
|
|
|
supportString += "* Websocket Check: disabled\n"; |
|
|
|
} |
|
|
|
supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; |
|
|
|
|
|
|
|
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { |
|
|
|
"headers": { "Accept": "application/json" } |
|
|
@ -113,10 +113,30 @@ async function generateSupportString(event, dj) { |
|
|
|
throw new Error(jsonResponse); |
|
|
|
} |
|
|
|
const configJson = await jsonResponse.json(); |
|
|
|
supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\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"; |
|
|
|
|
|
|
|
// 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`; |
|
|
|
} |
|
|
|
|
|
|
|
// 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").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-exists`), |
|
|
|
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) { |
|
|
|
// Time check
|
|
|
|
document.getElementById("time-browser-string").textContent = browserUTC; |
|
|
@ -225,6 +401,12 @@ function init(dj) { |
|
|
|
|
|
|
|
// DNS Check
|
|
|
|
checkDns(dj.dns_resolved); |
|
|
|
|
|
|
|
checkHttpResponse(); |
|
|
|
|
|
|
|
if (dj.enable_websocket) { |
|
|
|
checkWebsocketConnection(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// onLoad events
|
|
|
|