/* * JBoss, Home of Professional Open Source * Copyright 2015, Red Hat, Inc., and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * 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.jboss.weld.probe; import static org.jboss.weld.probe.Strings.TEXT_HTML; import java.io.CharArrayWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.regex.Pattern; import javax.enterprise.inject.Vetoed; import javax.inject.Inject; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import org.jboss.weld.bean.builtin.BeanManagerProxy; import org.jboss.weld.config.ConfigurationKey; import org.jboss.weld.config.WeldConfiguration; import org.jboss.weld.manager.BeanManagerImpl; import org.jboss.weld.probe.Invocation.Type; import org.jboss.weld.probe.InvocationMonitor.Action; import org.jboss.weld.probe.Resource.HttpMethod; import org.jboss.weld.util.reflection.Formats; /** * This filter takes care of the servlet integration. Basically, it: * * <ul> * <li>implements a simple REST API,</li> * <li>allows to group together monitored invocations within the same request,</li> * <li>allows to embed an informative HTML snippet to every response with Content-Type of value <code>text/html</code>.</li> * </ul> * * <p> * An integrator is required to register this filter. The filter should only be mapped to a single URL pattern of value <code>/*</code>. * </p> * * <p> * To disable the clippy support, set {@link ConfigurationKey#PROBE_EMBED_INFO_SNIPPET} to <code>false</code>. It's also possible to use the * {@link ConfigurationKey#PROBE_INVOCATION_MONITOR_EXCLUDE_TYPE} to skip the monitoring. * </p> * * @see ConfigurationKey#PROBE_EMBED_INFO_SNIPPET * @see ConfigurationKey#PROBE_INVOCATION_MONITOR_EXCLUDE_TYPE * @author Martin Kouba */ @Vetoed public class ProbeFilter implements Filter { static final String REST_URL_PATTERN_BASE = "/weld-probe"; static final String WELD_SERVLET_BEAN_MANAGER_KEY = "org.jboss.weld.environment.servlet.javax.enterprise.inject.spi.BeanManager"; @Inject private BeanManagerImpl beanManager; // It shouldn't be necessary to make these fields volatile - see also javax.servlet.GenericServlet.config private String snippetBase; private Probe probe; private JsonDataProvider jsonDataProvider; private boolean skipMonitoring; private Pattern allowRemoteAddressPattern; @Override public void init(FilterConfig filterConfig) throws ServletException { if (beanManager == null) { beanManager = BeanManagerProxy.tryUnwrap(filterConfig.getServletContext().getAttribute(WELD_SERVLET_BEAN_MANAGER_KEY)); if (beanManager == null) { throw ProbeLogger.LOG.probeFilterUnableToOperate(BeanManagerImpl.class); } } ProbeExtension probeExtension = beanManager.getExtension(ProbeExtension.class); if (probeExtension == null) { throw ProbeLogger.LOG.probeFilterUnableToOperate(ProbeExtension.class); } probe = probeExtension.getProbe(); if (!probe.isInitialized()) { throw ProbeLogger.LOG.probeNotInitialized(); } jsonDataProvider = probeExtension.getJsonDataProvider(); WeldConfiguration configuration = beanManager.getServices().get(WeldConfiguration.class); if (configuration.getBooleanProperty(ConfigurationKey.PROBE_EMBED_INFO_SNIPPET)) { snippetBase = initSnippetBase(filterConfig.getServletContext()); } String exclude = configuration.getStringProperty(ConfigurationKey.PROBE_INVOCATION_MONITOR_EXCLUDE_TYPE); skipMonitoring = !exclude.isEmpty() && Pattern.compile(exclude).matcher(ProbeFilter.class.getName()).matches(); String allowRemoteAddress = configuration.getStringProperty(ConfigurationKey.PROBE_ALLOW_REMOTE_ADDRESS); allowRemoteAddressPattern = allowRemoteAddress.isEmpty() ? null : Pattern.compile(allowRemoteAddress); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest)) { chain.doFilter(request, response); return; } final HttpServletRequest httpRequest = (HttpServletRequest) request; final HttpServletResponse httpResponse = (HttpServletResponse) response; if (allowRemoteAddressPattern != null && !allowRemoteAddressPattern.matcher(request.getRemoteAddr()).matches()) { ProbeLogger.LOG.requestDenied(httpRequest.getRequestURI(), request.getRemoteAddr()); httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); return; } final String[] resourcePathParts = getResourcePathParts(httpRequest.getRequestURI(), httpRequest.getServletContext().getContextPath()); if (resourcePathParts != null) { // Probe resource HttpMethod method = HttpMethod.from(httpRequest.getMethod()); if (method == null) { // Unsupported protocol if (httpRequest.getProtocol().endsWith("1.1")) { httpResponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else { httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); } return; } processResourceRequest(httpRequest, httpResponse, method, resourcePathParts); } else { // Application request - init monitoring and embed info snippet if required final Invocation.Builder builder; if (!skipMonitoring) { // Don't initialize a new builder if an entry point already exists builder = InvocationMonitor.initBuilder(false); if (builder != null) { builder.setDeclaringClassName(ProbeFilter.class.getName()); builder.setStart(System.currentTimeMillis()); builder.setMethodName("doFilter"); builder.setType(Type.BUSINESS); builder.setDescription(getDescription(httpRequest)); builder.ignoreIfNoChildren(); } } else { builder = null; } if (snippetBase == null) { FilterAction.of(request, response).doFilter(builder, probe, chain); } else { embedInfoSnippet(httpRequest, httpResponse, builder, chain); } } } @Override public void destroy() { } private void embedInfoSnippet(HttpServletRequest req, HttpServletResponse resp, Invocation.Builder builder, FilterChain chain) throws IOException, ServletException { ResponseWrapper responseWrapper = new ResponseWrapper(resp); FilterAction.of(req, responseWrapper).doFilter(builder, probe, chain); String captured = responseWrapper.getOutput(); if (captured != null && !captured.isEmpty()) { // Writer was used PrintWriter out = resp.getWriter(); if (resp.getContentType() != null && resp.getContentType().startsWith(TEXT_HTML)) { int idx = captured.indexOf("</body>"); if (idx == -1) { // </body> not found out.write(captured); } else { CharArrayWriter writer = new CharArrayWriter(); writer.write(captured.substring(0, idx)); writer.write(snippetBase); if (builder != null && !builder.isIgnored()) { writer.write("See <a style=\"color:#337ab7;text-decoration:underline;\" href=\""); writer.write(req.getServletContext().getContextPath()); // This path must be hardcoded unless we find an easy way to reference the client-specific configuration writer.write(REST_URL_PATTERN_BASE + "/#/invocation/"); writer.write("" + builder.getEntryPointIdx()); writer.write("\" target=\"_blank\">all bean invocations</a> within the HTTP request which rendered this page."); } writer.write("</div>"); writer.write(captured.substring(idx, captured.length())); out.write(writer.toString()); } } else { out.write(captured); } } } private String getDescription(HttpServletRequest req) { StringBuilder builder = new StringBuilder(); builder.append(req.getMethod()); builder.append(' '); builder.append(req.getRequestURI()); String queryString = req.getQueryString(); if (queryString != null) { builder.append('?'); builder.append(queryString); } return builder.toString(); } private String initSnippetBase(ServletContext servletContext) { // Note that we have to use in-line CSS StringBuilder builder = new StringBuilder(); builder.append("<!-- The following snippet was automatically added by Weld, see the documentation to disable this functionality -->"); builder.append( "<div id=\"weld-dev-mode-info\" style=\"position: fixed !important;bottom:0;left:0;width:100%;background-color:#f8f8f8;border:2px solid silver;padding:10px;border-radius:2px;margin:0px;font-size:14px;font-family:sans-serif;color:black;\">"); builder.append("<img alt=\"Weld logo\" style=\"vertical-align: middle;border-width:0px;\" src=\""); builder.append(servletContext.getContextPath()); builder.append(REST_URL_PATTERN_BASE + "/client/weld_icon_32x.png\">"); builder.append("  Running on Weld <span style=\"color:gray\">"); builder.append(Formats.getSimpleVersion()); builder.append( "</span>. The development mode is <span style=\"color:white;background-color:#d62728;padding:6px;border-radius:4px;font-size:12px;\">ENABLED</span>. Inspect your application with <a style=\"color:#337ab7;text-decoration:underline;\" href=\""); builder.append(servletContext.getContextPath()); builder.append(REST_URL_PATTERN_BASE); builder.append("\" target=\"_blank\">Probe Development Tool</a>."); builder.append( " <button style=\"float:right;background-color:#f8f8f8;border:1px solid silver; color:gray;border-radius:4px;padding:4px 10px 4px 10px;margin-left:2em;font-weight: bold;\" onclick=\"document.getElementById('weld-dev-mode-info').style.display='none';\">x</button>"); return builder.toString(); } private void processResourceRequest(HttpServletRequest req, HttpServletResponse resp, HttpMethod httpMethod, String[] resourcePathParts) throws IOException { Resource resource; if (resourcePathParts.length == 0) { resource = Resource.CLIENT_RESOURCE; } else { resource = matchResource(resourcePathParts); if (resource == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } } ProbeLogger.LOG.resourceMatched(resource, req.getRequestURI()); resource.handle(httpMethod, jsonDataProvider, resourcePathParts, req, resp); } private Resource matchResource(String[] resourcePathParts) { for (Resource resource : Resource.values()) { if (resource.matches(resourcePathParts)) { return resource; } } return null; } /** * * @return the array of resource path parts or <code>null</code> if the given URI does not represent a Probe resource */ static String[] getResourcePathParts(String requestUri, String contextPath) { final String path = requestUri.substring(contextPath.length(), requestUri.length()); if (path.startsWith(REST_URL_PATTERN_BASE)) { return Resource.splitPath(path.substring(REST_URL_PATTERN_BASE.length(), path.length())); } return null; } private static class ResponseWrapper extends HttpServletResponseWrapper { private final CharArrayWriter output; private final PrintWriter writer; ResponseWrapper(HttpServletResponse response) { super(response); output = new CharArrayWriter(); writer = new PrintWriter(output); } public PrintWriter getWriter() { return writer; } String getOutput() { return output.toString(); } } private static class FilterAction extends Action<FilterChain> { private static FilterAction of(ServletRequest request, ServletResponse response) { return new FilterAction(request, response); } private final ServletRequest request; private final ServletResponse response; private FilterAction(ServletRequest request, ServletResponse response) { this.request = request; this.response = response; } @Override protected Object proceed(FilterChain chain) throws Exception { chain.doFilter(request, response); return null; } void doFilter(Invocation.Builder builder, Probe probe, FilterChain chain) throws ServletException, IOException { if (builder == null) { chain.doFilter(request, response); } else { try { perform(builder, probe, chain); } catch (Exception e) { throw new ServletException(e); } } } } }