/*
* Copyright 2009 Fred Sauer 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 com.allen_sauer.gwt.log.server;
import com.google.gwt.core.server.StackTraceDeobfuscator;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.allen_sauer.gwt.log.client.Log;
import com.allen_sauer.gwt.log.client.RemoteLoggerService;
import com.allen_sauer.gwt.log.shared.LogRecord;
import com.allen_sauer.gwt.log.shared.WrappedClientThrowable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Default remote logger servlet, which can be configured as a {@code web.xml} servlet.
*/
@SuppressWarnings("serial")
public class RemoteLoggerServlet extends RemoteServiceServlet implements RemoteLoggerService {
/**
* HTTP header for cross-domain XHR.
*/
private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
/**
* HTTP header for cross-domain XHR.
*/
private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
/**
* HTTP header and {@code init-param} parameter name to specify allowed cross domain origins.
*/
private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
/**
* Deprecated symbolMaps intialization parameter.
*/
private static final String PARAMETER_SYMBOL_MAPS = "symbolMaps";
/**
* Location of symbolMaps directory as generated by the <code>-extra</code> GWT compile parameter
* provided by the file system.
*
* @see StackTraceDeobfuscator#fromFileSystem(String)
*/
private static final String PARAMETER_SYMBOL_MAPS_FILE_SYSTEM = "symbolMapsFileSystem";
/**
* Location of symbolMaps directory as generated by the <code>-extra</code> GWT compile parameter
* provided by a resource path.
*
* @see StackTraceDeobfuscator#fromResource(String)
*/
private static final String PARAMETER_SYMBOL_MAPS_RESOURCE_PATH = "symbolMapsResourcePath";
/**
* Location of symbolMaps directory as generated by the <code>-extra</code> GWT compile parameter
* provided via a URL.
*
* @see StackTraceDeobfuscator#fromUrl(java.net.URL)
*/
private static final String PARAMETER_SYMBOL_MAPS_URL = "symbolMapsResourceUrl";
/**
* Non-RFC standard header. See http://en.wikipedia.org/wiki/X-Forwarded-For
*/
private static final String X_FORWARDED_FOR = "X-Forwarded-For";
/**
* The {@code init-param} parameter value of
* {@value RemoteLoggerServlet#ACCESS_CONTROL_ALLOW_ORIGIN}.
*/
private String accessControlAllowOriginHeader;
private List<StackTraceDeobfuscator> deobfuscatorList;
private final HashSet<String> permutationStrongNamesChecked = new HashSet<String>();
@Override
public final void init(ServletConfig config) throws ServletException {
super.init(config);
deobfuscatorList = new ArrayList<StackTraceDeobfuscator>();
for (@SuppressWarnings("unchecked")
Enumeration<String> e = config.getInitParameterNames(); e.hasMoreElements();) {
String name = e.nextElement();
String value = config.getInitParameter(name);
if (name.startsWith(PARAMETER_SYMBOL_MAPS_FILE_SYSTEM)) {
deobfuscatorList.add(StackTraceDeobfuscator.fromFileSystem(value));
} else if (name.startsWith(PARAMETER_SYMBOL_MAPS_RESOURCE_PATH)) {
deobfuscatorList.add(StackTraceDeobfuscator.fromResource(value));
} else if (name.startsWith(PARAMETER_SYMBOL_MAPS_URL)) {
try {
URL url = new URL(value);
deobfuscatorList.add(StackTraceDeobfuscator.fromUrl(url));
} catch (MalformedURLException ex) {
Log.error("Servlet configuration parameter '" + name + "' specifies invalid URL '"
+ value + "'", ex);
}
} else if (name.startsWith(PARAMETER_SYMBOL_MAPS)) {
Log.warn("Servlet configuration parameter '" + name + "' is no longer supported");
}
}
if (deobfuscatorList.isEmpty()) {
Log.warn("In order to enable stack trace deobfuscation, please specify the '"
+ PARAMETER_SYMBOL_MAPS + "' <init-param> for the " + RemoteLoggerServlet.class.getName()
+ " servlet in your web.xml");
}
accessControlAllowOriginHeader = config.getInitParameter(ACCESS_CONTROL_ALLOW_ORIGIN);
}
@Override
public final ArrayList<LogRecord> log(ArrayList<LogRecord> logRecords) {
for (LogRecord record : logRecords) {
try {
HttpServletRequest request = getThreadLocalRequest();
record.set("remoteAddr", request.getRemoteAddr());
String xForwardedFor = request.getHeader(X_FORWARDED_FOR);
if (xForwardedFor != null) {
record.set(X_FORWARDED_FOR, xForwardedFor);
}
deobfuscate(record);
Log.log(record);
} catch (RuntimeException e) {
System.err.println("Failed to log message due to " + e.toString());
e.printStackTrace();
}
}
return shouldReturnDeobfuscatedStackTraceToClient() ? logRecords : null;
}
/**
* If the {@value #ACCESS_CONTROL_ALLOW_ORIGIN} servlet {@code init-param} is set, handle
* preflight {@code OPTIONS} requests which are sent by the browser before sending a cross-domain
* XHR.
*
* @param request the current HTTP request
* @param response the current HTTP response
* @throws ServletException see super implementation
* @throws IOException see super implementation
*/
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (!maybeSetAccessControlAllowHeaders(request, response)) {
super.doOptions(request, response);
return;
}
}
/**
* Method which returns the {@value #ACCESS_CONTROL_ALLOW_ORIGIN}, or {@code null} if no
* cross-domain access control headers should be set. This classes uses the
* {@link #ACCESS_CONTROL_ALLOW_ORIGIN} {@code init-param} configuration parameter. Subclasses may
* override this method implementation.
*
* @param request the current HTTP request
* @return the {@value #ACCESS_CONTROL_ALLOW_ORIGIN} which should be set in response to the
* current {@link #getThreadLocalResponse()}
*/
protected String getAccessControlAllowOriginHeader(HttpServletRequest request) {
return accessControlAllowOriginHeader;
}
/**
* Ensures that the the RPC response contains the necessary access control headers for
* cross-domain access.
*
* @param serializedResponse the serialized RPC response
*/
@Override
protected void onAfterResponseSerialized(String serializedResponse) {
super.onAfterResponseSerialized(serializedResponse);
maybeSetAccessControlAllowHeaders(getThreadLocalRequest(), getThreadLocalResponse());
}
/**
* Override this method to prevent clients from receiving deobfuscated JavaScript stack traces.
* For example, you may choose to only allow (logged in) developers to access resymbolized stack
* traces.
*
* @see #getThreadLocalRequest()
* @return true if the deobfuscated stack traces should be returned to the client
*/
protected boolean shouldReturnDeobfuscatedStackTraceToClient() {
return true;
}
private void deobfuscate(LogRecord record) {
WrappedClientThrowable wrappedClientThrowable = record.getModifiableWrappedClientThrowable();
deobfuscate(wrappedClientThrowable);
}
private void deobfuscate(WrappedClientThrowable wrappedClientThrowable) {
if (wrappedClientThrowable == null) {
// no throwable to deobfuscate
return;
}
// recursive
deobfuscate(wrappedClientThrowable.getCause());
String permutationStrongName = getPermutationStrongName();
if ("HostedMode".equals(permutationStrongName)) {
// For Development Mode
return;
}
StackTraceElement[] originalStackTrace = wrappedClientThrowable.getClientStackTrace();
StackTraceElement[] deobfuscatedStackTrace = originalStackTrace;
for (StackTraceDeobfuscator deobf : deobfuscatorList) {
deobfuscatedStackTrace = deobf.resymbolize(deobfuscatedStackTrace, permutationStrongName);
}
// Verify each permutation once that a symbolMap is available
if (permutationStrongNamesChecked.add(permutationStrongName)) {
if (equal(originalStackTrace, deobfuscatedStackTrace)) {
Log.warn("Failed to deobfuscate stack trace for permutation " + permutationStrongName
+ ". Verify that the corresponding symbolMap is available.");
}
}
wrappedClientThrowable.setClientStackTrace(deobfuscatedStackTrace);
}
private boolean equal(StackTraceElement[] st1, StackTraceElement[] st2) {
for (int i = 0; i < st2.length; i++) {
if (!st1[i].equals(st2[i])) {
return false;
}
}
return true;
}
/**
* Sets the {@value #ACCESS_CONTROL_ALLOW_HEADERS}, {@value #ACCESS_CONTROL_ALLOW_METHODS}, and
* {@value #ACCESS_CONTROL_ALLOW_ORIGIN} HTTP headers, if the {@link #ACCESS_CONTROL_ALLOW_ORIGIN}
* when cross-domain is enabled via the {@code init-param} parameter in {@code web.xml}. Returns
* {@code true} of the headers were set, otherwise {@code false}.
*
* @param request the current HTTP request
* @param response the current HTTP servlet response to which the headers can be added
* @return true if access control headers were set
*/
private boolean maybeSetAccessControlAllowHeaders(HttpServletRequest request,
HttpServletResponse response) {
String origin = getAccessControlAllowOriginHeader(request);
if (origin == null) {
return false;
}
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "POST");
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS,
"X-GWT-Module-Base, X-GWT-Permutation, Content-Type");
return true;
}
}