Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.glowroot.local.ui.HttpServerHandler Maven / Gradle / Ivy
/*
* Copyright 2013-2015 the original author or authors.
*
* 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.glowroot.local.ui;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.glowroot.shaded.fasterxml.jackson.core.JsonFactory;
import org.glowroot.shaded.fasterxml.jackson.core.JsonGenerator;
import org.glowroot.shaded.google.common.annotations.VisibleForTesting;
import org.glowroot.shaded.google.common.base.Charsets;
import org.glowroot.shaded.google.common.base.Joiner;
import org.glowroot.shaded.google.common.collect.ImmutableList;
import org.glowroot.shaded.google.common.collect.ImmutableMap;
import org.glowroot.shaded.google.common.collect.Lists;
import org.glowroot.shaded.google.common.io.CharStreams;
import org.glowroot.shaded.google.common.io.Resources;
import org.glowroot.shaded.google.common.net.MediaType;
import org.glowroot.shaded.netty.buffer.ByteBuf;
import org.glowroot.shaded.netty.buffer.Unpooled;
import org.glowroot.shaded.netty.channel.Channel;
import org.glowroot.shaded.netty.channel.ChannelFuture;
import org.glowroot.shaded.netty.channel.ChannelFutureListener;
import org.glowroot.shaded.netty.channel.ChannelHandler.Sharable;
import org.glowroot.shaded.netty.channel.ChannelHandlerContext;
import org.glowroot.shaded.netty.channel.ChannelInboundHandlerAdapter;
import org.glowroot.shaded.netty.channel.group.ChannelGroup;
import org.glowroot.shaded.netty.channel.group.DefaultChannelGroup;
import org.glowroot.shaded.netty.handler.codec.http.DefaultFullHttpResponse;
import org.glowroot.shaded.netty.handler.codec.http.FullHttpRequest;
import org.glowroot.shaded.netty.handler.codec.http.FullHttpResponse;
import org.glowroot.shaded.netty.handler.codec.http.HttpHeaders;
import org.glowroot.shaded.netty.handler.codec.http.HttpHeaders.Names;
import org.glowroot.shaded.netty.handler.codec.http.HttpHeaders.Values;
import org.glowroot.shaded.netty.handler.codec.http.HttpRequest;
import org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus;
import org.glowroot.shaded.netty.handler.codec.http.QueryStringDecoder;
import org.glowroot.shaded.netty.util.concurrent.GlobalEventExecutor;
import org.glowroot.shaded.h2.api.ErrorCode;
import org.immutables.value.Value;
import org.glowroot.shaded.slf4j.Logger;
import org.glowroot.shaded.slf4j.LoggerFactory;
import org.glowroot.common.Reflections;
import static org.glowroot.shaded.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.shaded.google.common.base.Preconditions.checkState;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.OK;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.REQUEST_TIMEOUT;
import static org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static org.glowroot.shaded.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
@Sharable
class HttpServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
private static final JsonFactory jsonFactory = new JsonFactory();
private static final long TEN_YEARS = DAYS.toMillis(365 * 10);
private static final long ONE_DAY = DAYS.toMillis(1);
private static final long FIVE_MINUTES = MINUTES.toMillis(5);
private static final String RESOURCE_BASE = "org/glowroot/local/ui/app-dist";
// only null when running tests with glowroot.ui.skip=true (e.g. travis "deploy" build)
private static final @Nullable String RESOURCE_BASE_URL_PREFIX;
private static final ImmutableMap mediaTypes =
ImmutableMap.builder()
.put("html", MediaType.HTML_UTF_8)
.put("js", MediaType.JAVASCRIPT_UTF_8)
.put("css", MediaType.CSS_UTF_8)
.put("ico", MediaType.ICO)
.put("woff", MediaType.WOFF)
.put("woff2", MediaType.create("application", "font-woff2"))
.put("map", MediaType.JSON_UTF_8)
.build();
static {
URL resourceBaseUrl = getUrlForPath(RESOURCE_BASE);
if (resourceBaseUrl == null) {
RESOURCE_BASE_URL_PREFIX = null;
} else {
RESOURCE_BASE_URL_PREFIX = resourceBaseUrl.toExternalForm();
}
}
private final ChannelGroup allChannels;
private final LayoutService layoutService;
private final ImmutableMap httpServices;
private final ImmutableList jsonServiceMappings;
private final HttpSessionManager httpSessionManager;
private final ThreadLocal*@Nullable*/Channel> currentChannel =
new ThreadLocal*@Nullable*/Channel>();
HttpServerHandler(LayoutService layoutService, Map httpServices,
HttpSessionManager httpSessionManager, List jsonServices) {
this.layoutService = layoutService;
this.httpServices = ImmutableMap.copyOf(httpServices);
this.httpSessionManager = httpSessionManager;
List jsonServiceMappings = Lists.newArrayList();
for (Object jsonService : jsonServices) {
for (Method method : jsonService.getClass().getDeclaredMethods()) {
GET annotationGET = method.getAnnotation(GET.class);
if (annotationGET != null) {
jsonServiceMappings.add(JsonServiceMapping.builder()
.httpMethod(HttpMethod.GET)
.path(annotationGET.value())
.service(jsonService)
.methodName(method.getName())
.build());
}
POST annotationPOST = method.getAnnotation(POST.class);
if (annotationPOST != null) {
jsonServiceMappings.add(JsonServiceMapping.builder()
.httpMethod(HttpMethod.POST)
.path(annotationPOST.value())
.service(jsonService)
.methodName(method.getName())
.build());
}
}
}
this.jsonServiceMappings = ImmutableList.copyOf(jsonServiceMappings);
allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
allChannels.add(ctx.channel());
super.channelActive(ctx);
}
void close() {
allChannels.close().awaitUninterruptibly();
}
void closeAllButCurrent() {
Channel current = currentChannel.get();
for (Channel channel : allChannels) {
if (channel != current) {
channel.close().awaitUninterruptibly();
}
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
FullHttpRequest request = (FullHttpRequest) msg;
logger.debug("messageReceived(): request.uri={}", request.getUri());
Channel channel = ctx.channel();
currentChannel.set(channel);
try {
FullHttpResponse response = handleRequest(ctx, request);
if (response != null) {
sendFullResponse(ctx, request, response);
}
} catch (Exception f) {
logger.error(f.getMessage(), f);
FullHttpResponse response = newHttpResponseWithStackTrace(f, INTERNAL_SERVER_ERROR,
null);
sendFullResponse(ctx, request, response);
} finally {
currentChannel.remove();
request.release();
}
}
@SuppressWarnings("argument.type.incompatible")
private void sendFullResponse(ChannelHandlerContext ctx, FullHttpRequest request,
FullHttpResponse response) {
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (httpSessionManager.getSessionId(request) != null
&& httpSessionManager.getAuthenticatedUser(request) == null
&& !response.headers().contains("Set-Cookie")) {
httpSessionManager.deleteSessionCookie(response);
}
response.headers().add("Glowroot-Layout-Version", layoutService.getLayoutVersion());
if (response.headers().get("Glowroot-Port-Changed") != null) {
// current connection is the only open channel on the old port, keepAlive=false will add
// the listener below to close the channel after the response completes
//
// remove the hacky header, no need to send it back to client
response.headers().remove("Glowroot-Port-Changed");
response.headers().add("Connection", "close");
keepAlive = false;
}
response.headers().add(Names.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive && !request.getProtocolVersion().isKeepAliveDefault()) {
response.headers().set(Names.CONNECTION, Values.KEEP_ALIVE);
}
ChannelFuture f = ctx.write(response);
if (!keepAlive) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (HttpServices.shouldLogException(cause)) {
logger.warn(cause.getMessage(), cause);
}
ctx.close();
}
private @Nullable FullHttpResponse handleRequest(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
logger.debug("handleRequest(): request.uri={}", request.getUri());
QueryStringDecoder decoder = new QueryStringDecoder(request.getUri());
String path = decoder.path();
logger.debug("handleRequest(): path={}", path);
FullHttpResponse response = handleIfLoginOrLogoutRequest(path, request);
if (response != null) {
return response;
}
HttpService httpService = getHttpService(path);
if (httpService != null) {
return handleHttpService(ctx, request, httpService);
}
JsonServiceMatcher jsonServiceMatcher = getJsonServiceMatcher(request, path);
if (jsonServiceMatcher != null) {
return handleJsonServiceMappings(request, jsonServiceMatcher.jsonServiceMapping(),
jsonServiceMatcher.matcher());
}
return handleStaticResource(path, request);
}
private @Nullable FullHttpResponse handleIfLoginOrLogoutRequest(String path,
FullHttpRequest request) throws IOException {
if (path.equals("/backend/authenticated-user")) {
// this is only used when running under 'grunt serve'
return handleAuthenticatedUserRequest(request);
}
if (path.equals("/backend/admin-login")) {
return httpSessionManager.login(request, true);
}
if (path.equals("/backend/read-only-login")) {
return httpSessionManager.login(request, false);
}
if (path.equals("/backend/sign-out")) {
return httpSessionManager.signOut(request);
}
return null;
}
private FullHttpResponse handleAuthenticatedUserRequest(FullHttpRequest request) {
String authenticatedUser = httpSessionManager.getAuthenticatedUser(request);
if (authenticatedUser == null) {
return HttpServices.createJsonResponse("null", OK);
} else {
return HttpServices.createJsonResponse("\"" + authenticatedUser + "\"", OK);
}
}
private @Nullable HttpService getHttpService(String path) throws Exception {
for (Entry entry : httpServices.entrySet()) {
Matcher matcher = entry.getKey().matcher(path);
if (matcher.matches()) {
return entry.getValue();
}
}
return null;
}
private @Nullable FullHttpResponse handleHttpService(ChannelHandlerContext ctx,
FullHttpRequest request, HttpService httpService) throws Exception {
if (!httpSessionManager.hasReadAccess(request)
&& !(httpService instanceof UnauthenticatedHttpService)) {
return handleNotAuthenticated(request);
}
boolean isGetRequest = request.getMethod().name().equals(HttpMethod.GET.name());
if (!isGetRequest && !httpSessionManager.hasAdminAccess(request)) {
return handleNotAuthorized();
}
return httpService.handleRequest(ctx, request);
}
private @Nullable JsonServiceMatcher getJsonServiceMatcher(FullHttpRequest request,
String path) {
for (JsonServiceMapping jsonServiceMapping : jsonServiceMappings) {
if (!jsonServiceMapping.httpMethod().name().equals(request.getMethod().name())) {
continue;
}
Matcher matcher = jsonServiceMapping.pattern().matcher(path);
if (matcher.matches()) {
return JsonServiceMatcher.of(jsonServiceMapping, matcher);
}
}
return null;
}
private FullHttpResponse handleJsonServiceMappings(FullHttpRequest request,
JsonServiceMapping jsonServiceMapping, Matcher matcher) {
if (!httpSessionManager.hasReadAccess(request)) {
return handleNotAuthenticated(request);
}
boolean isGetRequest = request.getMethod().name().equals(HttpMethod.GET.name());
if (!isGetRequest && !httpSessionManager.hasAdminAccess(request)) {
return handleNotAuthorized();
}
String requestText = getRequestText(request);
String[] args = new String[matcher.groupCount()];
for (int i = 0; i < args.length; i++) {
String group = matcher.group(i + 1);
checkNotNull(group);
args[i] = group;
}
logger.debug("handleJsonRequest(): serviceMethodName={}, args={}, requestText={}",
jsonServiceMapping.methodName(), args, requestText);
Object responseObject;
try {
responseObject = callMethod(jsonServiceMapping.service(),
jsonServiceMapping.methodName(), args, requestText);
} catch (Exception e) {
return newHttpResponseFromException(e);
}
return buildJsonResponse(responseObject);
}
private FullHttpResponse buildJsonResponse(@Nullable Object responseObject) {
FullHttpResponse response;
if (responseObject == null) {
response = new DefaultFullHttpResponse(HTTP_1_1, OK);
} else if (responseObject instanceof FullHttpResponse) {
response = (FullHttpResponse) responseObject;
} else if (responseObject instanceof String) {
ByteBuf content = Unpooled.copiedBuffer(responseObject.toString(), Charsets.ISO_8859_1);
response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
} else {
logger.warn("unexpected type of json service response: {}",
responseObject.getClass().getName());
return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
}
response.headers().add(Names.CONTENT_TYPE, MediaType.JSON_UTF_8);
HttpServices.preventCaching(response);
return response;
}
private FullHttpResponse handleNotAuthenticated(HttpRequest request) {
if (httpSessionManager.getSessionId(request) != null) {
return HttpServices.createJsonResponse("{\"timedOut\":true}", UNAUTHORIZED);
} else {
return new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED);
}
}
private FullHttpResponse handleNotAuthorized() {
return new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
}
private FullHttpResponse handleStaticResource(String path, HttpRequest request)
throws IOException {
URL url = getSecureUrlForPath(RESOURCE_BASE + path);
if (url == null) {
logger.warn("unexpected path: {}", path);
return new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND);
}
Date expires = getExpiresForPath(path);
if (request.headers().contains(Names.IF_MODIFIED_SINCE) && expires == null) {
// all static resources without explicit expires are versioned and can be safely
// cached forever
return new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
}
ByteBuf content = Unpooled.copiedBuffer(Resources.toByteArray(url));
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
if (expires != null) {
response.headers().add(Names.EXPIRES, expires);
} else {
response.headers().add(Names.LAST_MODIFIED, new Date(0));
response.headers().add(Names.EXPIRES, new Date(System.currentTimeMillis() + TEN_YEARS));
}
int extensionStartIndex = path.lastIndexOf('.');
checkState(extensionStartIndex != -1, "found path under %s with no extension: %s",
RESOURCE_BASE, path);
String extension = path.substring(extensionStartIndex + 1);
MediaType mediaType = mediaTypes.get(extension);
checkNotNull(mediaType, "found extension under %s with no media type: %s", RESOURCE_BASE,
extension);
response.headers().add(Names.CONTENT_TYPE, mediaType);
response.headers().add(Names.CONTENT_LENGTH, Resources.toByteArray(url).length);
return response;
}
private static @Nullable URL getSecureUrlForPath(String path) {
URL url = getUrlForPath(path);
if (url != null && RESOURCE_BASE_URL_PREFIX != null
&& url.toExternalForm().startsWith(RESOURCE_BASE_URL_PREFIX)) {
return url;
}
return null;
}
private static @Nullable URL getUrlForPath(String path) {
ClassLoader classLoader = HttpServerHandler.class.getClassLoader();
if (classLoader == null) {
return ClassLoader.getSystemResource(path);
} else {
return classLoader.getResource(path);
}
}
private static @Nullable Date getExpiresForPath(String path) {
if (path.startsWith("org/glowroot/local/ui/app-dist/favicon.")) {
return new Date(System.currentTimeMillis() + ONE_DAY);
} else if (path.endsWith(".js.map") || path.startsWith("/sources/")) {
// javascript source maps and source files are not versioned
return new Date(System.currentTimeMillis() + FIVE_MINUTES);
} else {
return null;
}
}
@VisibleForTesting
static FullHttpResponse newHttpResponseFromException(Exception exception) {
Exception e = exception;
if (e instanceof InvocationTargetException) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
e = (Exception) cause;
}
}
if (e instanceof JsonServiceException) {
// this is an "expected" exception, no need to log
JsonServiceException jsonServiceException = (JsonServiceException) e;
return newHttpResponseWithMessage(jsonServiceException.getStatus(),
jsonServiceException.getMessage());
}
logger.error(e.getMessage(), e);
if (e instanceof SQLException
&& ((SQLException) e).getErrorCode() == ErrorCode.STATEMENT_WAS_CANCELED) {
return newHttpResponseWithMessage(REQUEST_TIMEOUT,
"Query timed out (timeout is configurable under Configuration > Advanced)");
}
return newHttpResponseWithStackTrace(e, INTERNAL_SERVER_ERROR, null);
}
private static FullHttpResponse newHttpResponseWithMessage(HttpResponseStatus status,
@Nullable String message) {
// this is an "expected" exception, no need to send back stack trace
StringBuilder sb = new StringBuilder();
try {
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeStringField("message", message);
jg.writeEndObject();
jg.close();
return HttpServices.createJsonResponse(sb.toString(), status);
} catch (IOException f) {
logger.error(f.getMessage(), f);
return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
}
}
private static FullHttpResponse newHttpResponseWithStackTrace(Exception e,
HttpResponseStatus status, @Nullable String simplifiedMessage) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
StringBuilder sb = new StringBuilder();
try {
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
String message;
if (simplifiedMessage == null) {
Throwable cause = e;
Throwable childCause = cause.getCause();
while (childCause != null) {
cause = childCause;
childCause = cause.getCause();
}
message = cause.getMessage();
} else {
message = simplifiedMessage;
}
jg.writeStringField("message", message);
jg.writeStringField("stackTrace", sw.toString());
jg.writeEndObject();
jg.close();
return HttpServices.createJsonResponse(sb.toString(), status);
} catch (IOException f) {
logger.error(f.getMessage(), f);
return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
}
}
private static @Nullable Object callMethod(Object object, String methodName, String[] args,
String requestText) throws Exception {
List> parameterTypes = Lists.newArrayList();
List parameters = Lists.newArrayList();
for (int i = 0; i < args.length; i++) {
parameterTypes.add(String.class);
parameters.add(args[i]);
}
Method method = null;
try {
method = Reflections.getDeclaredMethod(object.getClass(), methodName,
parameterTypes.toArray(new Class[parameterTypes.size()]));
} catch (Exception e) {
// log exception at trace level
logger.trace(e.getMessage(), e);
// try again with requestText
parameterTypes.add(String.class);
parameters.add(requestText);
try {
method = Reflections.getDeclaredMethod(object.getClass(), methodName,
parameterTypes.toArray(new Class[parameterTypes.size()]));
} catch (Exception f) {
// log exception at debug level
logger.trace(f.getMessage(), f);
throw new NoSuchMethodException(methodName);
}
}
if (logger.isDebugEnabled()) {
String params = Joiner.on(", ").join(parameters);
logger.debug("{}.{}(): {}", object.getClass().getSimpleName(), methodName, params);
}
return Reflections.invoke(method, object,
parameters.toArray(new Object[parameters.size()]));
}
private static String getRequestText(FullHttpRequest request) {
if (request.getMethod() == org.glowroot.shaded.netty.handler.codec.http.HttpMethod.POST) {
return request.content().toString(Charsets.ISO_8859_1);
} else {
int index = request.getUri().indexOf('?');
if (index == -1) {
return "";
} else {
return request.getUri().substring(index + 1);
}
}
}
@Value.Immutable
static abstract class JsonServiceMatcherBase {
@Value.Parameter
abstract JsonServiceMapping jsonServiceMapping();
@Value.Parameter
abstract Matcher matcher();
}
@Value.Immutable
static abstract class JsonServiceMappingBase {
abstract HttpMethod httpMethod();
abstract String path();
abstract Object service();
abstract String methodName();
@Value.Derived
Pattern pattern() {
String path = path();
if (path.contains("(")) {
return Pattern.compile(path);
} else {
return Pattern.compile(Pattern.quote(path));
}
}
}
static enum HttpMethod {
GET, POST
}
}