/* * Data Hub Service (DHuS) - For Space data distribution. * Copyright (C) 2013,2014,2015 GAEL Systems * * This file is part of DHuS software sources. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package fr.gael.dhus.server.http.valve; import java.io.IOException; import java.net.InetAddress; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.util.RequestUtil; import org.apache.catalina.valves.ValveBase; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.crypto.codec.Base64; import fr.gael.dhus.server.http.valve.AccessInformation.FailureConnectionStatus; import fr.gael.dhus.server.http.valve.AccessInformation.PendingConnectionStatus; import fr.gael.dhus.server.http.valve.AccessInformation.SuccessConnectionStatus; import fr.gael.dhus.spring.context.SecurityContextProvider; import fr.gael.dhus.spring.security.CookieKey; import fr.gael.dhus.spring.security.authentication.ProxyWebAuthenticationDetails; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; public class AccessValve extends ValveBase { private static final Logger LOGGER = LogManager.getLogger(AccessValve.class); /** * Filter pattern is passed as Tomcat parameter. * It allows to focus on a specific path: i.e "^.*(/odata/v1/).*$"(odata only) * or to exclude element : "^((?!/(home|new)/).)*$" : all but web pages... */ private String pattern = null; /** * Parameter to activates/deactivates this valve */ private boolean enable = true; /** * Parameter to display info into the logger */ private boolean useLogger = true; private static final String CACHE_MANAGER_NAME = "dhus_cache"; private static final String CACHE_NAME = "user_connections"; private static Cache cache; /** * Local Address */ private static final String LOCAL_ADDR_VALUE; /** * This Local address is only computed once. */ static { String init; try { init = InetAddress.getLocalHost().getHostAddress(); } catch (Throwable e) { init = "127.0.0.1"; } LOCAL_ADDR_VALUE = init; } private static Cache getCache () { if (cache == null) { cache = CacheManager.getCacheManager (CACHE_MANAGER_NAME) .getCache (CACHE_NAME); // Override the current eviction policy to avoid removing pending // elements. cache.setMemoryStoreEvictionPolicy( new NoPendingEvictionPolicy(cache.getMemoryStoreEvictionPolicy())); } return cache; } @Override public void invoke (Request request, Response response) throws IOException, ServletException { // Case of Valve disabled. if (!isEnable()) { getNext().invoke(request, response); return; } final AccessInformation ai = new AccessInformation(); ai.setConnectionStatus(new PendingConnectionStatus()); // To be sure not to retrieve the same date trough concurrency calls. synchronized (this) { ai.setStartTimestamp(System.nanoTime()); ai.setStartDate (new Date ()); } try { this.doLog(request, response, ai); } finally { Element cached_element = new Element(UUID.randomUUID(), ai); getCache().put(cached_element); try { // Log of the pending request command. if (isUseLogger()) LOGGER.info ("Access " + ai); getNext().invoke(request, response); } catch (Throwable e) { response.addHeader("cause-message", e.getClass().getSimpleName() + " : " + e.getMessage()); //ai.setConnectionStatus(new FailureConnectionStatus(e)); response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); //throw e; } finally { ai.setReponseSize(response.getContentLength()); ai.setWrittenResponseSize(response.getContentWritten()); if (response.getStatus()>=400) { String message = RequestUtil.filter(response.getMessage()); if (message==null) { // The cause-message has been inserted into the reponse header // at error handler time. It no message is retrieved in the // standard response, the cause-message is used. message = response.getHeader("cause-message"); } Throwable throwable = null; if (message != null) throwable = new Throwable(message); else throwable = (Throwable) request.getAttribute( RequestDispatcher.ERROR_EXCEPTION); if (throwable==null) throwable = new Throwable(); ai.setConnectionStatus(new FailureConnectionStatus(throwable)); } else ai.setConnectionStatus(new SuccessConnectionStatus()); ai.setEndTimestamp(System.nanoTime()); if ((getPattern()==null) || ai.getRequest().matches(getPattern())) { cached_element.updateUpdateStatistics(); if (isUseLogger()) LOGGER.info ("Access " + ai); } } } } /** * Logs information into temporary cache. According to the Valve * configuration, log will also display into the logger. * @param request the input user request to log. * @param response the response to the user to be incremented. * return the log entry. * @throws IOException * @throws ServletException */ private void doLog (Request request, Response response, AccessInformation ai) throws IOException, ServletException { // Retrieve cookie to obtains existing context if any. Cookie integrityCookie=CookieKey.getIntegrityCookie(request.getCookies()); SecurityContext ctx = null; if (integrityCookie != null) { String integrity = integrityCookie.getValue (); if (integrity != null && !integrity.isEmpty ()) { ctx = SecurityContextProvider.getSecurityContext (integrity); } } if ((ctx!=null) && (ctx.getAuthentication()!=null)) { ai.setUsername(ctx.getAuthentication().getName()); } else { String[] basicAuth = extractAndDecodeHeader( request.getHeader("Authorization")); if (basicAuth!=null) ai.setUsername(basicAuth[0]); } if (request.getQueryString()!=null) { ai.setRequest(request.getRequestURL().append('?'). append(request.getQueryString()).toString()); } else { ai.setRequest(request.getRequestURL().toString()); } ai.setLocalAddress(LOCAL_ADDR_VALUE); ai.setLocalHost(request.getServerName()); ai.setRemoteAddress(ProxyWebAuthenticationDetails.getRemoteIp(request)); ai.setRemoteHost(ProxyWebAuthenticationDetails.getRemoteHost(request)); } private String[] extractAndDecodeHeader(String header) throws IOException { if (header == null || header.isEmpty ()) { return null; } byte[] base64Token = header.substring(6).getBytes("UTF-8"); byte[] decoded; try { decoded = Base64.decode(base64Token); } catch (IllegalArgumentException e) { throw new BadCredentialsException( "Failed to decode basic authentication token."); } String token = new String(decoded, "UTF-8"); int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException( "Invalid basic authentication token."); } return new String[]{token.substring(0,delim),token.substring(delim+1)}; } /** * Tomcat offers mechanism that automatically instantiate Valves that * implements such setters. This setter is used to set the pattern * of request URL that will be logged. * @param pattern the pattern to be applied to requests. */ public void setPattern(String pattern) { if (pattern==null) this.pattern = pattern; } /** * Retrieves pattern. * @return the pattern. */ public String getPattern() { return pattern; } public void setEnable(boolean enable) { this.enable = enable; } public boolean isEnable() { return this.enable; } public boolean isUseLogger() { return useLogger; } public void setUseLogger(boolean useLogger) { this.useLogger = useLogger; } public static Map<UUID, AccessInformation> getAccessInformationMap () { @SuppressWarnings ("rawtypes") List keys = getCache ().getKeysWithExpiryCheck (); Map<UUID, AccessInformation> map = new HashMap<> (); for (Object key: keys) { if (getCache().isKeyInCache(key)) { Object value = getCache ().get (key).getObjectValue (); if (key instanceof UUID && value instanceof AccessInformation) { map.put ((UUID) key, (AccessInformation)value); } } } return map; } /** * Provide metrics of top accesses. * @param top the number of top accesses. * @param skip number of items to skip. * @return the accesses metrics. */ public static AbuseMetrics getMetrics (int skip, int top) { return AbuseMetrics. computeAbuseMetricsFromAccess(getAccessInformationMap(), skip, top); } // TO BE REPLACED LATER (User configurable spring timer...) private static final Timer timer= new Timer(); static { try { AccessValve.timer.schedule(new TimerTask() { @Override public void run() { LOGGER.info(AccessValve.getMetrics(0, 10)); } }, 10000, 60000); } catch (Throwable e) { LOGGER.error("Cannot start metrics periodical display", e); } } static String twoDigit (double value) { return String.format("%9.2f", value); } }