/** * Get the expected name for the manifest cookie. * * @return the name (null if none) */ private static String getManifestCookieName() { AuraContext context = Aura.getContextService().getCurrentContext(); if (context.getApplicationDescriptor() != null) { StringBuilder sb = new StringBuilder(); if (context.getMode() != Mode.PROD) { sb.append(context.getMode()); sb.append("_"); } sb.append(context.getApplicationDescriptor().getNamespace()); sb.append("_"); sb.append(context.getApplicationDescriptor().getName()); sb.append(MANIFEST_COOKIE_TAIL); return sb.toString(); } return null; }
/** * check the top level component/app and get dependencies. * * <p>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. * * <p>If there is a QFE, we substitute the QFE descriptor for the one given us, and continue. * Again, we cannot allow caching. * * <p>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. * * <p>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); }
/** * Gets the UID for the application descriptor of the current context, or {@code null} if there is * no application (probably because of a compile error). */ public String getContextAppUid(AuraContext context) { DefDescriptor<? extends BaseComponentDef> app = context.getApplicationDescriptor(); if (app != null) { try { return definitionService.getDefRegistry().getUid(null, app); } catch (QuickFixException e) { // This is perfectly possible, but the error is handled in more // contextually-sensible places. For here, we know there's no // meaningful uid, so we fall through and return null. } } return null; }
/** Is AppCache allowed by the current configuration? */ public static boolean isManifestEnabled() { if (!Aura.getConfigAdapter().isClientAppcacheEnabled()) { return false; } AuraContext context = Aura.getContextService().getCurrentContext(); DefDescriptor<? extends BaseComponentDef> appDefDesc = context.getApplicationDescriptor(); if (appDefDesc != null && appDefDesc.getDefType().equals(DefType.APPLICATION)) { try { Boolean useAppcache = ((ApplicationDef) appDefDesc.getDef()).isAppcacheEnabled(); if (useAppcache != null) { return useAppcache.booleanValue(); } return false; } catch (QuickFixException e) { return false; } } return false; }
@Override public void assertAccess(DefDescriptor<?> desc) throws QuickFixException { if (!accessCache.contains(desc)) { Aura.getLoggingService().incrementNum("SecurityProviderCheck"); DefType defType = desc.getDefType(); String ns = desc.getNamespace(); AuraContext context = Aura.getContextService().getCurrentContext(); Mode mode = context.getMode(); String prefix = desc.getPrefix(); // // This breaks encapsulation! -gordon // boolean isTopLevel = desc.equals(context.getApplicationDescriptor()); if (isTopLevel) { // // If we are trying to access the top level component, we need to ensure // that it is _not_ abstract. // BaseComponentDef def = getDef(context.getApplicationDescriptor()); if (def != null && def.isAbstract() && def.getProviderDescriptor() == null) { throw new NoAccessException( String.format("Access to %s disallowed. Abstract definition.", desc)); } } // // If this is _not_ the top level, we allow circumventing the security provider. // This means that certain things will short-circuit, hopefully making checks faster... // Not sure if this is premature optimization or not. // if (!isTopLevel || desc.getDefType().equals(DefType.COMPONENT)) { if (!securedDefTypes.contains(defType) || unsecuredPrefixes.contains(prefix) || unsecuredNamespaces.contains(ns) || (mode != Mode.PROD && (!Aura.getConfigAdapter().isProduction()) && unsecuredNonProductionNamespaces.contains(ns))) { accessCache.add(desc); return; } if (ns != null && DefDescriptor.JAVA_PREFIX.equals(prefix)) { // handle java packages that have namespaces like aura.impl.blah for (String okNs : unsecuredNamespaces) { if (ns.startsWith(okNs)) { accessCache.add(desc); return; } } } } SecurityProviderDef securityProviderDef = getSecurityProvider(); if (securityProviderDef == null) { if (mode != Mode.PROD && !Aura.getConfigAdapter().isProduction()) { accessCache.add(desc); return; } else { throw new NoAccessException( String.format("Access to %s disallowed. No Security Provider found.", desc)); } } else { if (!securityProviderDef.isAllowed(desc)) { throw new NoAccessException( String.format( "Access to %s disallowed by %s", desc, securityProviderDef.getDescriptor().getName())); } } accessCache.add(desc); } }
/** * Handle an exception in the servlet. * * <p>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 serialize(Json json, AuraContext ctx) throws IOException { json.writeMapBegin(); json.writeMapEntry("mode", ctx.getMode()); DefDescriptor<? extends BaseComponentDef> appDesc = ctx.getApplicationDescriptor(); if (appDesc != null) { if (appDesc.getDefType().equals(DefType.APPLICATION)) { json.writeMapEntry( "app", String.format("%s:%s", appDesc.getNamespace(), appDesc.getName())); } else { json.writeMapEntry( "cmp", String.format("%s:%s", appDesc.getNamespace(), appDesc.getName())); } } if (ctx.getSerializeThemes()) { ThemeList themes = ctx.getThemeList(); if (!themes.isEmpty()) { List<String> stringed = Lists.newArrayList(); for (DefDescriptor<ThemeDef> theme : themes) { stringed.add(theme.getQualifiedName()); } json.writeMapEntry("themes", stringed); } Optional<String> dynamicVarsUid = themes.getActiveDynamicVarsUid(); if (dynamicVarsUid.isPresent()) { json.writeMapEntry("dynamicVarsUid", dynamicVarsUid.get()); } } if (ctx.getRequestedLocales() != null) { List<String> locales = new ArrayList<>(); for (Locale locale : ctx.getRequestedLocales()) { locales.add(locale.toString()); } json.writeMapEntry("requestedLocales", locales); } Map<String, String> loadedStrings = Maps.newHashMap(); Map<DefDescriptor<?>, String> clientLoaded = Maps.newHashMap(); clientLoaded.putAll(ctx.getClientLoaded()); for (Map.Entry<DefDescriptor<?>, String> entry : ctx.getLoaded().entrySet()) { loadedStrings.put( String.format( "%s@%s", entry.getKey().getDefType().toString(), entry.getKey().getQualifiedName()), entry.getValue()); clientLoaded.remove(entry.getKey()); } if (forClient) { for (DefDescriptor<?> deleted : clientLoaded.keySet()) { loadedStrings.put( String.format("%s@%s", deleted.getDefType().toString(), deleted.getQualifiedName()), DELETED); } } if (loadedStrings.size() > 0) { json.writeMapKey("loaded"); json.writeMap(loadedStrings); } if (ctx.getSerializeLastMod()) { json.writeMapEntry("lastmod", Long.toString(AuraBaseServlet.getLastMod())); } TestContextAdapter testContextAdapter = Aura.get(TestContextAdapter.class); if (testContextAdapter != null) { TestContext testContext = testContextAdapter.getTestContext(); if (testContext != null) { json.writeMapEntry("test", testContext.getName()); } } if (ctx.getFrameworkUID() != null) { json.writeMapEntry("fwuid", ctx.getFrameworkUID()); } if (forClient) { // client needs value providers, urls don't boolean started = false; for (GlobalValueProvider valueProvider : ctx.getGlobalProviders().values()) { if (!valueProvider.isEmpty()) { if (!started) { json.writeMapKey("globalValueProviders"); json.writeArrayBegin(); started = true; } json.writeComma(); json.writeIndent(); json.writeMapBegin(); json.writeMapEntry("type", valueProvider.getValueProviderKey().getPrefix()); json.writeMapEntry("values", valueProvider.getData()); json.writeMapEnd(); } } if (started) { json.writeArrayEnd(); } // // Now comes the tricky part, we have to serialize all of the definitions that are // required on the client side, and, of all types. This way, we won't have to handle // ugly cases of actual definitions nested inside our configs, and, we ensure that // all dependencies actually get sent to the client. Note that the 'loaded' set needs // to be updated as well, but that needs to happen prior to this. // Map<DefDescriptor<? extends Definition>, Definition> defMap; defMap = ctx.getDefRegistry().filterRegistry(ctx.getPreloadedDefinitions()); if (defMap.size() > 0) { List<Definition> componentDefs = Lists.newArrayList(); List<Definition> eventDefs = Lists.newArrayList(); List<Definition> libraryDefs = Lists.newArrayList(); for (Map.Entry<DefDescriptor<? extends Definition>, Definition> entry : defMap.entrySet()) { DefDescriptor<? extends Definition> desc = entry.getKey(); DefType dt = desc.getDefType(); Definition d = entry.getValue(); // // Ignore defs that ended up not being valid. This is arguably something // that the MDR should have done when filtering. // if (d != null) { if (DefType.COMPONENT.equals(dt) || DefType.APPLICATION.equals(dt)) { componentDefs.add(d); } else if (DefType.EVENT.equals(dt)) { eventDefs.add(d); } else if (DefType.LIBRARY.equals(dt)) { libraryDefs.add(d); } } } writeDefs(json, "componentDefs", componentDefs); writeDefs(json, "eventDefs", eventDefs); writeDefs(json, "libraryDefs", libraryDefs); } ctx.serializeAsPart(json); } json.writeMapEnd(); }
/** * Write out the manifest. * * <p>This writes out the full manifest for an application so that we can use the AppCache. * * <p>The manifest contains CSS and JavaScript URLs. These specified resources are copied into the * AppCache with the HTML template. When the page is reloaded, the existing manifest is compared * to the new manifest. If they are identical, the resources are served from the AppCache. * Otherwise, the resources are requested from the server and the AppCache is updated. * * @param request the request * @param response the response * @param context the context * @throws IOException if unable to write out the response */ @Override public void write(HttpServletRequest request, HttpServletResponse response, AuraContext context) throws IOException { servletUtilAdapter.setNoCache(response); try { Map<String, Object> attributes = getComponentAttributes(request); // // First, we make sure that the manifest is enabled. // if (!manifestUtil.isManifestEnabled(request)) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // // Now we validate the cookie, which includes loop detection. // this routine sets the response code. // if (!manifestUtil.checkManifestCookie(request, response)) { return; } boolean appOk = false; DefDescriptor<? extends BaseComponentDef> descr = null; try { descr = context.getApplicationDescriptor(); if (descr != null) { definitionService.updateLoaded(descr); appOk = true; } } catch (QuickFixException qfe) { // // ignore qfe, since we really don't care... the manifest will be 404ed. // This will eventually cause the browser to give up. Note that this case // should almost never occur, as it requires the qfe to be introduced between // the initial request (which will not set a manifest if it gets a qfe) and // the manifest request. // } catch (ClientOutOfSyncException coose) { // // In this case, we want to force a reload... A 404 on the manifest is // supposed to handle this. we hope that the client will do the right // thing, and reload everything. Note that this case really should only // happen if the client already has content, and thus should be refreshing // However, there are very odd edge cases that we probably can't detect // without keeping server side state, such as the case that something // is updated between the initial HTML request and the manifest request. // Not sure what browsers will do in this case. // } if (!appOk) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // // This writes both the app and framework signatures into // the manifest, so that if either one changes, the // manifest will change. Note that in most cases, we will // write these signatures in multiple places, but we just // need to make sure that they are in at least one place. // Map<String, Object> attribs = Maps.newHashMap(); String appUid = getContextAppUid(context); String nonce = configAdapter.getAuraFrameworkNonce(); // Since we don't get the UID from our URL, we set it here. context.setFrameworkUID(nonce); attribs.put(LAST_MOD, String.format("app=%s, FW=%s", appUid, nonce)); attribs.put(UID, appUid); StringWriter sw = new StringWriter(); String resetCssUrl = configAdapter.getResetCssURL(); if (resetCssUrl != null) { sw.write(resetCssUrl); sw.write('\n'); } for (String s : servletUtilAdapter.getStyles(context)) { sw.write(s); sw.write('\n'); } for (String s : servletUtilAdapter.getScripts(context, true, true, attributes)) { sw.write(s); sw.write('\n'); } // Add locker service safe eval worker url String lockerWorkerURL = configAdapter.getLockerWorkerURL(); if (lockerWorkerURL != null) { sw.write(lockerWorkerURL); sw.write('\n'); } // Add in any application specific resources if (descr != null && descr.getDefType().equals(DefType.APPLICATION)) { ApplicationDef def = (ApplicationDef) descr.getDef(); for (String s : def.getAdditionalAppCacheURLs()) { if (s != null) { sw.write(s); sw.write('\n'); } } } attribs.put(RESOURCE_URLS, sw.toString()); DefDescriptor<ComponentDef> tmplDesc = definitionService.getDefDescriptor("ui:manifest", ComponentDef.class); Component tmpl = instanceService.getInstance(tmplDesc, attribs); renderingService.render(tmpl, response.getWriter()); } catch (Exception e) { Aura.getExceptionAdapter().handleException(e); // Can't throw exception here: to set manifest OBSOLETE response.setStatus(HttpServletResponse.SC_NOT_FOUND); } }