| /* |
| * 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.time.Instant; |
| 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.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| import jakarta.servlet.RequestDispatcher; |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.http.Cookie; |
| import jakarta.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; |
| |
| |
| /** |
| * <p> |
| * Abstract implementation of the <b>Valve</b> 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 |
| * <a href="https://httpd.apache.org/">Apache HTTP Server</a> <code>mod_log_config</code> module. |
| * </p> |
| * <p> |
| * 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: |
| * </p> |
| * <ul> |
| * <li><b><code>%a</code></b> - Remote IP address |
| * <li><b><code>%A</code></b> - Local IP address |
| * <li><b><code>%b</code></b> - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent |
| * <li><b><code>%B</code></b> - Bytes sent, excluding HTTP headers |
| * <li><b><code>%h</code></b> - Remote host name (or IP address if <code>enableLookups</code> for the connector is |
| * false) |
| * <li><b><code>%H</code></b> - Request protocol |
| * <li><b><code>%l</code></b> - Remote logical username from identd (always returns '-') |
| * <li><b><code>%m</code></b> - Request method |
| * <li><b><code>%p</code></b> - Local port |
| * <li><b><code>%q</code></b> - Query string (prepended with a '?' if it exists, otherwise an empty string |
| * <li><b><code>%r</code></b> - First line of the request |
| * <li><b><code>%s</code></b> - HTTP status code of the response |
| * <li><b><code>%S</code></b> - User session ID |
| * <li><b><code>%t</code></b> - Date and time, in Common Log Format format |
| * <li><b><code>%u</code></b> - Remote user that was authenticated |
| * <li><b><code>%U</code></b> - Requested URL path |
| * <li><b><code>%v</code></b> - Local server name |
| * <li><b><code>%D</code></b> - Time taken to process the request, in microseconds |
| * <li><b><code>%T</code></b> - Time taken to process the request, in seconds |
| * <li><b><code>%F</code></b> - Time taken to commit the response, in milliseconds |
| * <li><b><code>%I</code></b> - current Request thread name (can compare later with stacktraces) |
| * <li><b><code>%X</code></b> - Connection status when response is completed: |
| * <ul> |
| * <li><code>X</code> = Connection aborted before the response completed.</li> |
| * <li><code>+</code> = Connection may be kept alive after the response is sent.</li> |
| * <li><code>-</code> = Connection will be closed after the response is sent.</li> |
| * </ul> |
| * </ul> |
| * <p> |
| * In addition, the caller can specify one of the following aliases for commonly utilized patterns: |
| * </p> |
| * <ul> |
| * <li><b>common</b> - <code>%h %l %u %t "%r" %s %b</code> |
| * <li><b>combined</b> - <code>%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"</code> |
| * </ul> |
| * <p> |
| * There is also support to write information from the cookie, incoming header, the Session or something else in the |
| * ServletRequest.<br> |
| * It is modeled after the <a href="https://httpd.apache.org/">Apache HTTP Server</a> log configuration syntax: |
| * </p> |
| * <ul> |
| * <li><code>%{xxx}i</code> for incoming headers |
| * <li><code>%{xxx}o</code> for outgoing response headers |
| * <li><code>%{xxx}c</code> for a specific cookie |
| * <li><code>%{xxx}r</code> xxx is an attribute in the ServletRequest |
| * <li><code>%{xxx}s</code> xxx is an attribute in the HttpSession |
| * <li><code>%{xxx}t</code> xxx is an enhanced SimpleDateFormat pattern (see Configuration Reference document for |
| * details on supported time patterns) |
| * <li><code>%{xxx}L</code> xxx is the identifier to log (see Configuration Reference document for details on supported |
| * identifiers) |
| * <li><code>%{xxx}T</code> xxx is the unit for the time taken to process the request (see Configuration Reference |
| * document for details on supported units) |
| * </ul> |
| * <p> |
| * Conditional logging is also supported. This can be done with the <code>conditionUnless</code> and |
| * <code>conditionIf</code> 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 <code>condition</code> attribute is synonym for |
| * <code>conditionUnless</code> and is provided for backwards compatibility. |
| * </p> |
| * <p> |
| * For extended attributes coming from a getAttribute() call, it is you responsibility to ensure there are no newline or |
| * control characters. |
| * </p> |
| * @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 |
| } |
| |
| private enum IdentifierType { |
| CONNECTION, |
| UNKNOWN |
| } |
| |
| |
| 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; |
| |
| /** |
| * <p> |
| * Cache structure for formatted timestamps based on seconds. |
| * </p> |
| * <p> |
| * 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. |
| * </p> |
| * <p> |
| * There is one cache for the CLF format (the access log standard format) and a HashMap of caches for additional |
| * formats used by SimpleDateFormat. |
| * </p> |
| * <p> |
| * 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 <code>en_US</code>. |
| * </p> |
| * <p> |
| * The cache is not threadsafe. It can be used without synchronization via thread local instances, or with |
| * synchronization as a global cache. |
| * </p> |
| * <p> |
| * The cache can be created with a parent cache to build a cache hierarchy. Access to the parent cache is |
| * threadsafe. |
| * </p> |
| * <p> |
| * This class uses a small thread local first level cache and a bigger synchronized global second level cache. |
| * </p> |
| */ |
| 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 final SimpleDateFormat formatter; |
| private boolean isCLF = false; |
| |
| private final Cache parent; |
| |
| 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) { |
| previousFormat = "[" + previousFormat + "]"; |
| } |
| } |
| cache[index] = previousFormat; |
| return previousFormat; |
| } |
| } |
| |
| /* Number of cached entries */ |
| private final int cacheSize; |
| |
| private final Locale cacheDefaultLocale; |
| private final DateFormatCache parent; |
| protected final Cache cLFCache; |
| private final Map<String,Cache> formatCache = new HashMap<>(); |
| |
| protected DateFormatCache(int size, Locale loc, DateFormatCache parentFC) { |
| cacheSize = size; |
| cacheDefaultLocale = loc; |
| parent = parentFC; |
| 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<DateFormatCache> localDateCache = |
| ThreadLocal.withInitial(() -> new DateFormatCache(localCacheSize, Locale.getDefault(), globalDateCache)); |
| |
| |
| /** |
| * Are we doing conditional logging ? default null. It is the value of <code>conditionUnless</code> property. |
| */ |
| protected String condition = null; |
| |
| /** |
| * Are we doing conditional logging ? default null. It is the value of <code>conditionIf</code> 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 <code>false</code>. |
| * |
| * @see #setRequestAttributesEnabled(boolean) |
| */ |
| protected boolean requestAttributesEnabled = false; |
| |
| /** |
| * Buffer pool used for log message generation. Pool used to reduce garbage generation. |
| */ |
| private final SynchronizedStack<CharArrayWriter> 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 <code>false</code>. |
| */ |
| @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 |
| |
| @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; |
| } |
| |
| CharArrayWriter result = charArrayWriters.pop(); |
| if (result == null) { |
| result = new CharArrayWriter(128); |
| } |
| |
| for (AccessLogElement logElement : logElements) { |
| logElement.addElement(result, 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 |
| |
| /** |
| * 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 { |
| /** |
| * Called to create an access log entry. |
| * |
| * @param buf The buffer to which the log element should be added |
| * @param request The request that triggered this access log entry |
| * @param response The response to the request that triggered this access log entry |
| * @param time The time taken in nanoseconds to process the request |
| */ |
| void addElement(CharArrayWriter buf, Request request, Response response, long time); |
| } |
| |
| /** |
| * Marks an AccessLogElement as needing to 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, 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 t) { |
| ExceptionUtils.handleThrowable(t); |
| init = "127.0.0.1"; |
| } |
| |
| if (ipv6Canonical) { |
| localAddrValue = IPv6Utils.canonize(init); |
| } else { |
| localAddrValue = init; |
| } |
| } |
| |
| @Override |
| public void addElement(CharArrayWriter buf, 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, Request request, Response response, long time) { |
| String value; |
| 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, 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.isEmpty()) { |
| value = request.getRemoteHost(); |
| } |
| if (value == null || value.isEmpty()) { |
| 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, Request request, Response response, long time) { |
| buf.append('-'); |
| } |
| } |
| |
| /** |
| * write request protocol - %H |
| */ |
| protected class ProtocolElement implements AccessLogElement { |
| @Override |
| public void addElement(CharArrayWriter buf, 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, 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.isEmpty()) { |
| 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, Request request, Response response, long time) { |
| Instant requestStartInstant = Instant.from(request.getCoyoteRequest().getStartInstant()); |
| long frac; |
| if (!usesBegin) { |
| requestStartInstant.plusNanos(time); |
| } |
| switch (type) { |
| case CLF: |
| buf.append(localDateCache.get().getFormat(requestStartInstant.toEpochMilli())); |
| break; |
| case SEC: |
| buf.append(Long.toString(requestStartInstant.getEpochSecond())); |
| break; |
| case MSEC: |
| buf.append(Long.toString(requestStartInstant.toEpochMilli())); |
| break; |
| case MSEC_FRAC: |
| frac = requestStartInstant.toEpochMilli() % 1000; |
| if (frac < 100) { |
| buf.append('0'); |
| if (frac < 10) { |
| buf.append('0'); |
| } |
| } |
| buf.append(Long.toString(frac)); |
| break; |
| case SDF: |
| long timestamp = requestStartInstant.toEpochMilli(); |
| String temp = localDateCache.get().getFormat(format, locale, timestamp); |
| if (usesMsecs) { |
| frac = timestamp % 1000; |
| StringBuilder tripleMsec = new StringBuilder(4); |
| if (frac < 100) { |
| tripleMsec.append('0'); |
| if (frac < 10) { |
| 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); |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * write first line of the request (method and request URI) - %r |
| */ |
| protected static class RequestElement implements AccessLogElement { |
| @Override |
| public void addElement(CharArrayWriter buf, 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, 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, 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 <code>true</code> to write '-' instead of 0 - %b. |
| */ |
| public ByteSentElement(boolean conversion) { |
| this.conversion = conversion; |
| } |
| |
| @Override |
| public void addElement(CharArrayWriter buf, 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, 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 { |
| public enum Style { |
| SECONDS { |
| @Override |
| public void append(CharArrayWriter buf, long time) { |
| buf.append(Long.toString(TimeUnit.NANOSECONDS.toSeconds(time))); |
| } |
| }, |
| SECONDS_FRACTIONAL { |
| @Override |
| public void append(CharArrayWriter buf, long time) { |
| time = time / 1000000; // Convert to millis |
| 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)); |
| } |
| }, |
| MILLISECONDS { |
| @Override |
| public void append(CharArrayWriter buf, long time) { |
| buf.append(Long.toString(TimeUnit.NANOSECONDS.toMillis(time))); |
| } |
| }, |
| MICROSECONDS { |
| @Override |
| public void append(CharArrayWriter buf, long time) { |
| buf.append(Long.toString(TimeUnit.NANOSECONDS.toMicros(time))); |
| } |
| }, |
| NANOSECONDS { |
| @Override |
| public void append(CharArrayWriter buf, long time) { |
| buf.append(Long.toString(time)); |
| } |
| }; |
| |
| /** |
| * Append the time to the buffer in the appropriate format. |
| * |
| * @param buf The buffer to append to. |
| * @param time The time to log in nanoseconds. |
| */ |
| public abstract void append(CharArrayWriter buf, long time); |
| } |
| |
| private final Style style; |
| |
| /** |
| * Creates a new ElapsedTimeElement that will log the time in the specified style. |
| * |
| * @param style The elapsed-time style to use. |
| */ |
| public ElapsedTimeElement(Style style) { |
| this.style = style; |
| } |
| |
| /** |
| * @param micros <code>true</code>, write time in microseconds - %D |
| * @param millis <code>true</code>, write time in milliseconds, if both arguments are <code>false</code>, write |
| * time in seconds - %T |
| */ |
| public ElapsedTimeElement(boolean micros, boolean millis) { |
| this(micros ? Style.MICROSECONDS : millis ? Style.MILLISECONDS : Style.SECONDS); |
| } |
| |
| @Override |
| public void addElement(CharArrayWriter buf, Request request, Response response, long time) { |
| style.append(buf, time); |
| } |
| } |
| |
| /** |
| * write time until first byte is written (commit time) in millis - %F |
| */ |
| protected static class FirstByteTimeElement implements AccessLogElement { |
| @Override |
| public void addElement(CharArrayWriter buf, Request request, Response response, long time) { |
| long commitTime = response.getCoyoteResponse().getCommitTimeNanos(); |
| if (commitTime == -1) { |
| buf.append('-'); |
| } else { |
| long delta = commitTime - request.getCoyoteRequest().getStartTimeNanos(); |
| buf.append(Long.toString(TimeUnit.NANOSECONDS.toMillis(delta))); |
| } |
| } |
| } |
| |
| /** |
| * write Query string (prepended with a '?' if it exists) - %q |
| */ |
| protected static class QueryElement implements AccessLogElement { |
| @Override |
| public void addElement(CharArrayWriter buf, 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, 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, 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, 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.isEmpty()) { |
| value = request.getServerName(); |
| } |
| if (value == null || value.isEmpty()) { |
| 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, 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, Request request, Response response, long time) { |
| Enumeration<String> 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, Request request, Response response, long time) { |
| StringBuilder value = null; |
| boolean first = true; |
| Cookie[] cookies = request.getCookies(); |
| if (cookies != null) { |
| for (Cookie cookie : cookies) { |
| if (cookieNameToLog.equals(cookie.getName())) { |
| if (value == null) { |
| value = new StringBuilder(); |
| } |
| if (first) { |
| first = false; |
| } else { |
| value.append(','); |
| } |
| value.append(cookie.getValue()); |
| } |
| } |
| } |
| if (value == null) { |
| 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, Request request, Response response, long time) { |
| if (null != response) { |
| Iterator<String> 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, Request request, Response response, long time) { |
| Object value; |
| 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, 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, 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('?'); |
| } |
| } |
| } |
| |
| |
| /** |
| * Write identifier element %{xxx}L |
| */ |
| protected static class IdentifierElement implements AccessLogElement { |
| |
| /** |
| * Type of identifier to log |
| */ |
| private final IdentifierType identifierType; |
| |
| public IdentifierElement() { |
| this(""); |
| } |
| |
| |
| public IdentifierElement(String type) { |
| if ("c".equals(type)) { |
| identifierType = IdentifierType.CONNECTION; |
| } else { |
| log.error(sm.getString("accessLogValve.invalidIdentifierType", type)); |
| identifierType = IdentifierType.UNKNOWN; |
| } |
| } |
| |
| @Override |
| public void addElement(CharArrayWriter buf, Request request, Response response, long time) { |
| switch (identifierType) { |
| case CONNECTION: |
| buf.append(request.getServletConnection().getConnectionId()); |
| break; |
| case UNKNOWN: |
| buf.append("???"); |
| } |
| } |
| } |
| |
| |
| /** |
| * Parse pattern string and create the array of AccessLogElement. |
| * |
| * @return the log elements array |
| */ |
| protected AccessLogElement[] createLogElements() { |
| List<AccessLogElement> 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.isEmpty()) { |
| list.add(new StringElement(buf.toString())); |
| } |
| return list.toArray(new AccessLogElement[0]); |
| } |
| |
| |
| private CachedElement[] createCachedElements(AccessLogElement[] elements) { |
| List<CachedElement> 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) { |
| return switch (pattern) { |
| case 'a' -> new RemoteAddrElement(name); |
| case 'c' -> new CookieElement(name); |
| case 'i' -> new HeaderElement(name); |
| case 'L' -> new IdentifierElement(name); |
| case 'o' -> new ResponseHeaderElement(name); |
| case 'p' -> new PortElement(name); |
| case 'r' -> { |
| if (TLSUtil.isTLSRequestAttribute(name)) { |
| tlsAttributeRequired = true; |
| } |
| yield new RequestAttributeElement(name); |
| } |
| case 's' -> new SessionAttributeElement(name); |
| case 't' -> new DateAndTimeElement(name); |
| case 'T' -> |
| // ms for milliseconds, us for microseconds, and s for seconds |
| switch (name) { |
| case "ns" -> new ElapsedTimeElement(ElapsedTimeElement.Style.NANOSECONDS); |
| case "us" -> new ElapsedTimeElement(ElapsedTimeElement.Style.MICROSECONDS); |
| case "ms" -> new ElapsedTimeElement(ElapsedTimeElement.Style.MILLISECONDS); |
| case "fracsec" -> new ElapsedTimeElement(ElapsedTimeElement.Style.SECONDS_FRACTIONAL); |
| case null, default -> new ElapsedTimeElement(false, false); |
| }; |
| default -> new StringElement("???"); |
| }; |
| } |
| |
| /** |
| * Create an AccessLogElement implementation. |
| * |
| * @param pattern char in the log pattern |
| * |
| * @return the log element |
| */ |
| protected AccessLogElement createAccessLogElement(char pattern) { |
| return switch (pattern) { |
| case 'a' -> new RemoteAddrElement(); |
| case 'A' -> new LocalAddrElement(ipv6Canonical); |
| case 'b' -> new ByteSentElement(true); |
| case 'B' -> new ByteSentElement(false); |
| case 'D' -> new ElapsedTimeElement(true, false); |
| case 'F' -> new FirstByteTimeElement(); |
| case 'h' -> new HostElement(); |
| case 'H' -> new ProtocolElement(); |
| case 'l' -> new LogicalUserNameElement(); |
| case 'm' -> new MethodElement(); |
| case 'p' -> new PortElement(); |
| case 'q' -> new QueryElement(); |
| case 'r' -> new RequestElement(); |
| case 's' -> new HttpStatusCodeElement(); |
| case 'S' -> new SessionIdElement(); |
| case 't' -> new DateAndTimeElement(); |
| case 'T' -> new ElapsedTimeElement(false, false); |
| case 'u' -> new UserElement(); |
| case 'U' -> new RequestURIElement(); |
| case 'v' -> new LocalServerNameElement(); |
| case 'I' -> new ThreadNameElement(); |
| case 'X' -> new ConnectionStatusElement(); |
| default -> 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) { |
| escapeAndAppend(input, dest, false); |
| } |
| |
| protected static void escapeAndAppend(String input, CharArrayWriter dest, boolean escapeQuoteAsDouble) { |
| 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; |
| if (escapeQuoteAsDouble) { |
| dest.append("\"\""); |
| } else { |
| 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"); |
| case '\n' -> // dec 10 |
| dest.append("\\n"); |
| case '\r' -> // dec 13 |
| dest.append("\\r"); |
| case '\t' -> // dec 09 |
| dest.append("\\t"); |
| // 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); |
| } |
| } |
| } |