Java tutorial
/* * Data Hub Service (DHuS) - For Space data distribution. * Copyright (C) 2015-2018 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.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.olingo.v1.entity.Connection; 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.ApplicationContextProvider; 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); private static final SecurityContextProvider SEC_CTX_PROVIDER = ApplicationContextProvider .getBean(SecurityContextProvider.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 { // valve disabled or previous valve sent an error, return if (!isEnable() || response.isError()) { 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 = SEC_CTX_PROVIDER.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) { 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; } public static Map<String, Connection> getConnections(String username) { @SuppressWarnings("rawtypes") List keys = getCache().getKeysWithExpiryCheck(); Map<String, Connection> map = new HashMap<>(); for (Object key : keys) { if (getCache().isKeyInCache(key)) { Object value = getCache().get(key).getObjectValue(); if (value != null && key instanceof UUID && value instanceof AccessInformation) { AccessInformation info = (AccessInformation) value; String accessuser = info.getUsername(); if (username == null || username.isEmpty() || (accessuser != null && accessuser.equals(username))) { String k = key.toString(); map.put(k, new Connection(k, info)); } } } } 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); } static String twoDigit(double value) { return String.format("%9.2f", value); } }