package org.ovirt.engine.ui.frontend.server.gwt; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; import javax.ejb.EJB; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.bind.annotation.adapters.HexBinaryAdapter; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.node.BooleanNode; import org.codehaus.jackson.node.ObjectNode; import org.ovirt.engine.core.branding.BrandingFilter; import org.ovirt.engine.core.branding.BrandingManager; import org.ovirt.engine.core.common.businessentities.aaa.DbUser; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigCommon; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.constants.SessionConstants; import org.ovirt.engine.core.common.interfaces.BackendLocal; import org.ovirt.engine.core.common.queries.ConfigurationValues; import org.ovirt.engine.core.common.queries.GetConfigurationValueParameters; import org.ovirt.engine.core.common.queries.VdcQueryParametersBase; import org.ovirt.engine.core.common.queries.VdcQueryReturnValue; import org.ovirt.engine.core.common.queries.VdcQueryType; import org.ovirt.engine.core.utils.servlet.LocaleFilter; import org.ovirt.engine.core.utils.servlet.ServletUtils; /** * Renders the HTML host page of a GWT application. * <p> * Embeds additional data (JavaScript objects) into the host page. * By default, information about the currently logged-in user is included via {@code userInfo} object. */ public abstract class GwtDynamicHostPageServlet extends HttpServlet { /** * Request attributes that participate in MD5 checksum calculation. */ public enum MD5Attributes { ATTR_SELECTOR_SCRIPT("selectorScript"), //$NON-NLS-1$ ATTR_USER_INFO("userInfo"), //$NON-NLS-1$ ATTR_STYLES("brandingStyle"), //$NON-NLS-1$ ATTR_MESSAGES("messages"), //$NON-NLS-1$ ATTR_BASE_CONTEXT_PATH("baseContextPath"), //$NON-NLS-1$ ATTR_LOCALE(LocaleFilter.LOCALE), ATTR_APPLICATION_TYPE(BrandingFilter.APPLICATION_NAME), ATTR_ENGINE_RPM_VERSION("engineRpmVersion"), //$NON-NLS-1$ ATTR_DISPLAY_UNCAUGHT_UI_EXCEPTIONS("DISPLAY_UNCAUGHT_UI_EXCEPTIONS"); //$NON-NLS-1$ private final String attributeKey; MD5Attributes(String key) { this.attributeKey = key; } /** * Get the key associated with this attribute. */ public String getKey() { return attributeKey; } } private static final long serialVersionUID = 3946034162721073929L; public static final String IF_NONE_MATCH_HEADER = "If-None-Match"; //$NON-NLS-1$ public static final String ETAG_HEADER = "Etag"; //$NON-NLS-1$ private static final String HOST_JSP = "/GwtHostPage.jsp"; //$NON-NLS-1$ private static final String UTF_CONTENT_TYPE = "text/html; charset=UTF-8"; //$NON-NLS-1$ private BackendLocal backend; private ObjectMapper mapper; private BrandingManager brandingManager; @EJB(beanInterface = BackendLocal.class, mappedName = "java:global/engine/bll/Backend!org.ovirt.engine.core.common.interfaces.BackendLocal") public void setBackend(BackendLocal backend) { this.backend = backend; } @Override public void init() { init(new ObjectMapper(), BrandingManager.getInstance()); } void init(ObjectMapper mapper, BrandingManager brandingManager) { this.mapper = mapper; this.brandingManager = brandingManager; } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { final String engineSessionId = getEngineSessionId(request); // Set attribute for selector script request.setAttribute(MD5Attributes.ATTR_SELECTOR_SCRIPT.getKey(), getSelectorScriptName()); // Set the messages that need to be replaced. request.setAttribute(MD5Attributes.ATTR_MESSAGES.getKey(), getBrandingMessages(getApplicationTypeFromRequest(request), getLocaleFromRequest(request))); request.setAttribute(MD5Attributes.ATTR_BASE_CONTEXT_PATH.getKey(), getValueObject(ServletUtils.getBaseContextPath(request))); request.setAttribute(MD5Attributes.ATTR_DISPLAY_UNCAUGHT_UI_EXCEPTIONS.getKey(), getDisplayUncaughtUIExceptions() ? BooleanNode.TRUE : BooleanNode.FALSE); // Set attributes for userInfo object DbUser loggedInUser = getLoggedInUser(engineSessionId); if (loggedInUser != null) { String ssoToken = getSsoToken(engineSessionId); request.setAttribute(MD5Attributes.ATTR_USER_INFO.getKey(), getUserInfoObject(loggedInUser, ssoToken)); } // Set attribute for engineRpmVersion object String engineRpmVersion = getEngineRpmVersion(engineSessionId); request.setAttribute(MD5Attributes.ATTR_ENGINE_RPM_VERSION.getKey(), getValueObject(engineRpmVersion)); try { // Calculate MD5 for use with If-None-Match request header String md5sum = getMd5Sum(request); if (request.getHeader(IF_NONE_MATCH_HEADER) != null && request.getHeader(IF_NONE_MATCH_HEADER).equals(md5sum)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { RequestDispatcher dispatcher = request.getRequestDispatcher(HOST_JSP); response.setContentType(UTF_CONTENT_TYPE); response.addHeader(ETAG_HEADER, md5sum); if (dispatcher != null) { dispatcher.include(request, response); } } } catch (NoSuchAlgorithmException ex) { throw new ServletException(ex); } } protected String getEngineSessionId(final HttpServletRequest request) { return (String) request.getSession().getAttribute(SessionConstants.HTTP_SESSION_ENGINE_SESSION_ID_KEY); } private String getSsoToken(final String engineSessionId) { return (String) runQuery(VdcQueryType.GetEngineSessionIdToken, new VdcQueryParametersBase(), engineSessionId); } protected Boolean getDisplayUncaughtUIExceptions() { return Config.<Boolean> getValue(ConfigValues.DisplayUncaughtUIExceptions); } /** * Retrieves the application type from the request object, this can return null if the * attribute containing the application type is empty. * @param request The {@code HttpServletRequest} object. * @return A string containing the application type. */ private String getApplicationTypeFromRequest(final HttpServletRequest request) { return (String) request.getAttribute(BrandingFilter.APPLICATION_NAME); } @Override protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } /** * @return Name of the GWT selector script, e.g. {@code myapp.nocache.js}. */ protected abstract String getSelectorScriptName(); /** * Get the user locale from the request. The {@code LocaleFilter} should have populated the value. * @param request {@code ServletRequest} that contains the locale used to look up the messages. * @return The {@code Locale} from the request. if not found defaults to Locale.US */ private Locale getLocaleFromRequest(final ServletRequest request) { Locale locale = (Locale) request.getAttribute(LocaleFilter.LOCALE); //$NON-NLS-1$ if (locale == null) { // If no locale defined, default back to the default. locale = LocaleFilter.DEFAULT_LOCALE; } return locale; } /** * Create a Javascript value object with the value being the passed in value. * @param value The {@code String} value to use as the value of the object. * @return A String representation of the Javascript object. */ protected String getValueObject(final String value) { ObjectNode node = mapper.createObjectNode(); node.put("value", value); //$NON-NLS-1$ return node.toString(); } /** * Get a JavaScript associative array string that define the branding messages. * @param applicationName the application name. * @param locale {@code Locale} to use to look up the messages. * @return The messages as a {@code String} */ private String getBrandingMessages(final String applicationName, final Locale locale) { return brandingManager.getMessages(applicationName, locale); } /** * @return {@code true} if all queries should be filtered according to user permissions, {@code false} otherwise. */ protected abstract boolean filterQueries(); protected void initQueryParams(VdcQueryParametersBase queryParams, String sessionId) { queryParams.setSessionId(sessionId); queryParams.setFiltered(filterQueries()); } /** * Executes a backend {@linkplain BackendLocal#runQuery query} and returns its result value if successful. * <p> * Returns {@code null} otherwise. */ protected Object runQuery(VdcQueryType queryType, VdcQueryParametersBase queryParams, String sessionId) { initQueryParams(queryParams, sessionId); VdcQueryReturnValue result = backend.runQuery(queryType, queryParams); return result != null && result.getSucceeded() ? result.getReturnValue() : null; } /** * Executes a backend {@linkplain BackendLocal#runPublicQuery public query} and returns its result value if * successful. * <p> * Returns {@code null} otherwise. */ protected Object runPublicQuery(VdcQueryType queryType, VdcQueryParametersBase queryParams, String sessionId) { initQueryParams(queryParams, sessionId); VdcQueryReturnValue result = backend.runPublicQuery(queryType, queryParams); return result != null && result.getSucceeded() ? result.getReturnValue() : null; } protected ObjectNode createObjectNode() { return mapper.createObjectNode(); } protected ArrayNode createArrayNode() { return mapper.createArrayNode(); } protected DbUser getLoggedInUser(String sessionId) { return (DbUser) runQuery(VdcQueryType.GetUserBySessionId, new VdcQueryParametersBase(), sessionId); } protected ObjectNode getUserInfoObject(DbUser loggedInUser, String ssoToken) { ObjectNode obj = createObjectNode(); obj.put("id", loggedInUser.getId().toString()); //$NON-NLS-1$ obj.put("userName", loggedInUser.getLoginName()); //$NON-NLS-1$ obj.put("domain", loggedInUser.getDomain()); //$NON-NLS-1$ obj.put("isAdmin", loggedInUser.isAdmin()); //$NON-NLS-1$ obj.put("ssoToken", ssoToken); //$NON-NLS-1$ return obj; } protected String getMd5Sum(HttpServletRequest request) throws NoSuchAlgorithmException, UnsupportedEncodingException { return new HexBinaryAdapter().marshal(getMd5Digest(request).digest()); } /** * Calculate an MD5 sum to be used as an ETag in the request header. * The attributes come from the request scope and they keys are * determine by the {@code MD5Attributes} enum. * * @param request The {@code HttpServletRequest} to use as the source * of the attribute values. * @return A {@code MessageDigest} which will be used to generate the * string representation of the MD5 sum. * @throws NoSuchAlgorithmException If the method cannot create the digest * object. */ protected MessageDigest getMd5Digest(HttpServletRequest request) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest digest = createMd5Digest(); for (MD5Attributes attribute: MD5Attributes.values()) { if (request.getAttribute(attribute.getKey()) != null) { digest.update(request.getAttribute(attribute.getKey()).toString().getBytes(StandardCharsets.UTF_8)); } } return digest; } protected MessageDigest createMd5Digest() throws NoSuchAlgorithmException { return MessageDigest.getInstance("MD5"); //$NON-NLS-1$ } protected String getEngineRpmVersion(String sessionId) { return (String) runPublicQuery(VdcQueryType.GetConfigurationValue, new GetConfigurationValueParameters(ConfigurationValues.ProductRPMVersion, ConfigCommon.defaultConfigurationVersion), sessionId); } }