| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.knox.gateway; |
| |
| import java.io.IOException; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.UUID; |
| 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.HttpServletRequestWrapper; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.knox.gateway.audit.api.Action; |
| import org.apache.knox.gateway.audit.api.ActionOutcome; |
| import org.apache.knox.gateway.audit.api.AuditContext; |
| import org.apache.knox.gateway.audit.api.AuditService; |
| import org.apache.knox.gateway.audit.api.AuditServiceFactory; |
| import org.apache.knox.gateway.audit.api.Auditor; |
| import org.apache.knox.gateway.audit.api.CorrelationContext; |
| import org.apache.knox.gateway.audit.api.CorrelationService; |
| import org.apache.knox.gateway.audit.api.CorrelationServiceFactory; |
| import org.apache.knox.gateway.audit.api.ResourceType; |
| import org.apache.knox.gateway.audit.log4j.audit.AuditConstants; |
| import org.apache.knox.gateway.audit.log4j.correlation.Log4jCorrelationContext; |
| import org.apache.knox.gateway.config.GatewayConfig; |
| import org.apache.knox.gateway.filter.AbstractGatewayFilter; |
| import org.apache.knox.gateway.i18n.messages.MessagesFactory; |
| import org.apache.knox.gateway.i18n.resources.ResourcesFactory; |
| import org.apache.knox.gateway.topology.Topology; |
| import org.apache.knox.gateway.util.ServletRequestUtils; |
| import org.apache.knox.gateway.util.urltemplate.Matcher; |
| import org.apache.knox.gateway.util.urltemplate.Parser; |
| import org.apache.knox.gateway.util.urltemplate.Template; |
| |
| import static org.apache.knox.gateway.filter.CorrelationHandler.REQUEST_ID_HEADER_NAME; |
| |
| public class GatewayFilter implements Filter { |
| |
| private static final FilterChain EMPTY_CHAIN = new FilterChain() { |
| @Override |
| public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse ) throws IOException, ServletException { |
| } |
| }; |
| |
| private static final GatewayMessages LOG = MessagesFactory.get( GatewayMessages.class ); |
| private static final GatewayResources RES = ResourcesFactory.get( GatewayResources.class ); |
| private static AuditService auditService = AuditServiceFactory.getAuditService(); |
| private static Auditor auditor = auditService.getAuditor( |
| AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME, |
| AuditConstants.KNOX_COMPONENT_NAME ); |
| |
| private Set<Holder> holders; |
| private Matcher<Chain> chains; |
| private FilterConfig config; |
| |
| public GatewayFilter() { |
| holders = new HashSet<>(); |
| chains = new Matcher<>(); |
| } |
| |
| @Override |
| public void init( FilterConfig filterConfig ) throws ServletException { |
| this.config = filterConfig; |
| } |
| |
| @Override |
| public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { |
| doFilter( servletRequest, servletResponse ); |
| try { |
| if (filterChain != null) { |
| filterChain.doFilter(servletRequest, servletResponse); |
| } |
| } finally { |
| auditLog(servletRequest, servletResponse); |
| // Make sure to destroy the correlationContext to prevent threading issues |
| CorrelationServiceFactory.getCorrelationService().detachContext(); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse ) throws IOException, ServletException { |
| HttpServletRequest httpRequest = (HttpServletRequest)servletRequest; |
| HttpServletResponse httpResponse = (HttpServletResponse)servletResponse; |
| |
| //TODO: The resulting pathInfo + query needs to be added to the servlet context somehow so that filters don't need to rebuild it. This is done in HttpClientDispatch right now for example. |
| String path = httpRequest.getPathInfo(); |
| String requestPath = ServletRequestUtils.getRequestPath(httpRequest); |
| String requestPathWithQuery = ServletRequestUtils.getRequestPathWithQuery(httpRequest); |
| |
| Template pathWithQueryTemplate; |
| try { |
| pathWithQueryTemplate = Parser.parseLiteral( requestPathWithQuery ); |
| } catch( URISyntaxException e ) { |
| throw new ServletException( e ); |
| } |
| String contextWithPathAndQuery = ServletRequestUtils.getContextPathWithQuery(httpRequest); |
| LOG.receivedRequest( httpRequest.getMethod(), requestPath ); |
| |
| servletRequest.setAttribute( |
| AbstractGatewayFilter.SOURCE_REQUEST_URL_ATTRIBUTE_NAME, pathWithQueryTemplate ); |
| servletRequest.setAttribute( |
| AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME, contextWithPathAndQuery ); |
| |
| Matcher<Chain>.Match match = chains.match( pathWithQueryTemplate ); |
| |
| // if there was no match then look for a default service for the topology |
| if (match == null) { |
| Topology topology = (Topology) servletRequest.getServletContext().getAttribute("org.apache.knox.gateway.topology"); |
| if (topology != null) { |
| String defaultServicePath = topology.getDefaultServicePath(); |
| if (defaultServicePath != null) { |
| try { |
| String newPathWithQuery = defaultServicePath + "/" + pathWithQueryTemplate; |
| match = chains.match(Parser.parseLiteral(newPathWithQuery)); |
| String origUrl = ((HttpServletRequest) servletRequest).getRequestURL().toString(); |
| String url = origUrl; |
| if (path == null || "/".equals(path)) { |
| url += defaultServicePath; |
| } else { |
| int index = origUrl.indexOf(path); |
| url = origUrl.substring(0, index) + "/" + defaultServicePath + path; |
| } |
| servletRequest = new ForwardedRequest((HttpServletRequest) servletRequest, |
| defaultServicePath, |
| url); |
| } catch (URISyntaxException e) { |
| throw new ServletException( e ); |
| } |
| } |
| } |
| } |
| |
| /* If request contains X-Request-Id header use it else use random uuid as correlation id */ |
| final String reqID = |
| StringUtils.isBlank(((HttpServletRequest) servletRequest).getHeader(REQUEST_ID_HEADER_NAME)) ? |
| UUID.randomUUID().toString() : |
| ((HttpServletRequest) servletRequest).getHeader(REQUEST_ID_HEADER_NAME); |
| |
| assignCorrelationRequestId(reqID); |
| |
| // Populate Audit/correlation parameters |
| AuditContext auditContext = auditService.getContext(); |
| if(auditContext == null) { |
| auditContext = auditService.createContext(); |
| } |
| auditContext.setTargetServiceName( match == null ? null : match.getValue().getResourceRole() ); |
| auditContext.setRemoteIp( getRemoteAddress(servletRequest) ); |
| auditContext.setRemoteHostname( servletRequest.getRemoteHost() ); |
| auditService.attachContext(auditContext); |
| auditor.audit( |
| Action.ACCESS, contextWithPathAndQuery, ResourceType.URI, |
| ActionOutcome.UNAVAILABLE, RES.requestMethod(((HttpServletRequest)servletRequest).getMethod())); |
| |
| if( match != null ) { |
| Chain chain = match.getValue(); |
| servletRequest.setAttribute( AbstractGatewayFilter.TARGET_SERVICE_ROLE, chain.getResourceRole() ); |
| try { |
| chain.doFilter( |
| UrlEncodedFormRequest.isUrlEncodedForm(servletRequest) |
| ? new UrlEncodedFormRequest((HttpServletRequest) servletRequest) |
| : servletRequest, |
| servletResponse); |
| } catch( IOException | RuntimeException | ThreadDeath | ServletException e ) { |
| LOG.failedToExecuteFilter( e ); |
| auditor.audit( Action.ACCESS, contextWithPathAndQuery, ResourceType.URI, ActionOutcome.FAILURE ); |
| throw e; |
| } catch( Throwable e ) { |
| LOG.failedToExecuteFilter( e ); |
| auditor.audit( Action.ACCESS, contextWithPathAndQuery, ResourceType.URI, ActionOutcome.FAILURE ); |
| throw new ServletException( e ); |
| } |
| } else { |
| LOG.failedToMatchPath( requestPath ); |
| httpResponse.setStatus( HttpServletResponse.SC_NOT_FOUND ); |
| } |
| |
| //KAM[ Don't do this or the Jetty default servlet will overwrite any response setup by the filter. |
| // filterChain.doFilter( servletRequest, servletResponse ); |
| //] |
| } |
| |
| private String getRemoteAddress(ServletRequest servletRequest) { |
| GatewayConfig gatewayConfig = |
| (GatewayConfig) servletRequest.getServletContext(). |
| getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE); |
| |
| String addrHeaderName = gatewayConfig.getHeaderNameForRemoteAddress(); |
| String addr = ((HttpServletRequest)servletRequest).getHeader(addrHeaderName); |
| if (addr == null || addr.trim().isEmpty()) { |
| addr = servletRequest.getRemoteAddr(); |
| } |
| return addr; |
| } |
| |
| @Override |
| public void destroy() { |
| for( Holder holder : holders ) { |
| holder.destroy(); |
| } |
| } |
| |
| private void addHolder( Holder holder ) { |
| holders.add( holder ); |
| Chain chain = chains.get( holder.template ); |
| if( chain == null ) { |
| chain = new Chain(); |
| chain.setResourceRole( holder.getResourceRole() ); |
| chains.add( holder.template, chain ); |
| } |
| chain.chainList.add( holder ); |
| } |
| |
| public void addFilter( String path, String name, Filter filter, Map<String,String> params, String resourceRole ) throws URISyntaxException { |
| Holder holder = new Holder( path, name, filter, params, resourceRole ); |
| addHolder( holder ); |
| } |
| |
| public void addFilter( String path, String name, String clazz, Map<String,String> params, String resourceRole ) throws URISyntaxException { |
| Holder holder = new Holder( path, name, clazz, params, resourceRole ); |
| addHolder( holder ); |
| } |
| |
| // Now creating the correlation context only if required since it may be created upstream in the CorrelationHandler. |
| private void assignCorrelationRequestId(final String requestID) { |
| CorrelationService correlationService = CorrelationServiceFactory.getCorrelationService(); |
| CorrelationContext correlationContext = correlationService.getContext(); |
| if( correlationContext == null ) { |
| correlationService.attachContext(new Log4jCorrelationContext(requestID, null, null)); |
| } |
| } |
| |
| private void auditLog(ServletRequest servletRequest, ServletResponse servletResponse) { |
| final int status = ((HttpServletResponse) servletResponse).getStatus(); |
| final String requestUri, actionOutcome; |
| if (HttpServletResponse.SC_SERVICE_UNAVAILABLE == status) { |
| requestUri = ServletRequestUtils.getContextPathWithQuery(servletRequest); |
| actionOutcome = ActionOutcome.UNAVAILABLE; |
| } else { |
| requestUri = (String) servletRequest.getAttribute(AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME); |
| actionOutcome = ActionOutcome.SUCCESS; |
| } |
| auditor.audit(Action.ACCESS, requestUri, ResourceType.URI, actionOutcome, RES.responseStatus(status)); |
| } |
| |
| private class Chain implements FilterChain { |
| private List<Holder> chainList; |
| private String resourceRole; |
| |
| Chain() { |
| this.chainList = new ArrayList<>(); |
| } |
| |
| Chain( List<Holder> chainList) { |
| this.chainList = chainList; |
| } |
| |
| @Override |
| public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse ) throws IOException, ServletException { |
| if( chainList != null && !chainList.isEmpty() ) { |
| final Filter filter = chainList.get( 0 ); |
| final FilterChain chain = subChain(); |
| filter.doFilter( servletRequest, servletResponse, chain ); |
| } |
| } |
| |
| private FilterChain subChain() { |
| if( chainList != null && chainList.size() > 1 ) { |
| return new Chain( chainList.subList( 1, chainList.size() ) ); |
| } else { |
| return EMPTY_CHAIN; |
| } |
| } |
| |
| private String getResourceRole() { |
| return resourceRole; |
| } |
| |
| private void setResourceRole( String resourceRole ) { |
| this.resourceRole = resourceRole; |
| } |
| } |
| |
| public class Holder implements Filter, FilterConfig { |
| private Template template; |
| private String name; |
| private Map<String,String> params; |
| private Filter instance; |
| private Class<? extends Filter> clazz; |
| private String type; |
| private String resourceRole; |
| |
| Holder( String path, String name, Filter filter, Map<String,String> params, String resourceRole ) throws URISyntaxException { |
| this.template = Parser.parseTemplate( path ); |
| this.name = name; |
| this.params = params; |
| this.instance = filter; |
| this.clazz = filter.getClass(); |
| this.type = clazz.getCanonicalName(); |
| this.resourceRole = resourceRole; |
| } |
| |
| Holder( String path, String name, String clazz, Map<String,String> params, String resourceRole ) throws URISyntaxException { |
| this.template = Parser.parseTemplate( path ); |
| this.name = name; |
| this.params = params; |
| this.instance = null; |
| this.clazz = null; |
| this.type = clazz; |
| this.resourceRole = resourceRole; |
| } |
| |
| @Override |
| public String getFilterName() { |
| return name; |
| } |
| |
| @Override |
| public ServletContext getServletContext() { |
| return GatewayFilter.this.config.getServletContext(); |
| } |
| |
| @Override |
| public String getInitParameter( String name ) { |
| String value = null; |
| if( params != null ) { |
| value = params.get( name ); |
| } |
| return value; |
| } |
| |
| @Override |
| public Enumeration<String> getInitParameterNames() { |
| Enumeration<String> names = null; |
| if( params != null ) { |
| names = Collections.enumeration( params.keySet() ); |
| } |
| return names; |
| } |
| |
| @Override |
| public void init( FilterConfig filterConfig ) throws ServletException { |
| getInstance().init( filterConfig ); |
| } |
| |
| @Override |
| public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { |
| final Filter filter = getInstance(); |
| filter.doFilter( servletRequest, servletResponse, filterChain ); |
| } |
| |
| @Override |
| public void destroy() { |
| if( instance != null ) { |
| instance.destroy(); |
| instance = null; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Class<? extends Filter> getClazz() throws ClassNotFoundException { |
| if( clazz == null ) { |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| if( loader == null ) { |
| loader = this.getClass().getClassLoader(); |
| } |
| clazz = (Class)loader.loadClass( type ); |
| } |
| return clazz; |
| } |
| |
| private Filter getInstance() throws ServletException { |
| if( instance == null ) { |
| try { |
| if( clazz == null ) { |
| clazz = getClazz(); |
| } |
| Filter f = clazz.newInstance(); |
| f.init(this); |
| instance = f; |
| } catch( Exception e ) { |
| throw new ServletException( e ); |
| } |
| } |
| return instance; |
| } |
| |
| private String getResourceRole() { |
| return resourceRole; |
| } |
| |
| } |
| |
| /** |
| * A request wrapper class that wraps a request and adds the context path if |
| * needed. |
| */ |
| static class ForwardedRequest extends HttpServletRequestWrapper { |
| |
| private String newURL; |
| private String contextpath; |
| |
| ForwardedRequest(final HttpServletRequest request, |
| final String contextpath, final String newURL) { |
| super(request); |
| this.newURL = newURL; |
| this.contextpath = contextpath; |
| } |
| |
| @Override |
| public StringBuffer getRequestURL() { |
| return new StringBuffer(newURL); |
| } |
| |
| @Override |
| public String getRequestURI() { |
| return newURL; |
| } |
| |
| @Override |
| public String getContextPath() { |
| return super.getContextPath() + "/" + this.contextpath; |
| } |
| |
| } |
| } |