diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index f5ca18449fe1..b541eccca1fe 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -313,6 +313,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp if (!DefaultForgotPasswordAPIAuthenticatorCmd.APINAME.equalsIgnoreCase(command) || StringUtils.isNotBlank(username)) { LOGGER.debug("Authentication failure: {}", e.getMessage()); } + } catch (RuntimeException e) { + if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API) { + invalidateHttpSession(session, "invalidating session after unexpected login failure"); + } + throw e; } if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) { @@ -426,6 +431,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp } catch (final Exception ex) { LOGGER.error("unknown exception writing api response", ex); auditTrailSb.append(" unknown exception writing api response"); + if (!resp.isCommitted()) { + final String serializedResponseText = apiServer.getSerializedApiError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Internal error processing the request", params, responseType); + HttpUtils.writeHttpResponse(resp, serializedResponseText, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseType, ApiServer.JSONContentType.value()); + } } finally { ACCESSLOGGER.info(auditTrailSb.toString()); if (LOGGER.isDebugEnabled()) { diff --git a/server/src/test/java/com/cloud/api/ApiServletTest.java b/server/src/test/java/com/cloud/api/ApiServletTest.java index 749533625551..b51d62afe18d 100644 --- a/server/src/test/java/com/cloud/api/ApiServletTest.java +++ b/server/src/test/java/com/cloud/api/ApiServletTest.java @@ -318,6 +318,53 @@ public void processRequestInContextLogin() throws UnknownHostException { Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class)); } + @SuppressWarnings("unchecked") + @Test + public void processRequestInContextLoginUnexpectedExceptionInvalidatesSessionAndReturnsError() throws UnknownHostException { + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getSession(false)).thenReturn(null); + Mockito.when(request.getSession(true)).thenReturn(session); + HashMap params = new HashMap(); + params.put(ApiConstants.COMMAND, new String[] { "login" }); + params.put(ApiConstants.USERNAME, new String[] { "TEST" }); + params.put(ApiConstants.PASSWORD, new String[] { "TEST-PWD" }); + Mockito.when(request.getParameterMap()).thenReturn(params); + Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.LOGIN_API); + Mockito.when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), + Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))) + .thenThrow(new NullPointerException("test")); + Mockito.when(apiServer.getSerializedApiError(Mockito.eq(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), Mockito.anyString(), Mockito.anyMap(), Mockito.anyString())) + .thenReturn("{\"errorresponse\":{}}"); + + servlet.processRequestInContext(request, response); + + Mockito.verify(session).invalidate(); + Mockito.verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString()); + } + + @SuppressWarnings("unchecked") + @Test + public void processRequestInContextNonLoginUnexpectedExceptionKeepsSessionAndReturnsError() throws UnknownHostException { + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getSession(Mockito.anyBoolean())).thenReturn(session); + HashMap params = new HashMap(); + params.put(ApiConstants.COMMAND, new String[] { "login" }); + Mockito.when(request.getParameterMap()).thenReturn(params); + Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.READONLY_API); + Mockito.when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), + Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))) + .thenThrow(new NullPointerException("test")); + Mockito.when(apiServer.getSerializedApiError(Mockito.eq(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), Mockito.anyString(), Mockito.anyMap(), Mockito.anyString())) + .thenReturn("{\"errorresponse\":{}}"); + + servlet.processRequestInContext(request, response); + + Mockito.verify(session, Mockito.never()).invalidate(); + Mockito.verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString()); + } + @Test public void getClientAddressWithXForwardedFor() throws UnknownHostException { setConfigValue("proxy.header.verify", "true"); diff --git a/ui/src/main.js b/ui/src/main.js index 7441f8010865..9f6cbb5bba6f 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -68,8 +68,22 @@ vueApp.use(backupUtilPlugin) vueApp.use(extensions) vueApp.use(directives) +vueProps.$localStorage.keys().forEach(key => { + try { + vueProps.$localStorage.get(key) + } catch (e) { + console.warn('Removing unreadable local storage entry: ' + key) + vueProps.$localStorage.remove(key) + } +}) + +let configLoaded = false + const renderError = (err) => { console.error('Fatal error during app initialization: ', err) + const details = configLoaded + ? 'The application failed to start: ' + err + '. Check the browser console for more information.' + : 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.' const ErrorComponent = { render: () => h( 'div', @@ -81,7 +95,7 @@ const renderError = (err) => { h('summary', { style: 'cursor: pointer;' }, 'Technical details'), h('pre', { style: 'text-align: left; display: inline-block; margin-top: 10px;' - }, 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.') + }, details) ]) ] ) @@ -97,6 +111,7 @@ fetch('config.json?ts=' + Date.now()) return response.json() }) .then(async config => { + configLoaded = true vueProps.$config = config let baseUrl = config.apiBase if (config.multipleServer) { diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 4103ff920d63..87a48dd93967 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -64,7 +64,10 @@ function setLoginCookies (result) { } function processLoginResponse (commit, response) { - const result = response.loginresponse || {} + const result = response?.loginresponse || {} + if (!result.sessionkey) { + throw new Error('Login response not valid.') + } setLoginCookies(result) vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000) commit('SET_TOKEN', result.sessionkey)