/*
* 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.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.net.URI;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpHeaders;
import org.auraframework.Aura;
import org.auraframework.def.ApplicationDef;
import org.auraframework.def.BaseComponentDef;
import org.auraframework.def.ComponentDef;
import org.auraframework.def.DefDescriptor;
import org.auraframework.def.DefDescriptor.DefType;
import org.auraframework.http.RequestParam.EnumParam;
import org.auraframework.http.RequestParam.StringParam;
import org.auraframework.instance.Action;
import org.auraframework.service.ContextService;
import org.auraframework.service.DefinitionService;
import org.auraframework.service.LoggingService;
import org.auraframework.service.SerializationService;
import org.auraframework.service.ServerService;
import org.auraframework.system.AuraContext;
import org.auraframework.system.AuraContext.Format;
import org.auraframework.system.AuraContext.Mode;
import org.auraframework.system.Message;
import org.auraframework.throwable.AuraRuntimeException;
import org.auraframework.throwable.ClientOutOfSyncException;
import org.auraframework.throwable.NoAccessException;
import org.auraframework.throwable.SystemErrorException;
import org.auraframework.throwable.quickfix.QuickFixException;
import org.auraframework.util.AuraTextUtil;
import org.auraframework.util.json.JsonReader;
import org.auraframework.util.json.JsonStreamReader.JsonParseException;
import com.google.common.collect.Maps;
/**
* The servlet for initialization and actions in Aura.
*
* The sequence of requests is:
* <ol>
* <li>GET(AuraServlet): initial fetch of an aura app/component + Resource Fetches:
* <ul>
* <li>GET(AuraResourceServlet:MANIFESt):optional get the manifest</li>
* <li>GET(AuraResourceServlet:CSS):get the styles for a component</li>
* <li>GET(AuraResourceServlet:JS):get the definitions for a component</li>
* <li>GET(AuraResourceServlet:JSON):???</li>
* </ul>
* </li>
* <li>Application Execution
* <ul>
* <li>GET(AuraServlet:JSON): Fetch additional aura app/component
* <ul>
* <li>GET(AuraResourceServlet:MANIFEST):optional get the manifest</li>
* <li>GET(AuraResourceServlet:CSS):get the styles for a component</li>
* <li>GET(AuraResourceServlet:JS):get the definitions for a component</li>
* <li>GET(AuraResourceServlet:JSON):???</li>
* </ul>
* </li>
* <li>POST(AuraServlet:JSON): Execute actions.</li>
* </ul>
* </li>
* </ol>
*
* Run from aura-jetty project. Pass in these vmargs: <code>
* -Dconfig=${AURA_HOME}/config -Daura.home=${AURA_HOME} -DPORT=9090
* </code>
*
* Exception handling is dealt with in {@link #handleServletException} which should almost always be called when
* exceptions are caught. This routine will use {@link org.auraframework.adapter.ExceptionAdapter ExceptionAdapter} to
* log and rewrite exceptions as necessary.
*/
public class AuraServlet extends AuraBaseServlet {
private static final long serialVersionUID = 2218469644108785216L;
protected final static StringParam tag = new StringParam(AURA_PREFIX + "tag", 128, true);
private static final EnumParam<DefType> defTypeParam = new EnumParam<DefType>(AURA_PREFIX + "deftype", false,
DefType.class);
private final static StringParam messageParam = new StringParam("message", 0, false);
private final static StringParam beaconParam = new StringParam("beaconData", 0, false);
// FIXME: is this really a good idea?
private final static StringParam nocacheParam = new StringParam("nocache", 0, false);
@Override
public void init() throws ServletException {
super.init();
}
/**
* Check for the nocache parameter and redirect as necessary.
*
* Not entirely sure what this is used for (need doco). It is part of the appcache refresh, forcing a reload while
* avoiding the appcache.
*
* It maybe should be done differently (e.g. a nonce).
*
* @param request The request to retrieve the parameter.
* @param response the response (for setting the location header.
* @returns true if we are finished with the request.
*/
private void handleNoCacheRedirect(String nocache, HttpServletResponse response) throws IOException {
//
// FIXME:!!!
// This is part of the appcache refresh, forcing a reload while
// avoiding the appcache. It is here because (fill in the blank).
//
// This should probably be handled a little differently, maybe even
// before we do any checks at all.
//
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
String newLocation = "/";
try {
final URI uri = new URI(nocache);
final String fragment = uri.getFragment();
final String query = uri.getQuery();
final StringBuilder sb = new StringBuilder(uri.getPath());
if(query != null && !query.isEmpty()) {
sb.append("?").append(query);
}
if (fragment != null && !fragment.isEmpty()) {
sb.append("#").append(fragment);
}
newLocation = sb.toString();
} catch (Exception e) {
// This exception should never happen.
// If happened: log a gack and redirect
Aura.getExceptionAdapter().handleException(e);
}
setNoCache(response);
response.setHeader(HttpHeaders.LOCATION, newLocation);
}
/**
* Handle an HTTP GET operation.
*
* The HTTP GET operation is used to retrieve resources from the Aura servlet. It is only used for this purpose,
* where POST is used for actions.
*
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
DefinitionService definitionService;
AuraContext context;
String tagName;
DefType defType;
//
// Initial setup. This should never fail.
//
try {
response.setCharacterEncoding(UTF_ENCODING);
context = Aura.getContextService().getCurrentContext();
response.setContentType(getContentType(context.getFormat()));
definitionService = Aura.getDefinitionService();
} catch (RuntimeException re) {
//
// If we can't get this far, log the exception and bolt.
// We can't do our normal exception handling because
// at this point we simply broke.
//
Aura.getExceptionAdapter().handleException(re);
send404(request, response);
return;
}
DefDescriptor<? extends BaseComponentDef> defDescriptor;
BaseComponentDef def;
//
// Now check and fetch parameters.
// These are not formally part of the Aura API, as this is the initial
// request. All we need are a tag/type or descriptor. Except, of course,
// the special case of nocache, which is required by the appcache handling.
// I would love for a simpler way to be figured out.
//
try {
String nocache = nocacheParam.get(request);
if (nocache != null && !nocache.isEmpty()) {
handleNoCacheRedirect(nocache, response);
return;
}
tagName = tag.get(request);
defType = defTypeParam.get(request, DefType.COMPONENT);
if (tagName == null || tagName.isEmpty()) {
throw new AuraRuntimeException("Invalid request, tag must not be empty");
}
Mode mode = context.getMode();
if (!isValidDefType(defType, mode)) {
send404(request, response);
return;
}
if (context.getFormat() != Format.HTML) {
throw new AuraRuntimeException("Invalid request, GET must use HTML");
}
defDescriptor = definitionService.getDefDescriptor(tagName,
defType == DefType.APPLICATION ? ApplicationDef.class : ComponentDef.class);
} catch (RequestParam.InvalidParamException ipe) {
handleServletException(new SystemErrorException(ipe), false, context, request, response, false);
return;
} catch (RequestParam.MissingParamException mpe) {
handleServletException(new SystemErrorException(mpe), false, context, request, response, false);
return;
} catch (Throwable t) {
handleServletException(new SystemErrorException(t), false, context, request, response, false);
return;
}
// Knowing the app, we can do the HTTP headers, so of which depend on
// the app in play, so we couldn't do this earlier.
setBasicHeaders(defDescriptor, request, response);
try {
context.setFrameworkUID(Aura.getConfigAdapter().getAuraFrameworkNonce());
context.setApplicationDescriptor(defDescriptor);
definitionService.updateLoaded(defDescriptor);
def = definitionService.getDefinition(defDescriptor);
if (!context.isTestMode() && !context.isDevMode()) {
assertAccess(def);
}
} catch (QuickFixException qfe) {
//
// Whoops. we need to set up our preloads correctly here.
//
setupQuickFix(context);
handleServletException(qfe, true, context, request, response, false);
return;
} catch (Throwable t) {
handleServletException(t, false, context, request, response, false);
return;
}
SerializationService serializationService = Aura.getSerializationService();
LoggingService loggingService = Aura.getLoggingService();
try {
if (shouldCacheHTMLTemplate(request)) {
setLongCache(response);
} else {
setNoCache(response);
}
loggingService.startTimer(LoggingService.TIMER_SERIALIZATION);
loggingService.startTimer(LoggingService.TIMER_SERIALIZATION_AURA);
// Prevents Mhtml Xss exploit:
PrintWriter out = response.getWriter();
out.write("\n ");
serializationService.write(def, getComponentAttributes(request),
def.getDescriptor().getDefType().getPrimaryInterface(), out);
} catch (Throwable e) {
handleServletException(e, false, context, request, response, true);
} finally {
loggingService.stopTimer(LoggingService.TIMER_SERIALIZATION_AURA);
loggingService.stopTimer(LoggingService.TIMER_SERIALIZATION);
}
}
private void assertAccess(BaseComponentDef def) throws QuickFixException {
String defaultNamespace = Aura.getConfigAdapter().getDefaultNamespace();
DefDescriptor<?> referencingDescriptor = (defaultNamespace != null && !defaultNamespace.isEmpty())
? Aura.getDefinitionService().getDefDescriptor(String.format("%s:servletAccess", defaultNamespace), ApplicationDef.class)
: null;
Aura.getDefinitionService().getDefRegistry().assertAccess(referencingDescriptor, def);
}
protected boolean isValidDefType(DefType defType, Mode mode) {
return (defType == DefType.APPLICATION || defType == DefType.COMPONENT);
}
private Map<String, Object> getComponentAttributes(HttpServletRequest request) {
Enumeration<String> attributeNames = request.getParameterNames();
Map<String, Object> attributes = new HashMap<String, Object>();
while (attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
if (!name.startsWith(AURA_PREFIX)) {
Object value = new StringParam(name, 0, false).get(request);
attributes.put(name, value);
}
}
return attributes;
}
/**
* @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
IOException {
SerializationService serializationService = Aura.getSerializationService();
LoggingService loggingService = Aura.getLoggingService();
ContextService contextService = Aura.getContextService();
ServerService serverService = Aura.getServerService();
AuraContext context = contextService.getCurrentContext();
response.setCharacterEncoding(UTF_ENCODING);
boolean written = false;
setNoCache(response);
try {
if (context.getFormat() != Format.JSON) {
throw new AuraRuntimeException("Invalid request, post must use JSON");
}
response.setContentType(getContentType(Format.JSON));
String msg = messageParam.get(request);
if (msg == null) {
throw new AuraRuntimeException("Invalid request, no message");
}
//
// handle transaction beacon JSON data
// FIXME: this should be an action.
//
String beaconData = beaconParam.get(request);
if (!"undefined".equals(beaconData) && !AuraTextUtil.isNullEmptyOrWhitespace(beaconData)) {
loggingService.setValue(LoggingService.BEACON_DATA, new JsonReader().read(beaconData));
}
String fwUID = Aura.getConfigAdapter().getAuraFrameworkNonce();
if (!fwUID.equals(context.getFrameworkUID())) {
throw new ClientOutOfSyncException("Framework has been updated");
}
context.setFrameworkUID(fwUID);
Message message;
loggingService.startTimer(LoggingService.TIMER_DESERIALIZATION);
try {
message = serializationService.read(new StringReader(msg), Message.class);
} finally {
loggingService.stopTimer(LoggingService.TIMER_DESERIALIZATION);
}
// The bootstrap action cannot not have a CSRF token so we let it
// through
boolean isBootstrapAction = false;
if (message.getActions().size() == 1) {
Action action = message.getActions().get(0);
String name = action.getDescriptor().getQualifiedName();
if (name.equals("aura://ComponentController/ACTION$getApplication")
|| (name.equals("aura://ComponentController/ACTION$getComponent") && !isProductionMode(context
.getMode()))) {
isBootstrapAction = true;
}
}
if (!isBootstrapAction) {
validateCSRF(csrfToken.get(request));
}
DefDescriptor<? extends BaseComponentDef> applicationDescriptor = context.getApplicationDescriptor();
// Knowing the app, we can do the HTTP headers, so of which depend on
// the app in play, so we couldn't do this
setBasicHeaders(applicationDescriptor, request, response);
if (applicationDescriptor != null) {
// ClientOutOfSync will drop down.
try {
Aura.getDefinitionService().updateLoaded(applicationDescriptor);
} catch (QuickFixException qfe) {
//
// ignore quick fix. If we got a 'new' quickfix, it will be thrown as
// a client out of sync exception, since the UID will not match.
//
}
if (!context.isTestMode() && !context.isDevMode()) {
assertAccess(applicationDescriptor.getDef());
}
}
Map<String, Object> attributes = null;
if (isBootstrapAction) {
attributes = Maps.newHashMap();
attributes.put("token", getToken());
}
PrintWriter out = response.getWriter();
written = true;
out.write(CSRF_PROTECT);
serverService.run(message, context, out, attributes);
} catch (RequestParam.InvalidParamException ipe) {
handleServletException(new SystemErrorException(ipe), false, context, request, response, false);
return;
} catch (RequestParam.MissingParamException mpe) {
handleServletException(new SystemErrorException(mpe), false, context, request, response, false);
return;
} catch (JsonParseException jpe) {
handleServletException(new SystemErrorException(jpe), false, context, request, response, false);
} catch (Exception e) {
handleServletException(e, false, context, request, response, written);
}
}
protected void sendPost404(HttpServletRequest request, HttpServletResponse response) {
throw new NoAccessException("Missing required perms, or tried to access inaccessible namespace.");
}
}