Java tutorial
/* * Copyright (C) 2013 salesforce.com, inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.auraframework.impl.adapter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.auraframework.adapter.ConfigAdapter; import org.auraframework.adapter.ContentSecurityPolicy; import org.auraframework.adapter.ExceptionAdapter; import org.auraframework.adapter.ServletUtilAdapter; import org.auraframework.annotations.Annotations.ServiceComponent; import org.auraframework.clientlibrary.ClientLibraryService; import org.auraframework.def.BaseComponentDef; import org.auraframework.def.ClientLibraryDef; import org.auraframework.def.DefDescriptor; import org.auraframework.def.DefDescriptor.DefType; import org.auraframework.http.CSP; import org.auraframework.instance.InstanceStack; import org.auraframework.service.ContextService; import org.auraframework.service.DefinitionService; import org.auraframework.service.SerializationService; import org.auraframework.system.AuraContext; import org.auraframework.system.AuraContext.Format; import org.auraframework.system.AuraContext.Mode; import org.auraframework.system.AuraResource; import org.auraframework.system.MasterDefRegistry; import org.auraframework.throwable.AuraUnhandledException; import org.auraframework.throwable.ClientOutOfSyncException; import org.auraframework.throwable.NoAccessException; import org.auraframework.throwable.quickfix.DefinitionNotFoundException; import org.auraframework.throwable.quickfix.QuickFixException; import org.auraframework.util.AuraTextUtil; import org.auraframework.util.json.JsonEncoder; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @ServiceComponent public class ServletUtilAdapterImpl implements ServletUtilAdapter { private ContextService contextService; private ConfigAdapter configAdapter; private ExceptionAdapter exceptionAdapter; private SerializationService serializationService; private ClientLibraryService clientLibraryService; protected DefinitionService definitionService; /** * "Short" pages (such as manifest cookies and AuraFrameworkServlet pages) expire in 1 day. */ protected static final long SHORT_EXPIRE_SECONDS = 24L * 60 * 60; protected static final long SHORT_EXPIRE = SHORT_EXPIRE_SECONDS * 1000; /** * "Long" pages (such as resources and cached HTML templates) expire in 45 days. We also use this to "pre-expire" * no-cache pages, setting their expiration a month and a half into the past for user agents that don't understand * Cache-Control: no-cache. * Same as auraBaseServlet.java */ protected static final long LONG_EXPIRE = 45 * SHORT_EXPIRE; protected static final String UTF_ENCODING = "UTF-8"; protected static final String HTML_CONTENT_TYPE = "text/html"; protected static final String JAVASCRIPT_CONTENT_TYPE = "text/javascript"; protected static final String MANIFEST_CONTENT_TYPE = "text/cache-manifest"; protected static final String CSS_CONTENT_TYPE = "text/css"; protected static final String SVG_CONTENT_TYPE = "image/svg+xml"; /** Clickjack protection HTTP header */ protected static final String HDR_FRAME_OPTIONS = "X-FRAME-OPTIONS"; /** Baseline clickjack protection level for HDR_FRAME_OPTIONS header */ protected static final String HDR_FRAME_SAMEORIGIN = "SAMEORIGIN"; /** No-framing-at-all clickjack protection level for HDR_FRAME_OPTIONS header */ protected static final String HDR_FRAME_DENY = "DENY"; /** Limited access for HDR_FRAME_OPTIONS */ protected static final String HDR_FRAME_ALLOWFROM = "ALLOW-FROM "; /** * Semi-standard HDR_FRAME_OPTIONS to have no restrictions. Used because no * header at all is taken as an invitation for filters to add their own ideas. */ protected static final String HDR_FRAME_ALLOWALL = "ALLOWALL"; /** * Handle an exception in the servlet. * * This routine should be called whenever an exception has surfaced to the top level of the servlet. It should not be * overridden unless Aura is entirely subsumed. Most special cases can be handled by the Aura user by implementing * {@link ExceptionAdapter ExceptionAdapter}. * * @param t the throwable to write out. * @param quickfix is this exception a valid quick-fix * @param context the aura context. * @param request the request. * @param response the response. * @param written true if we have started writing to the output stream. * @throws IOException if the output stream does. * @throws ServletException if send404 does (should not generally happen). */ @Override public void handleServletException(Throwable t, boolean quickfix, AuraContext context, HttpServletRequest request, HttpServletResponse response, boolean written) throws IOException { try { Throwable mappedEx = t; boolean map = !quickfix; Format format = context.getFormat(); // // This seems to fail, though the documentation implies that you can do // it. // // if (written && !response.isCommitted()) { // response.resetBuffer(); // written = false; // } if (!written) { // Should we only delete for JSON? setNoCache(response); } if (mappedEx instanceof IOException) { // // Just re-throw IOExceptions. // throw (IOException) mappedEx; } else if (mappedEx instanceof NoAccessException) { Throwable cause = mappedEx.getCause(); String denyMessage = mappedEx.getMessage(); map = false; if (cause != null) { // // Note that the exception handler can remap the cause here. // cause = exceptionAdapter.handleException(cause); denyMessage += ": cause = " + cause.getMessage(); } // // Is this correct?!?!?! // if (format != Format.JSON) { this.send404(request.getServletContext(), request, response); if (!isProductionMode(context.getMode())) { // Preserve new lines and tabs in the stacktrace since this is directly being written on to the // page denyMessage = "<pre>" + AuraTextUtil.escapeForHTML(denyMessage) + "</pre>"; response.getWriter().println(denyMessage); } return; } } else if (mappedEx instanceof QuickFixException) { if (isProductionMode(context.getMode())) { // // In production environments, we want wrap the quick-fix. But be a little careful here. // We should never mark the top level as a quick-fix, because that means that we gack // on every mis-spelled app. In this case we simply send a 404 and bolt. // if (mappedEx instanceof DefinitionNotFoundException) { DefinitionNotFoundException dnfe = (DefinitionNotFoundException) mappedEx; if (dnfe.getDescriptor() != null && dnfe.getDescriptor().equals(context.getApplicationDescriptor())) { // We're in production and tried to hit an aura app that doesn't exist. // just show the standard 404 page. this.send404(request.getServletContext(), request, response); return; } } map = true; mappedEx = new AuraUnhandledException("404 Not Found (Application Error)", mappedEx); } } if (map) { mappedEx = exceptionAdapter.handleException(mappedEx); } PrintWriter out = response.getWriter(); // // If we have written out data, We are kinda toast in this case. // We really want to roll it all back, but we can't, so we opt // for the best we can do. For HTML we can do nothing at all. // if (format == Format.JSON) { if (!written) { out.write(CSRF_PROTECT); } // // If an exception happened while we were emitting JSON, we want the // client to ignore the now-corrupt data structure. 404s and 500s // cause the client to prepend /*, so we can effectively erase the // bad data by appending a */ here and then serializing the exception // info. // out.write("*/"); // // Unfortunately we can't do the following now. It might be possible // in some cases, but we don't want to go there unless we have to. // } if (format == Format.JS || format == Format.CSS) { // Make sure js and css doesn't get cached in browser, appcache, etc response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); } if (format == Format.JSON || format == Format.HTML || format == Format.JS || format == Format.CSS) { // // We only write out exceptions for HTML or JSON. // Seems bogus, but here it is. // // Start out by cleaning out some settings to ensure we don't // check too many things, leading to a circular failure. Note // that this is still a bit dangerous, as we seem to have a lot // of magic in the serializer. // // Clear the InstanceStack before trying to serialize the exception since the Throwable has likely // rendered the stack inaccurate, and may falsely trigger NoAccessExceptions. InstanceStack stack = this.contextService.getCurrentContext().getInstanceStack(); List<String> list = stack.getStackInfo(); for (int count = list.size(); count > 0; count--) { stack.popInstance(stack.peek()); } serializationService.write(mappedEx, null, out); if (format == Format.JSON) { out.write("/*ERROR*/"); } } } catch (IOException ioe) { throw ioe; } catch (Throwable death) { // // Catch any other exception and log it. This is actually kinda bad, because something has // gone horribly wrong. We should write out some sort of generic page other than a 404, // but at this point, it is unclear what we can do, as stuff is breaking right and left. // try { response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); exceptionAdapter.handleException(death); if (!isProductionMode(context.getMode())) { response.getWriter().println(death.getMessage()); } } catch (IOException ioe) { throw ioe; } catch (Throwable doubleDeath) { // we are totally hosed. if (!isProductionMode(context.getMode())) { response.getWriter().println(doubleDeath.getMessage()); } } } finally { this.contextService.endContext(); } } @Override public void send404(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.getWriter() .println("404 Not Found" + "<!-- Extra text so IE will display our custom 404 page -->" + "<!-- -->" + "<!-- -->" + "<!-- -->" + "<!-- -->" + "<!-- -->" + "<!-- -->" + "<!-- -->" + "<!-- -->"); this.contextService.endContext(); } @Override public List<String> getScripts(AuraContext context, boolean safeInlineJs, boolean ignoreNonCacheableScripts, Map<String, Object> attributes) throws QuickFixException { List<String> ret = Lists.newArrayList(); // Client libraries ret.addAll(getJsClientLibraryUrls(context)); ret.addAll(getBaseScripts(context, attributes)); ret.addAll(getFrameworkScripts(context, safeInlineJs, ignoreNonCacheableScripts, attributes)); return ret; } @Override public List<String> getStyles(AuraContext context) throws QuickFixException { Set<String> ret = Sets.newLinkedHashSet(); // Add css client libraries ret.addAll(getCssClientLibraryUrls(context)); ret.add(getAppCssUrl(context)); return new ArrayList<>(ret); } /** * Gets all client libraries specified. Uses client library service to resolve any urls that weren't specified. * Returns list of non empty client library urls. * * * @param context aura context * @param type CSS or JS * @return list of urls for client libraries */ private List<String> getClientLibraryUrls(AuraContext context, ClientLibraryDef.Type type) throws QuickFixException { return new ArrayList<>(clientLibraryService.getUrls(context, type)); } /** * Get the set of base scripts for a context. */ @Override public List<String> getBaseScripts(AuraContext context, Map<String, Object> attributes) throws QuickFixException { Set<String> ret = Sets.newLinkedHashSet(); // Aura framework ret.add(getFrameworkUrl()); return new ArrayList<>(ret); } @Override public List<String> getFrameworkScripts(AuraContext context, boolean safeInlineJs, boolean ignoreNonCacheableScripts, Map<String, Object> attributes) throws QuickFixException { List<String> ret = Lists.newArrayList(); if (safeInlineJs && !ignoreNonCacheableScripts) { ret.add(getInlineJsUrl(context, attributes)); } ret.add(getAppJsUrl(context, null)); if (!ignoreNonCacheableScripts) { ret.add(getBootstrapUrl(context, attributes)); } return ret; } @Override public List<String> getFrameworkFallbackScripts(AuraContext context, boolean safeInlineJs, Map<String, Object> attributes) throws QuickFixException { List<String> ret = Lists.newArrayList(); // TODO W-3269340 use fallback url for all required files to boot aura: inline.js, app.js, aura_*.js, libs_*.js ret.add(getBootstrapUrl(context, attributes) + " " + getBootstrapFallbackUrl(context, attributes)); ret.add(configAdapter.getEncryptionKeyURL(true) + " " + configAdapter.getEncryptionKeyFallbackURL(true)); return ret; } @Override public List<String> getJsClientLibraryUrls(AuraContext context) throws QuickFixException { return getClientLibraryUrls(context, ClientLibraryDef.Type.JS); } @Override public List<String> getCssClientLibraryUrls(AuraContext context) throws QuickFixException { return new ArrayList<>(getClientLibraryUrls(context, ClientLibraryDef.Type.CSS)); } @Override public String getFrameworkUrl() { return configAdapter.getAuraJSURL(); } private void addAttributes(StringBuilder builder, Map<String, Object> attributes) { // // This feels a lot like a hack. // if (attributes != null && !attributes.isEmpty()) { builder.append("?aura.attributes="); builder.append(AuraTextUtil.urlencode(JsonEncoder.serialize(attributes, false, false))); } } /** * Get the set of base scripts for a context. */ @Override public String getBootstrapUrl(AuraContext context, Map<String, Object> attributes) { return commonJsUrl("/bootstrap.js", context, attributes); } @Override public String getBootstrapFallbackUrl(AuraContext context, Map<String, Object> attributes) { String contextPath = context.getContextPath(); String nonce = context.getFrameworkUID(); return String.format("%s/auraFW/resources/%s/aura/fallback/fallback.bootstrap.js", contextPath, nonce); } @Override public String getInlineJsUrl(AuraContext context, Map<String, Object> attributes) { return commonJsUrl("/inline.js", context, attributes); } @Override public String getAppJsUrl(AuraContext context, Map<String, Object> attributes) { return commonJsUrl("/app.js", context, attributes); } @Override public String getAppCssUrl(AuraContext context) { String contextPath = context.getContextPath(); StringBuilder defs = new StringBuilder(contextPath).append("/l/"); defs.append(context.getEncodedURL(AuraContext.EncodingStyle.Css)); defs.append("/app.css"); return defs.toString(); } private String commonJsUrl(String filepath, AuraContext context, Map<String, Object> attributes) { StringBuilder url = new StringBuilder(context.getContextPath()).append("/l/"); url.append(context.getEncodedURL(AuraContext.EncodingStyle.Normal)); url.append(filepath); if (attributes != null) { addAttributes(url, attributes); } return url.toString(); } /** * Tell the browser to not cache. * * This sets several headers to try to ensure that the page will not be cached. Not sure if last modified matters * -goliver * * @param response the HTTP response to which we will add headers. */ @Override public void setNoCache(HttpServletResponse response) { long past = System.currentTimeMillis() - LONG_EXPIRE; response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store"); response.setHeader(HttpHeaders.PRAGMA, "no-cache"); response.setDateHeader(HttpHeaders.EXPIRES, past); response.setDateHeader(HttpHeaders.LAST_MODIFIED, past); } /** * Check to see if we are in production mode. */ @Override public boolean isProductionMode(Mode mode) { return mode == Mode.PROD || configAdapter.isProduction(); } /** * Sets mandatory headers, notably for anti-clickjacking. */ @Override public void setCSPHeaders(DefDescriptor<?> top, HttpServletRequest req, HttpServletResponse rsp) { ContentSecurityPolicy csp = configAdapter .getContentSecurityPolicy(top == null ? null : top.getQualifiedName(), req); if (csp != null) { rsp.setHeader(CSP.Header.SECURE, csp.getCspHeaderValue()); Collection<String> terms = csp.getFrameAncestors(); if (terms != null) { // not open to the world; figure whether we can express an X-FRAME-OPTIONS header: if (terms.size() == 0) { // closed to any framing at all rsp.setHeader(HDR_FRAME_OPTIONS, HDR_FRAME_DENY); } else if (terms.size() == 1) { // With one ancestor term, we're either SAMEORIGIN or ALLOWFROM for (String site : terms) { if (site == null) { // Add same-origin headers and policy terms rsp.addHeader(HDR_FRAME_OPTIONS, HDR_FRAME_SAMEORIGIN); } else if (!site.contains("*") && !site.matches("^[a-z]+:$")) { // XFO can't express wildcards or protocol-only, so set only for a specific site: rsp.addHeader(HDR_FRAME_OPTIONS, HDR_FRAME_ALLOWFROM + site); } else { // When XFO can't express it, still set an ALLOWALL so filters don't jump in rsp.addHeader(HDR_FRAME_OPTIONS, HDR_FRAME_ALLOWALL); } } } } } } /** * Set a long cache timeout. * * This sets several headers to try to ensure that the page will be cached for a reasonable length of time. Of note * is the last-modified header, which is set to a day ago so that browsers consider it to be safe. * * @param response the HTTP response to which we will add headers. */ @Override public void setLongCache(HttpServletResponse response) { this.setCacheTimeout(response, LONG_EXPIRE); } /** * Set a 'short' cache timeout. * * This sets several headers to try to ensure that the page will be cached for a shortish length of time. Of note is * the last-modified header, which is set to a day ago so that browsers consider it to be safe. * * @param response the HTTP response to which we will add headers. */ @Override public void setShortCache(HttpServletResponse response) { this.setCacheTimeout(response, SHORT_EXPIRE); } /** * Sets cache timeout to a given value. * * This sets several headers to try to ensure that the page will be cached for the given length of time. Of note is * the last-modified header, which is set to a day ago so that browsers consider it to be safe. * * @param response the HTTP response to which we will add headers. * @param expiration timeout value in milliseconds. */ @Override public void setCacheTimeout(HttpServletResponse response, long expiration) { long now = System.currentTimeMillis(); response.setHeader(HttpHeaders.VARY, "Accept-Encoding"); response.setHeader(HttpHeaders.CACHE_CONTROL, String.format("max-age=%s, public", expiration / 1000)); response.setDateHeader(HttpHeaders.EXPIRES, now + expiration); response.setDateHeader(HttpHeaders.LAST_MODIFIED, now - SHORT_EXPIRE); } @Override public String getContentType(AuraContext.Format format) { switch (format) { case MANIFEST: return MANIFEST_CONTENT_TYPE; case CSS: return CSS_CONTENT_TYPE; case JS: return JAVASCRIPT_CONTENT_TYPE; case JSON: return JsonEncoder.MIME_TYPE; case HTML: return HTML_CONTENT_TYPE; case SVG: return SVG_CONTENT_TYPE; default: } return ("text/plain"); } @Override public boolean isValidDefType(DefType defType, Mode mode) { return (defType == DefType.APPLICATION || defType == DefType.COMPONENT); } @Override public boolean resourceServletGetPre(HttpServletRequest request, HttpServletResponse response, AuraResource resource) { return false; } @Override public boolean actionServletGetPre(HttpServletRequest request, HttpServletResponse response) throws IOException { return false; } @Override public boolean actionServletPostPre(HttpServletRequest request, HttpServletResponse response) throws IOException { return false; } /** * check the top level component/app and get dependencies. * * This routine checks to see that we have a valid top level component. If our top level component is out of sync, * we have to ignore it here, but we _must_ force the client to not cache the response. * * If there is a QFE, we substitute the QFE descriptor for the one given us, and continue. Again, we cannot allow * caching. * * Finally, if there is no descriptor given, we simply ignore the request and give them an empty response. Which is * done here by returning null. * * Also note that this handles the 'if-modified-since' header, as we want to tell the browser that nothing changed * in that case. * * @param request the request (for exception handling) * @param response the response (for exception handling) * @param context the context to get the definition. * @return the set of descriptors we are sending back, or null in the case that we handled the response. * @throws IOException if there was an IO exception handling a client out of sync exception * @throws ServletException if there was a problem handling the out of sync */ @Override public Set<DefDescriptor<?>> verifyTopLevel(HttpServletRequest request, HttpServletResponse response, AuraContext context) throws IOException { DefDescriptor<? extends BaseComponentDef> appDesc = context.getApplicationDescriptor(); MasterDefRegistry mdr = context.getDefRegistry(); context.setPreloading(true); if (appDesc == null) { // // This means we have nothing to say to the client, so the response is // left completely empty. // return null; } long ifModifiedSince = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); String uid = context.getUid(appDesc); try { try { definitionService.updateLoaded(appDesc); if (uid != null && ifModifiedSince != -1) { // // In this case, we have an unmodified descriptor, so just tell // the client that. // response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return null; } } catch (ClientOutOfSyncException coose) { // // We can't actually handle an out of sync here, since we are doing a // resource load. We have to ignore it, and continue as if nothing happened. // But in the process, we make sure to set 'no-cache' so that the result // is thrown away. This may actually not give the right result in bizarre // corner cases... beware cache inconsistencies on revert after a QFE. // // We actually probably should do something different, like send a minimalist // set of stuff to make the client re-try. // this.setNoCache(response); String oosUid = mdr.getUid(null, appDesc); return mdr.getDependencies(oosUid); } } catch (QuickFixException qfe) { // // A quickfix exception means that we couldn't compile something. // In this case, we still want to preload things, but we want to preload // quick fix values, note that we force NoCache here. // this.setNoCache(response); this.handleServletException(qfe, true, context, request, response, true); return null; } this.setLongCache(response); if (uid == null) { uid = context.getUid(appDesc); } return mdr.getDependencies(uid); } /** * get the manifest URL. * * This routine will simply return the string, it does not check to see if the manifest is * enabled first. * * @return a string for the manifest URL. */ @Override public String getManifestUrl(AuraContext context, Map<String, Object> attributes) { String contextPath = context.getContextPath(); String ret = ""; StringBuilder defs = new StringBuilder(contextPath).append("/l/"); defs.append(context.getEncodedURL(AuraContext.EncodingStyle.Bare)); defs.append("/app.manifest"); addAttributes(defs, attributes); ret = defs.toString(); return ret; } /** * @param definitionService the definitionService to set */ @Inject public void setDefinitionService(DefinitionService definitionService) { this.definitionService = definitionService; } /** * Injection override. */ @Inject public void setContextService(ContextService contextService) { this.contextService = contextService; } /** * Injection override. */ @Inject public void setConfigAdapter(ConfigAdapter configAdapter) { this.configAdapter = configAdapter; } /** * Injection override. */ @Inject public void setExceptionAdapter(ExceptionAdapter exceptionAdapter) { this.exceptionAdapter = exceptionAdapter; } /** * Injection override. */ @Inject public void setSerializationService(SerializationService serializationService) { this.serializationService = serializationService; } /** * Injection override. */ @Inject public void setClientLibraryService(ClientLibraryService clientLibraryService) { this.clientLibraryService = clientLibraryService; } }