All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.catalina.valves.AbstractAccessLogValve Maven / Gradle / Ivy

There is a newer version: 11.0.0-M20
Show newest version
/*
 * 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.catalina.valves;


import java.io.CharArrayWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;

import org.apache.catalina.AccessLog;
import org.apache.catalina.Globals;
import org.apache.catalina.Session;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.TLSUtil;
import org.apache.coyote.ActionCode;
import org.apache.coyote.RequestInfo;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.collections.SynchronizedStack;
import org.apache.tomcat.util.net.IPv6Utils;


/**
 * 

* Abstract implementation of the Valve interface that generates a web server access log with the detailed line * contents matching a configurable pattern. The syntax of the available patterns is similar to that supported by the * Apache HTTP Server mod_log_config module. *

*

* Patterns for the logged message may include constant text or any of the following replacement strings, for which the * corresponding information from the specified Response is substituted: *

*
    *
  • %a - Remote IP address *
  • %A - Local IP address *
  • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent *
  • %B - Bytes sent, excluding HTTP headers *
  • %h - Remote host name (or IP address if enableLookups for the connector is * false) *
  • %H - Request protocol *
  • %l - Remote logical username from identd (always returns '-') *
  • %m - Request method *
  • %p - Local port *
  • %q - Query string (prepended with a '?' if it exists, otherwise an empty string *
  • %r - First line of the request *
  • %s - HTTP status code of the response *
  • %S - User session ID *
  • %t - Date and time, in Common Log Format format *
  • %u - Remote user that was authenticated *
  • %U - Requested URL path *
  • %v - Local server name *
  • %D - Time taken to process the request, in millis *
  • %T - Time taken to process the request, in seconds *
  • %F - Time taken to commit the response, in millis *
  • %I - current Request thread name (can compare later with stacktraces) *
  • %X - Connection status when response is completed: *
      *
    • X = Connection aborted before the response completed.
    • *
    • + = Connection may be kept alive after the response is sent.
    • *
    • - = Connection will be closed after the response is sent.
    • *
    *
*

* In addition, the caller can specify one of the following aliases for commonly utilized patterns: *

*
    *
  • common - %h %l %u %t "%r" %s %b *
  • combined - %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" *
*

* There is also support to write information from the cookie, incoming header, the Session or something else in the * ServletRequest.
* It is modeled after the Apache HTTP Server log configuration syntax: *

*
    *
  • %{xxx}i for incoming headers *
  • %{xxx}o for outgoing response headers *
  • %{xxx}c for a specific cookie *
  • %{xxx}r xxx is an attribute in the ServletRequest *
  • %{xxx}s xxx is an attribute in the HttpSession *
  • %{xxx}t xxx is an enhanced SimpleDateFormat pattern (see Configuration Reference document for * details on supported time patterns) *
*

* Conditional logging is also supported. This can be done with the conditionUnless and * conditionIf properties. If the value returned from ServletRequest.getAttribute(conditionUnless) yields a * non-null value, the logging will be skipped. If the value returned from ServletRequest.getAttribute(conditionIf) * yields the null value, the logging will be skipped. The condition attribute is synonym for * conditionUnless and is provided for backwards compatibility. *

*

* For extended attributes coming from a getAttribute() call, it is you responsibility to ensure there are no newline or * control characters. *

* * @author Craig R. McClanahan * @author Jason Brittain * @author Remy Maucherat * @author Takayuki Kaneko * @author Peter Rossbach */ public abstract class AbstractAccessLogValve extends ValveBase implements AccessLog { private static final Log log = LogFactory.getLog(AbstractAccessLogValve.class); /** * The list of our time format types. */ private enum FormatType { CLF, SEC, MSEC, MSEC_FRAC, SDF } /** * The list of our port types. */ private enum PortType { LOCAL, REMOTE } /** * The list of our ip address types. */ private enum RemoteAddressType { REMOTE, PEER } // ------------------------------------------------------ Constructor public AbstractAccessLogValve() { super(true); } // ----------------------------------------------------- Instance Variables /** * enabled this component */ protected boolean enabled = true; /** * Use IPv6 canonical representation format as defined by RFC 5952. */ private boolean ipv6Canonical = false; /** * The pattern used to format our access log lines. */ protected String pattern = null; /** * The size of our global date format cache */ private static final int globalCacheSize = 300; /** * The size of our thread local date format cache */ private static final int localCacheSize = 60; /** *

* Cache structure for formatted timestamps based on seconds. *

*

* The cache consists of entries for a consecutive range of seconds. The length of the range is configurable. It is * implemented based on a cyclic buffer. New entries shift the range. *

*

* There is one cache for the CLF format (the access log standard format) and a HashMap of caches for additional * formats used by SimpleDateFormat. *

*

* Although the cache supports specifying a locale when retrieving a formatted timestamp, each format will always * use the locale given when the format was first used. New locales can only be used for new formats. The CLF format * will always be formatted using the locale en_US. *

*

* The cache is not threadsafe. It can be used without synchronization via thread local instances, or with * synchronization as a global cache. *

*

* The cache can be created with a parent cache to build a cache hierarchy. Access to the parent cache is * threadsafe. *

*

* This class uses a small thread local first level cache and a bigger synchronized global second level cache. *

*/ protected static class DateFormatCache { protected class Cache { /* CLF log format */ private static final String cLFFormat = "dd/MMM/yyyy:HH:mm:ss Z"; /* Second used to retrieve CLF format in most recent invocation */ private long previousSeconds = Long.MIN_VALUE; /* Value of CLF format retrieved in most recent invocation */ private String previousFormat = ""; /* First second contained in cache */ private long first = Long.MIN_VALUE; /* Last second contained in cache */ private long last = Long.MIN_VALUE; /* Index of "first" in the cyclic cache */ private int offset = 0; /* Helper object to be able to call SimpleDateFormat.format(). */ private final Date currentDate = new Date(); protected final String cache[]; private SimpleDateFormat formatter; private boolean isCLF = false; private Cache parent = null; private Cache(Cache parent) { this(null, parent); } private Cache(String format, Cache parent) { this(format, null, parent); } private Cache(String format, Locale loc, Cache parent) { cache = new String[cacheSize]; for (int i = 0; i < cacheSize; i++) { cache[i] = null; } if (loc == null) { loc = cacheDefaultLocale; } if (format == null) { isCLF = true; format = cLFFormat; formatter = new SimpleDateFormat(format, Locale.US); } else { formatter = new SimpleDateFormat(format, loc); } formatter.setTimeZone(TimeZone.getDefault()); this.parent = parent; } private String getFormatInternal(long time) { long seconds = time / 1000; /* * First step: if we have seen this timestamp during the previous call, and we need CLF, return the * previous value. */ if (seconds == previousSeconds) { return previousFormat; } /* Second step: Try to locate in cache */ previousSeconds = seconds; int index = (offset + (int) (seconds - first)) % cacheSize; if (index < 0) { index += cacheSize; } if (seconds >= first && seconds <= last) { if (cache[index] != null) { /* Found, so remember for next call and return. */ previousFormat = cache[index]; return previousFormat; } /* Third step: not found in cache, adjust cache and add item */ } else if (seconds >= last + cacheSize || seconds <= first - cacheSize) { first = seconds; last = first + cacheSize - 1; index = 0; offset = 0; for (int i = 1; i < cacheSize; i++) { cache[i] = null; } } else if (seconds > last) { for (int i = 1; i < seconds - last; i++) { cache[(index + cacheSize - i) % cacheSize] = null; } first = seconds - (cacheSize - 1); last = seconds; offset = (index + 1) % cacheSize; } else if (seconds < first) { for (int i = 1; i < first - seconds; i++) { cache[(index + i) % cacheSize] = null; } first = seconds; last = seconds + (cacheSize - 1); offset = index; } /* * Last step: format new timestamp either using parent cache or locally. */ if (parent != null) { synchronized (parent) { previousFormat = parent.getFormatInternal(time); } } else { currentDate.setTime(time); previousFormat = formatter.format(currentDate); if (isCLF) { StringBuilder current = new StringBuilder(32); current.append('['); current.append(previousFormat); current.append(']'); previousFormat = current.toString(); } } cache[index] = previousFormat; return previousFormat; } } /* Number of cached entries */ private int cacheSize = 0; private final Locale cacheDefaultLocale; private final DateFormatCache parent; protected final Cache cLFCache; private final Map formatCache = new HashMap<>(); protected DateFormatCache(int size, Locale loc, DateFormatCache parent) { cacheSize = size; cacheDefaultLocale = loc; this.parent = parent; Cache parentCache = null; if (parent != null) { synchronized (parent) { parentCache = parent.getCache(null, null); } } cLFCache = new Cache(parentCache); } private Cache getCache(String format, Locale loc) { Cache cache; if (format == null) { cache = cLFCache; } else { cache = formatCache.get(format); if (cache == null) { Cache parentCache = null; if (parent != null) { synchronized (parent) { parentCache = parent.getCache(format, loc); } } cache = new Cache(format, loc, parentCache); formatCache.put(format, cache); } } return cache; } public String getFormat(long time) { return cLFCache.getFormatInternal(time); } public String getFormat(String format, Locale loc, long time) { return getCache(format, loc).getFormatInternal(time); } } /** * Global date format cache. */ private static final DateFormatCache globalDateCache = new DateFormatCache(globalCacheSize, Locale.getDefault(), null); /** * Thread local date format cache. */ private static final ThreadLocal localDateCache = ThreadLocal .withInitial(() -> new DateFormatCache(localCacheSize, Locale.getDefault(), globalDateCache)); /** * The system time when we last updated the Date that this valve uses for log lines. */ private static final ThreadLocal localDate = ThreadLocal.withInitial(Date::new); /** * Are we doing conditional logging. default null. It is the value of conditionUnless property. */ protected String condition = null; /** * Are we doing conditional logging. default null. It is the value of conditionIf property. */ protected String conditionIf = null; /** * Name of locale used to format timestamps in log entries and in log file name suffix. */ protected String localeName = Locale.getDefault().toString(); /** * Locale used to format timestamps in log entries and in log file name suffix. */ protected Locale locale = Locale.getDefault(); /** * Array of AccessLogElement, they will be used to make log message. */ protected AccessLogElement[] logElements = null; /** * Array of elements where the value needs to be cached at the start of the request. */ protected CachedElement[] cachedElements = null; /** * Should this valve use request attributes for IP address, hostname, protocol and port used for the request. * Default is false. * * @see #setRequestAttributesEnabled(boolean) */ protected boolean requestAttributesEnabled = false; /** * Buffer pool used for log message generation. Pool used to reduce garbage generation. */ private SynchronizedStack charArrayWriters = new SynchronizedStack<>(); /** * Log message buffers are usually recycled and re-used. To prevent excessive memory usage, if a buffer grows beyond * this size it will be discarded. The default is 256 characters. This should be set to larger than the typical * access log message size. */ private int maxLogMessageBufferSize = 256; /** * Does the configured log pattern include a known TLS attribute? */ private boolean tlsAttributeRequired = false; // ------------------------------------------------------------- Properties public int getMaxLogMessageBufferSize() { return maxLogMessageBufferSize; } public void setMaxLogMessageBufferSize(int maxLogMessageBufferSize) { this.maxLogMessageBufferSize = maxLogMessageBufferSize; } public boolean getIpv6Canonical() { return ipv6Canonical; } public void setIpv6Canonical(boolean ipv6Canonical) { this.ipv6Canonical = ipv6Canonical; } /** * {@inheritDoc} Default is false. */ @Override public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { this.requestAttributesEnabled = requestAttributesEnabled; } @Override public boolean getRequestAttributesEnabled() { return requestAttributesEnabled; } /** * @return the enabled flag. */ public boolean getEnabled() { return enabled; } /** * @param enabled The enabled to set. */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * @return the format pattern. */ public String getPattern() { return this.pattern; } /** * Set the format pattern, first translating any recognized alias. * * @param pattern The new pattern */ public void setPattern(String pattern) { if (pattern == null) { this.pattern = ""; } else if (pattern.equals(Constants.AccessLog.COMMON_ALIAS)) { this.pattern = Constants.AccessLog.COMMON_PATTERN; } else if (pattern.equals(Constants.AccessLog.COMBINED_ALIAS)) { this.pattern = Constants.AccessLog.COMBINED_PATTERN; } else { this.pattern = pattern; } logElements = createLogElements(); if (logElements != null) { cachedElements = createCachedElements(logElements); } } /** * Return whether the attribute name to look for when performing conditional logging. If null, every request is * logged. * * @return the attribute name */ public String getCondition() { return condition; } /** * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. * * @param condition Set to null to log everything */ public void setCondition(String condition) { this.condition = condition; } /** * Return whether the attribute name to look for when performing conditional logging. If null, every request is * logged. * * @return the attribute name */ public String getConditionUnless() { return getCondition(); } /** * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. * * @param condition Set to null to log everything */ public void setConditionUnless(String condition) { setCondition(condition); } /** * Return whether the attribute name to look for when performing conditional logging. If null, every request is * logged. * * @return the attribute name */ public String getConditionIf() { return conditionIf; } /** * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. * * @param condition Set to null to log everything */ public void setConditionIf(String condition) { this.conditionIf = condition; } /** * Return the locale used to format timestamps in log entries and in log file name suffix. * * @return the locale */ public String getLocale() { return localeName; } /** * Set the locale used to format timestamps in log entries and in log file name suffix. Changing the locale is only * supported as long as the AccessLogValve has not logged anything. Changing the locale later can lead to * inconsistent formatting. * * @param localeName The locale to use. */ public void setLocale(String localeName) { this.localeName = localeName; locale = findLocale(localeName, locale); } // --------------------------------------------------------- Public Methods /** * Log a message summarizing the specified request and response, according to the format specified by the * pattern property. * * @param request Request being processed * @param response Response being processed * * @exception IOException if an input/output error has occurred * @exception ServletException if a servlet error has occurred */ @Override public void invoke(Request request, Response response) throws IOException, ServletException { if (tlsAttributeRequired) { // The log pattern uses TLS attributes. Ensure these are populated // before the request is processed because with NIO2 it is possible // for the connection to be closed (and the TLS info lost) before // the access log requests the TLS info. Requesting it now causes it // to be cached in the request. request.getAttribute(Globals.CERTIFICATES_ATTR); } if (cachedElements != null) { for (CachedElement element : cachedElements) { element.cache(request); } } getNext().invoke(request, response); } @Override public void log(Request request, Response response, long time) { if (!getState().isAvailable() || !getEnabled() || logElements == null || condition != null && null != request.getRequest().getAttribute(condition) || conditionIf != null && null == request.getRequest().getAttribute(conditionIf)) { return; } /* * XXX This is a bit silly, but we want to have start and stop time and duration consistent. It would be better * to keep start and stop simply in the request and/or response object and remove time (duration) from the * interface. */ long start = request.getCoyoteRequest().getStartTime(); Date date = getDate(start + time); CharArrayWriter result = charArrayWriters.pop(); if (result == null) { result = new CharArrayWriter(128); } for (AccessLogElement logElement : logElements) { logElement.addElement(result, date, request, response, time); } log(result); if (result.size() <= maxLogMessageBufferSize) { result.reset(); charArrayWriters.push(result); } } // -------------------------------------------------------- Protected Methods /** * Log the specified message. * * @param message Message to be logged. This object will be recycled by the calling method. */ protected abstract void log(CharArrayWriter message); // -------------------------------------------------------- Private Methods /** * This method returns a Date object that is accurate to within one second. If a thread calls this method to get a * Date and it's been less than 1 second since a new Date was created, this method simply gives out the same Date * again so that the system doesn't spend time creating Date objects unnecessarily. * * @param systime The time * * @return the date object */ private static Date getDate(long systime) { Date date = localDate.get(); date.setTime(systime); return date; } /** * Find a locale by name. * * @param name The locale name * @param fallback Fallback locale if the name is not found * * @return the locale object */ protected static Locale findLocale(String name, Locale fallback) { if (name == null || name.isEmpty()) { return Locale.getDefault(); } else { for (Locale l : Locale.getAvailableLocales()) { if (name.equals(l.toString())) { return l; } } } log.error(sm.getString("accessLogValve.invalidLocale", name)); return fallback; } /** * AccessLogElement writes the partial message into the buffer. */ protected interface AccessLogElement { void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time); } /** * Marks an AccessLogElement as needing to be have the value cached at the start of the request rather than just * recorded at the end as the source data for the element may not be available at the end of the request. This * typically occurs for remote network information, such as ports, IP addresses etc. when the connection is closed * unexpectedly. These elements take advantage of these values being cached elsewhere on first request and do not * cache the value in the element since the elements are state-less. */ protected interface CachedElement { void cache(Request request); } /** * write thread name - %I */ protected static class ThreadNameElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { RequestInfo info = request.getCoyoteRequest().getRequestProcessor(); if (info != null) { buf.append(info.getWorkerThreadName()); } else { buf.append('-'); } } } /** * write local IP address - %A */ protected static class LocalAddrElement implements AccessLogElement { private final String localAddrValue; public LocalAddrElement(boolean ipv6Canonical) { String init; try { init = InetAddress.getLocalHost().getHostAddress(); } catch (Throwable e) { ExceptionUtils.handleThrowable(e); init = "127.0.0.1"; } if (ipv6Canonical) { localAddrValue = IPv6Utils.canonize(init); } else { localAddrValue = init; } } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(localAddrValue); } } /** * write remote IP address - %a */ protected class RemoteAddrElement implements AccessLogElement, CachedElement { /** * Type of address to log */ private static final String remoteAddress = "remote"; private static final String peerAddress = "peer"; private final RemoteAddressType remoteAddressType; public RemoteAddrElement() { remoteAddressType = RemoteAddressType.REMOTE; } public RemoteAddrElement(String type) { switch (type) { case remoteAddress: remoteAddressType = RemoteAddressType.REMOTE; break; case peerAddress: remoteAddressType = RemoteAddressType.PEER; break; default: log.error(sm.getString("accessLogValve.invalidRemoteAddressType", type)); remoteAddressType = RemoteAddressType.REMOTE; break; } } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String value = null; if (remoteAddressType == RemoteAddressType.PEER) { value = request.getPeerAddr(); } else { if (requestAttributesEnabled) { Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE); if (addr == null) { value = request.getRemoteAddr(); } else { value = addr.toString(); } } else { value = request.getRemoteAddr(); } } if (ipv6Canonical) { value = IPv6Utils.canonize(value); } buf.append(value); } @Override public void cache(Request request) { if (!requestAttributesEnabled) { if (remoteAddressType == RemoteAddressType.PEER) { request.getPeerAddr(); } else { request.getRemoteAddr(); } } } } /** * write remote host name - %h */ protected class HostElement implements AccessLogElement, CachedElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String value = null; if (requestAttributesEnabled) { Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE); if (host != null) { value = host.toString(); } } if (value == null || value.length() == 0) { value = request.getRemoteHost(); } if (value == null || value.length() == 0) { value = "-"; } if (ipv6Canonical) { value = IPv6Utils.canonize(value); } buf.append(value); } @Override public void cache(Request request) { if (!requestAttributesEnabled) { request.getRemoteHost(); } } } /** * write remote logical username from identd (always returns '-') - %l */ protected static class LogicalUserNameElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append('-'); } } /** * write request protocol - %H */ protected class ProtocolElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (requestAttributesEnabled) { Object proto = request.getAttribute(PROTOCOL_ATTRIBUTE); if (proto == null) { buf.append(request.getProtocol()); } else { buf.append(proto.toString()); } } else { buf.append(request.getProtocol()); } } } /** * write remote user that was authenticated (if any), else '-' - %u */ protected static class UserElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (request != null) { String value = request.getRemoteUser(); if (value != null) { escapeAndAppend(value, buf); } else { buf.append('-'); } } else { buf.append('-'); } } } /** * write date and time, in configurable format (default CLF) - %t or %{format}t */ protected class DateAndTimeElement implements AccessLogElement { /** * Format prefix specifying request start time */ private static final String requestStartPrefix = "begin"; /** * Format prefix specifying response end time */ private static final String responseEndPrefix = "end"; /** * Separator between optional prefix and rest of format */ private static final String prefixSeparator = ":"; /** * Special format for seconds since epoch */ private static final String secFormat = "sec"; /** * Special format for milliseconds since epoch */ private static final String msecFormat = "msec"; /** * Special format for millisecond part of timestamp */ private static final String msecFractionFormat = "msec_frac"; /** * The patterns we use to replace "S" and "SSS" millisecond formatting of SimpleDateFormat by our own handling */ private static final String msecPattern = "{#}"; private static final String tripleMsecPattern = msecPattern + msecPattern + msecPattern; /* Our format description string, null if CLF */ private final String format; /* Does the format string contain characters we need to escape */ private final boolean needsEscaping; /* Whether to use begin of request or end of response as the timestamp */ private final boolean usesBegin; /* The format type */ private final FormatType type; /* Whether we need to postprocess by adding milliseconds */ private boolean usesMsecs = false; protected DateAndTimeElement() { this(null); } /** * Replace the millisecond formatting character 'S' by some dummy characters in order to make the resulting * formatted time stamps cacheable. We replace the dummy chars later with the actual milliseconds because that's * relatively cheap. */ private String tidyFormat(String format) { boolean escape = false; StringBuilder result = new StringBuilder(); int len = format.length(); char x; for (int i = 0; i < len; i++) { x = format.charAt(i); if (escape || x != 'S') { result.append(x); } else { result.append(msecPattern); usesMsecs = true; } if (x == '\'') { escape = !escape; } } return result.toString(); } protected DateAndTimeElement(String sdf) { String format = sdf; boolean needsEscaping = false; if (sdf != null) { CharArrayWriter writer = new CharArrayWriter(); escapeAndAppend(sdf, writer); String escaped = writer.toString(); if (!escaped.equals(sdf)) { needsEscaping = true; } } this.needsEscaping = needsEscaping; boolean usesBegin = false; FormatType type = FormatType.CLF; if (format != null) { if (format.equals(requestStartPrefix)) { usesBegin = true; format = ""; } else if (format.startsWith(requestStartPrefix + prefixSeparator)) { usesBegin = true; format = format.substring(6); } else if (format.equals(responseEndPrefix)) { usesBegin = false; format = ""; } else if (format.startsWith(responseEndPrefix + prefixSeparator)) { usesBegin = false; format = format.substring(4); } if (format.length() == 0) { type = FormatType.CLF; } else if (format.equals(secFormat)) { type = FormatType.SEC; } else if (format.equals(msecFormat)) { type = FormatType.MSEC; } else if (format.equals(msecFractionFormat)) { type = FormatType.MSEC_FRAC; } else { type = FormatType.SDF; format = tidyFormat(format); } } this.format = format; this.usesBegin = usesBegin; this.type = type; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { long timestamp = date.getTime(); long frac; if (usesBegin) { timestamp -= time; } /* * Implementation note: This is deliberately not implemented using switch. If a switch is used the compiler * (at least the Oracle one) will use a synthetic class to implement the switch. The problem is that this * class needs to be pre-loaded when using a SecurityManager and the name of that class will depend on any * anonymous inner classes and any other synthetic classes. As such the name is not constant and keeping the * pre-loading up to date as the name changes is error prone. */ if (type == FormatType.CLF) { buf.append(localDateCache.get().getFormat(timestamp)); } else if (type == FormatType.SEC) { buf.append(Long.toString(timestamp / 1000)); } else if (type == FormatType.MSEC) { buf.append(Long.toString(timestamp)); } else if (type == FormatType.MSEC_FRAC) { frac = timestamp % 1000; if (frac < 100) { if (frac < 10) { buf.append('0'); buf.append('0'); } else { buf.append('0'); } } buf.append(Long.toString(frac)); } else { // FormatType.SDF String temp = localDateCache.get().getFormat(format, locale, timestamp); if (usesMsecs) { frac = timestamp % 1000; StringBuilder tripleMsec = new StringBuilder(4); if (frac < 100) { if (frac < 10) { tripleMsec.append('0'); tripleMsec.append('0'); } else { tripleMsec.append('0'); } } tripleMsec.append(frac); temp = temp.replace(tripleMsecPattern, tripleMsec); temp = temp.replace(msecPattern, Long.toString(frac)); } if (needsEscaping) { escapeAndAppend(temp, buf); } else { buf.append(temp); } } } } /** * write first line of the request (method and request URI) - %r */ protected static class RequestElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (request != null) { String method = request.getMethod(); if (method == null) { // No method means no request line buf.append('-'); } else { buf.append(request.getMethod()); buf.append(' '); buf.append(request.getRequestURI()); if (request.getQueryString() != null) { buf.append('?'); buf.append(request.getQueryString()); } buf.append(' '); buf.append(request.getProtocol()); } } else { buf.append('-'); } } } /** * write HTTP status code of the response - %s */ protected static class HttpStatusCodeElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (response != null) { // This approach is used to reduce GC from toString conversion int status = response.getStatus(); if (100 <= status && status < 1000) { buf.append((char) ('0' + (status / 100))).append((char) ('0' + ((status / 10) % 10))) .append((char) ('0' + (status % 10))); } else { buf.append(Integer.toString(status)); } } else { buf.append('-'); } } } /** * write local or remote port for request connection - %p and %{xxx}p */ protected class PortElement implements AccessLogElement, CachedElement { /** * Type of port to log */ private static final String localPort = "local"; private static final String remotePort = "remote"; private final PortType portType; public PortElement() { portType = PortType.LOCAL; } public PortElement(String type) { switch (type) { case remotePort: portType = PortType.REMOTE; break; case localPort: portType = PortType.LOCAL; break; default: log.error(sm.getString("accessLogValve.invalidPortType", type)); portType = PortType.LOCAL; break; } } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (requestAttributesEnabled && portType == PortType.LOCAL) { Object port = request.getAttribute(SERVER_PORT_ATTRIBUTE); if (port == null) { buf.append(Integer.toString(request.getServerPort())); } else { buf.append(port.toString()); } } else { if (portType == PortType.LOCAL) { buf.append(Integer.toString(request.getServerPort())); } else { buf.append(Integer.toString(request.getRemotePort())); } } } @Override public void cache(Request request) { if (portType == PortType.REMOTE) { request.getRemotePort(); } } } /** * write bytes sent, excluding HTTP headers - %b, %B */ protected static class ByteSentElement implements AccessLogElement { private final boolean conversion; /** * @param conversion true to write '-' instead of 0 - %b. */ public ByteSentElement(boolean conversion) { this.conversion = conversion; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { // Don't need to flush since trigger for log message is after the // response has been committed long length = response.getBytesWritten(false); if (length <= 0) { // Protect against nulls and unexpected types as these values // may be set by untrusted applications Object start = request.getAttribute(Globals.SENDFILE_FILE_START_ATTR); if (start instanceof Long) { Object end = request.getAttribute(Globals.SENDFILE_FILE_END_ATTR); if (end instanceof Long) { length = ((Long) end).longValue() - ((Long) start).longValue(); } } } if (length <= 0 && conversion) { buf.append('-'); } else { buf.append(Long.toString(length)); } } } /** * write request method (GET, POST, etc.) - %m */ protected static class MethodElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (request != null) { buf.append(request.getMethod()); } } } /** * write time taken to process the request - %D, %T */ protected static class ElapsedTimeElement implements AccessLogElement { private final boolean millis; /** * @param millis true, write time in millis - %D, if false, write time in seconds - %T */ public ElapsedTimeElement(boolean millis) { this.millis = millis; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (millis) { buf.append(Long.toString(time)); } else { // second buf.append(Long.toString(time / 1000)); buf.append('.'); int remains = (int) (time % 1000); buf.append(Long.toString(remains / 100)); remains = remains % 100; buf.append(Long.toString(remains / 10)); buf.append(Long.toString(remains % 10)); } } } /** * write time until first byte is written (commit time) in millis - %F */ protected static class FirstByteTimeElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { long commitTime = response.getCoyoteResponse().getCommitTime(); if (commitTime == -1) { buf.append('-'); } else { long delta = commitTime - request.getCoyoteRequest().getStartTime(); buf.append(Long.toString(delta)); } } } /** * write Query string (prepended with a '?' if it exists) - %q */ protected static class QueryElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String query = null; if (request != null) { query = request.getQueryString(); } if (query != null) { buf.append('?'); buf.append(query); } } } /** * write user session ID - %S */ protected static class SessionIdElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (request == null) { buf.append('-'); } else { Session session = request.getSessionInternal(false); if (session == null) { buf.append('-'); } else { buf.append(session.getIdInternal()); } } } } /** * write requested URL path - %U */ protected static class RequestURIElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (request != null) { buf.append(request.getRequestURI()); } else { buf.append('-'); } } } /** * write local server name - %v */ protected class LocalServerNameElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String value = null; if (requestAttributesEnabled) { Object serverName = request.getAttribute(SERVER_NAME_ATTRIBUTE); if (serverName != null) { value = serverName.toString(); } } if (value == null || value.length() == 0) { value = request.getServerName(); } if (value == null || value.length() == 0) { value = "-"; } if (ipv6Canonical) { value = IPv6Utils.canonize(value); } buf.append(value); } } /** * write any string */ protected static class StringElement implements AccessLogElement { private final String str; public StringElement(String str) { this.str = str; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(str); } } /** * write incoming headers - %{xxx}i */ protected static class HeaderElement implements AccessLogElement { private final String header; public HeaderElement(String header) { this.header = header; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { Enumeration iter = request.getHeaders(header); if (iter.hasMoreElements()) { escapeAndAppend(iter.nextElement(), buf); while (iter.hasMoreElements()) { buf.append(','); escapeAndAppend(iter.nextElement(), buf); } return; } buf.append('-'); } } /** * write a specific cookie - %{xxx}c */ protected static class CookieElement implements AccessLogElement { private final String cookieNameToLog; public CookieElement(String cookieNameToLog) { this.cookieNameToLog = cookieNameToLog; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { StringBuilder value = new StringBuilder(); boolean first = true; Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookieNameToLog.equals(cookie.getName())) { if (first) { first = false; } else { value.append(','); } value.append(cookie.getValue()); } } } if (value.length() == 0) { buf.append('-'); } else { escapeAndAppend(value.toString(), buf); } } } /** * write a specific response header - %{xxx}o */ protected static class ResponseHeaderElement implements AccessLogElement { private final String header; public ResponseHeaderElement(String header) { this.header = header; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (null != response) { Iterator iter = response.getHeaders(header).iterator(); if (iter.hasNext()) { escapeAndAppend(iter.next(), buf); while (iter.hasNext()) { buf.append(','); escapeAndAppend(iter.next(), buf); } return; } } buf.append('-'); } } /** * write an attribute in the ServletRequest - %{xxx}r */ protected static class RequestAttributeElement implements AccessLogElement { private final String attribute; public RequestAttributeElement(String attribute) { this.attribute = attribute; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { Object value = null; if (request != null) { value = request.getAttribute(attribute); } else { value = "??"; } if (value != null) { if (value instanceof String) { escapeAndAppend((String) value, buf); } else { escapeAndAppend(value.toString(), buf); } } else { buf.append('-'); } } } /** * write an attribute in the HttpSession - %{xxx}s */ protected static class SessionAttributeElement implements AccessLogElement { private final String attribute; public SessionAttributeElement(String attribute) { this.attribute = attribute; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { Object value = null; if (null != request) { HttpSession sess = request.getSession(false); if (null != sess) { value = sess.getAttribute(attribute); } } else { value = "??"; } if (value != null) { if (value instanceof String) { escapeAndAppend((String) value, buf); } else { escapeAndAppend(value.toString(), buf); } } else { buf.append('-'); } } } /** * Write connection status when response is completed - %X */ protected static class ConnectionStatusElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (response != null && request != null) { boolean statusFound = false; // Check whether connection IO is in "not allowed" state AtomicBoolean isIoAllowed = new AtomicBoolean(false); request.getCoyoteRequest().action(ActionCode.IS_IO_ALLOWED, isIoAllowed); if (!isIoAllowed.get()) { buf.append('X'); statusFound = true; } else { // Check for connection aborted cond if (response.isError()) { Throwable ex = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); if (ex instanceof ClientAbortException) { buf.append('X'); statusFound = true; } } } // If status is not found yet, cont to check whether connection is keep-alive or close if (!statusFound) { String connStatus = response.getHeader(org.apache.coyote.http11.Constants.CONNECTION); if (org.apache.coyote.http11.Constants.CLOSE.equalsIgnoreCase(connStatus)) { buf.append('-'); } else { buf.append('+'); } } } else { // Unknown connection status buf.append('?'); } } } /** * Parse pattern string and create the array of AccessLogElement. * * @return the log elements array */ protected AccessLogElement[] createLogElements() { List list = new ArrayList<>(); boolean replace = false; StringBuilder buf = new StringBuilder(); for (int i = 0; i < pattern.length(); i++) { char ch = pattern.charAt(i); if (replace) { /* * For code that processes {, the behavior will be ... if I do not encounter a closing } - then I ignore * the { */ if ('{' == ch) { StringBuilder name = new StringBuilder(); int j = i + 1; for (; j < pattern.length() && '}' != pattern.charAt(j); j++) { name.append(pattern.charAt(j)); } if (j + 1 < pattern.length()) { /* the +1 was to account for } which we increment now */ j++; list.add(createAccessLogElement(name.toString(), pattern.charAt(j))); i = j; /* Since we walked more than one character */ } else { // D'oh - end of string - pretend we never did this // and do processing the "old way" list.add(createAccessLogElement(ch)); } } else { list.add(createAccessLogElement(ch)); } replace = false; } else if (ch == '%') { replace = true; list.add(new StringElement(buf.toString())); buf = new StringBuilder(); } else { buf.append(ch); } } if (buf.length() > 0) { list.add(new StringElement(buf.toString())); } return list.toArray(new AccessLogElement[0]); } private CachedElement[] createCachedElements(AccessLogElement[] elements) { List list = new ArrayList<>(); for (AccessLogElement element : elements) { if (element instanceof CachedElement) { list.add((CachedElement) element); } } return list.toArray(new CachedElement[0]); } /** * Create an AccessLogElement implementation which needs an element name. * * @param name Header name * @param pattern char in the log pattern * * @return the log element */ protected AccessLogElement createAccessLogElement(String name, char pattern) { switch (pattern) { case 'i': return new HeaderElement(name); case 'c': return new CookieElement(name); case 'o': return new ResponseHeaderElement(name); case 'a': return new RemoteAddrElement(name); case 'p': return new PortElement(name); case 'r': if (TLSUtil.isTLSRequestAttribute(name)) { tlsAttributeRequired = true; } return new RequestAttributeElement(name); case 's': return new SessionAttributeElement(name); case 't': return new DateAndTimeElement(name); default: return new StringElement("???"); } } /** * Create an AccessLogElement implementation. * * @param pattern char in the log pattern * * @return the log element */ protected AccessLogElement createAccessLogElement(char pattern) { switch (pattern) { case 'a': return new RemoteAddrElement(); case 'A': return new LocalAddrElement(ipv6Canonical); case 'b': return new ByteSentElement(true); case 'B': return new ByteSentElement(false); case 'D': return new ElapsedTimeElement(true); case 'F': return new FirstByteTimeElement(); case 'h': return new HostElement(); case 'H': return new ProtocolElement(); case 'l': return new LogicalUserNameElement(); case 'm': return new MethodElement(); case 'p': return new PortElement(); case 'q': return new QueryElement(); case 'r': return new RequestElement(); case 's': return new HttpStatusCodeElement(); case 'S': return new SessionIdElement(); case 't': return new DateAndTimeElement(); case 'T': return new ElapsedTimeElement(false); case 'u': return new UserElement(); case 'U': return new RequestURIElement(); case 'v': return new LocalServerNameElement(); case 'I': return new ThreadNameElement(); case 'X': return new ConnectionStatusElement(); default: return new StringElement("???" + pattern + "???"); } } /* * This method is intended to mimic the escaping performed by httpd and mod_log_config. mod_log_config escapes more * elements than indicated by the documentation. See: * https://github.com/apache/httpd/blob/trunk/modules/loggers/mod_log_config.c * * The following escaped elements are not supported by Tomcat: - %C cookie value (see %{}c below) - %e environment * variable - %f filename - %l remote logname (always logs "-") - %n note - %R handler - %ti trailer request header * - %to trailer response header - %V server name per UseCanonicalName setting * * The following escaped elements are not escaped in Tomcat because values that would require escaping are rejected * before they reach the AccessLogValve: - %h remote host - %H request protocol - %m request method - %q query * string - %r request line - %U request URI - %v canonical server name * * The following escaped elements are supported by Tomcat: - %{}i request header - %{}o response header - %u remote * user * * The following additional Tomcat elements are escaped for consistency: - %{}c cookie value - %{}r request * attribute - %{}s session attribute * * giving a total of 6 elements that are escaped in Tomcat. * * Quoting from the httpd docs: "...non-printable and other special characters in %r, %i and %o are escaped using * \xhh sequences, where hh stands for the hexadecimal representation of the raw byte. Exceptions from this rule are * " and \, which are escaped by prepending a backslash, and all whitespace characters, which are written in their * C-style notation (\n, \t, etc)." * * Reviewing the httpd code, characters with the high bit set are escaped. The httpd is assuming a single byte * encoding which may not be true for Tomcat so Tomcat uses the Java \\uXXXX encoding. */ protected static void escapeAndAppend(String input, CharArrayWriter dest) { if (input == null || input.isEmpty()) { dest.append('-'); return; } int len = input.length(); // As long as we don't encounter chars that need escaping, // we only remember start and length of that string part. // "next" is the start of the string part containing these chars, // "current - 1" is its end. So writing from "next" with length // "current - next" writes that part. // We write that part whenever we find a character to escape and the // unchanged and unwritten string part is not empty. int next = 0; char c; for (int current = 0; current < len; current++) { c = input.charAt(current); // Fast path if (c >= 32 && c < 127) { // special case " and \ switch (c) { case '\\': // dec 92 // Write unchanged string parts if (current > next) { dest.write(input, next, current - next); } next = current + 1; dest.append("\\\\"); break; case '\"': // dec 34 // Write unchanged string parts if (current > next) { dest.write(input, next, current - next); } next = current + 1; dest.append("\\\""); break; // Don't output individual unchanged chars, // write the sub string only when the first char to encode // is encountered plus at the end. default: } // Control (1-31), delete (127) or above 127 } else { // Write unchanged string parts if (current > next) { dest.write(input, next, current - next); } next = current + 1; switch (c) { // Standard escapes for some control chars case '\f': // dec 12 dest.append("\\f"); break; case '\n': // dec 10 dest.append("\\n"); break; case '\r': // dec 13 dest.append("\\r"); break; case '\t': // dec 09 dest.append("\\t"); break; // Unicode escape \\uXXXX default: dest.append("\\u"); dest.append(HexUtils.toHexString(c)); } } } // Write remaining unchanged string parts if (len > next) { dest.write(input, next, len - next); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy