/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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 io.undertow.server.handlers.accesslog;

import io.undertow.UndertowLogger;
import io.undertow.Version;
import io.undertow.attribute.AuthenticationTypeExchangeAttribute;
import io.undertow.attribute.BytesSentAttribute;
import io.undertow.attribute.CompositeExchangeAttribute;
import io.undertow.attribute.ConstantExchangeAttribute;
import io.undertow.attribute.CookieAttribute;
import io.undertow.attribute.DateTimeAttribute;
import io.undertow.attribute.ExchangeAttribute;
import io.undertow.attribute.ExchangeAttributeParser;
import io.undertow.attribute.ExchangeAttributes;
import io.undertow.attribute.LocalIPAttribute;
import io.undertow.attribute.QueryStringAttribute;
import io.undertow.attribute.QuotingExchangeAttribute;
import io.undertow.attribute.ReadOnlyAttributeException;
import io.undertow.attribute.RemoteIPAttribute;
import io.undertow.attribute.RemoteUserAttribute;
import io.undertow.attribute.RequestHeaderAttribute;
import io.undertow.attribute.RequestMethodAttribute;
import io.undertow.attribute.RequestProtocolAttribute;
import io.undertow.attribute.RequestSchemeAttribute;
import io.undertow.attribute.RequestURLAttribute;
import io.undertow.attribute.ResponseCodeAttribute;
import io.undertow.attribute.ResponseHeaderAttribute;
import io.undertow.attribute.ResponseTimeAttribute;
import io.undertow.attribute.SecureExchangeAttribute;
import io.undertow.attribute.SubstituteEmptyWrapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;

import java.io.IOException;
import java.io.StringReader;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

Parser that transforms an extended access log format string into a Undertow access log format string.
Author:Stuart Douglas
/** * Parser that transforms an extended access log format string into a * Undertow access log format string. * * @author Stuart Douglas */
public class ExtendedAccessLogParser {
parser that is used to access servlet context attributes, to avoid bringing in servlet context dependencies
/** * parser that is used to access servlet context attributes, to avoid bringing in servlet * context dependencies */
private final ExchangeAttributeParser parser; public ExtendedAccessLogParser(ClassLoader classLoader) { this.parser = ExchangeAttributes.parser(classLoader, QuotingExchangeAttribute.WRAPPER); } private static class PatternTokenizer { private StringReader sr = null; private StringBuilder buf = new StringBuilder(); private boolean ended = false; private boolean subToken; private boolean parameter; PatternTokenizer(String str) { sr = new StringReader(str); } public boolean hasSubToken() { return subToken; } public boolean hasParameter() { return parameter; } public String getToken() throws IOException { if (ended) return null; String result = null; subToken = false; parameter = false; int c = sr.read(); while (c != -1) { switch (c) { case ' ': result = buf.toString(); buf = new StringBuilder(); buf.append((char) c); return result; case '-': result = buf.toString(); buf = new StringBuilder(); subToken = true; return result; case '(': result = buf.toString(); buf = new StringBuilder(); parameter = true; return result; case ')': result = buf.toString(); buf = new StringBuilder(); break; default: buf.append((char) c); } c = sr.read(); } ended = true; if (buf.length() != 0) { return buf.toString(); } else { return null; } } public String getParameter() throws IOException { String result; if (!parameter) { return null; } parameter = false; int c = sr.read(); while (c != -1) { if (c == ')') { result = buf.toString(); buf = new StringBuilder(); return result; } buf.append((char) c); c = sr.read(); } return null; } public String getWhiteSpaces() throws IOException { if (isEnded()) return ""; StringBuilder whiteSpaces = new StringBuilder(); if (buf.length() > 0) { whiteSpaces.append(buf); buf = new StringBuilder(); } int c = sr.read(); while (Character.isWhitespace((char) c)) { whiteSpaces.append((char) c); c = sr.read(); } if (c == -1) { ended = true; } else { buf.append((char) c); } return whiteSpaces.toString(); } public boolean isEnded() { return ended; } public String getRemains() throws IOException { StringBuilder remains = new StringBuilder(); for (int c = sr.read(); c != -1; c = sr.read()) { remains.append((char) c); } return remains.toString(); } } public ExchangeAttribute parse(String pattern) { List<ExchangeAttribute> list = new ArrayList<ExchangeAttribute>(); PatternTokenizer tokenizer = new PatternTokenizer(pattern); try { // Ignore leading whitespace. tokenizer.getWhiteSpaces(); if (tokenizer.isEnded()) { UndertowLogger.ROOT_LOGGER.extendedAccessLogEmptyPattern(); return null; } String token = tokenizer.getToken(); while (token != null) { if (UndertowLogger.ROOT_LOGGER.isDebugEnabled()) { UndertowLogger.ROOT_LOGGER.debug("token = " + token); } ExchangeAttribute element = getLogElement(token, tokenizer); if (element == null) { break; } list.add(element); String whiteSpaces = tokenizer.getWhiteSpaces(); if (whiteSpaces.length() > 0) { list.add(new ConstantExchangeAttribute(whiteSpaces)); } if (tokenizer.isEnded()) { break; } token = tokenizer.getToken(); } if (UndertowLogger.ROOT_LOGGER.isDebugEnabled()) { UndertowLogger.ROOT_LOGGER.debug("finished decoding with element size of: " + list.size()); } return new CompositeExchangeAttribute(list.toArray(new ExchangeAttribute[list.size()])); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.extendedAccessLogPatternParseError(e); return null; } } protected ExchangeAttribute getLogElement(String token, PatternTokenizer tokenizer) throws IOException { if ("date".equals(token)) { return new DateTimeAttribute("yyyy-MM-dd", "GMT"); } else if ("time".equals(token)) { if (tokenizer.hasSubToken()) { String nextToken = tokenizer.getToken(); if ("taken".equals(nextToken)) { //if response timing are not enabled we just print a '-' return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(new ResponseTimeAttribute(TimeUnit.SECONDS), "-"); } } else { return new DateTimeAttribute("HH:mm:ss", "GMT"); } } else if ("bytes".equals(token)) { return new BytesSentAttribute(true); } else if ("cached".equals(token)) { /* I don't know how to evaluate this! */ return new ConstantExchangeAttribute("-"); } else if ("c".equals(token)) { String nextToken = tokenizer.getToken(); if ("ip".equals(nextToken)) { return RemoteIPAttribute.INSTANCE; } else if ("dns".equals(nextToken)) { return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { final InetSocketAddress peerAddress = exchange.getConnection().getPeerAddress(InetSocketAddress.class); try { return peerAddress.getHostName(); } catch (Throwable e) { return peerAddress.getHostString(); } } @Override public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { throw new ReadOnlyAttributeException(); } }; } } else if ("s".equals(token)) { String nextToken = tokenizer.getToken(); if ("ip".equals(nextToken)) { return LocalIPAttribute.INSTANCE; } else if ("dns".equals(nextToken)) { return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { try { return exchange.getConnection().getLocalAddress(InetSocketAddress.class).getHostName(); } catch (Throwable e) { return "localhost"; } } @Override public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { throw new ReadOnlyAttributeException(); } }; } } else if ("cs".equals(token)) { return getClientToServerElement(tokenizer); } else if ("sc".equals(token)) { return getServerToClientElement(tokenizer); } else if ("sr".equals(token) || "rs".equals(token)) { return getProxyElement(tokenizer); } else if ("x".equals(token)) { return getXParameterElement(tokenizer); } UndertowLogger.ROOT_LOGGER.extendedAccessLogUnknownToken(token); return null; } protected ExchangeAttribute getClientToServerElement( PatternTokenizer tokenizer) throws IOException { if (tokenizer.hasSubToken()) { String token = tokenizer.getToken(); if ("method".equals(token)) { return RequestMethodAttribute.INSTANCE; } else if ("uri".equals(token)) { if (tokenizer.hasSubToken()) { token = tokenizer.getToken(); if ("stem".equals(token)) { return RequestURLAttribute.INSTANCE; } else if ("query".equals(token)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(QueryStringAttribute.BARE_INSTANCE, "-"); } } else { return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { String query = exchange.getQueryString(); if (query.isEmpty()) { return exchange.getRequestURI(); } else { StringBuilder buf = new StringBuilder(); buf.append(exchange.getRequestURI()); buf.append('?'); buf.append(exchange.getQueryString()); return buf.toString(); } } @Override public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { throw new ReadOnlyAttributeException(); } }; } } } else if (tokenizer.hasParameter()) { String parameter = tokenizer.getParameter(); if (parameter == null) { UndertowLogger.ROOT_LOGGER.extendedAccessLogMissingClosing(); return null; } return new QuotingExchangeAttribute(new RequestHeaderAttribute(new HttpString(parameter))); } UndertowLogger.ROOT_LOGGER.extendedAccessLogCannotDecode(tokenizer.getRemains()); return null; } protected ExchangeAttribute getServerToClientElement( PatternTokenizer tokenizer) throws IOException { if (tokenizer.hasSubToken()) { String token = tokenizer.getToken(); if ("status".equals(token)) { return ResponseCodeAttribute.INSTANCE; } else if ("comment".equals(token)) { return new ConstantExchangeAttribute("?"); } } else if (tokenizer.hasParameter()) { String parameter = tokenizer.getParameter(); if (parameter == null) { UndertowLogger.ROOT_LOGGER.extendedAccessLogMissingClosing(); return null; } return new QuotingExchangeAttribute(new ResponseHeaderAttribute(new HttpString(parameter))); } UndertowLogger.ROOT_LOGGER.extendedAccessLogCannotDecode(tokenizer.getRemains()); return null; } protected ExchangeAttribute getProxyElement(PatternTokenizer tokenizer) throws IOException { String token = null; if (tokenizer.hasSubToken()) { tokenizer.getToken(); return new ConstantExchangeAttribute("-"); } else if (tokenizer.hasParameter()) { tokenizer.getParameter(); return new ConstantExchangeAttribute("-"); } UndertowLogger.ROOT_LOGGER.extendedAccessLogCannotDecode(token); return null; } protected ExchangeAttribute getXParameterElement(PatternTokenizer tokenizer) throws IOException { if (!tokenizer.hasSubToken()) { UndertowLogger.ROOT_LOGGER.extendedAccessLogBadXParam(); return null; } final String token = tokenizer.getToken(); if (!tokenizer.hasParameter()) { UndertowLogger.ROOT_LOGGER.extendedAccessLogBadXParam(); return null; } String parameter = tokenizer.getParameter(); if (parameter == null) { UndertowLogger.ROOT_LOGGER.extendedAccessLogMissingClosing(); return null; } if ("A".equals(token)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(parser.parse("%{sc," + parameter + "}"),"-"); } else if ("C".equals(token)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(new CookieAttribute(parameter),"-"); } else if ("R".equals(token)) { return parser.parse("%{r," + parameter + "}"); } else if ("S".equals(token)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(parser.parse("%{s," + parameter + "}"),"-"); } else if ("H".equals(token)) { return getServletRequestElement(parameter); } else if ("P".equals(token)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(parser.parse("%{rp," + parameter + "}"),"-"); } else if ("O".equals(token)) { return new QuotingExchangeAttribute(new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { HeaderValues values = exchange.getResponseHeaders().get(parameter); if (values != null && values.size() > 0) { StringBuilder buffer = new StringBuilder(); for (int i = 0; i < values.size(); i++) { String string = values.get(i); buffer.append(string); if (i + 1 < values.size()) buffer.append(","); } return buffer.toString(); } return null; } @Override public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { throw new ReadOnlyAttributeException(); } }); } UndertowLogger.ROOT_LOGGER.extendedAccessLogCannotDecodeXParamValue(token); return null; } protected ExchangeAttribute getServletRequestElement(String parameter) { if ("authType".equals(parameter)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(AuthenticationTypeExchangeAttribute.INSTANCE,"-"); } else if ("remoteUser".equals(parameter)) { return new SubstituteEmptyWrapper.SubstituteEmptyAttribute(RemoteUserAttribute.INSTANCE,"-"); } else if ("requestedSessionId".equals(parameter)) { return parser.parse("%{REQUESTED_SESSION_ID}"); } else if ("requestedSessionIdFromCookie".equals(parameter)) { return parser.parse("%{REQUESTED_SESSION_ID_FROM_COOKIE}"); } else if ("requestedSessionIdValid".equals(parameter)) { return parser.parse("%{REQUESTED_SESSION_ID_VALID}"); } else if ("contentLength".equals(parameter)) { return new QuotingExchangeAttribute(new RequestHeaderAttribute(Headers.CONTENT_LENGTH)); } else if ("characterEncoding".equals(parameter)) { return parser.parse("%{REQUEST_CHARACTER_ENCODING}"); } else if ("locale".equals(parameter)) { return parser.parse("%{REQUEST_LOCALE}"); } else if ("protocol".equals(parameter)) { return RequestProtocolAttribute.INSTANCE; } else if ("scheme".equals(parameter)) { return RequestSchemeAttribute.INSTANCE; } else if ("secure".equals(parameter)) { return SecureExchangeAttribute.INSTANCE; } UndertowLogger.ROOT_LOGGER.extendedAccessLogCannotDecodeXParamValue(parameter); return null; } public static class ExtendedAccessLogHeaderGenerator implements LogFileHeaderGenerator { private final String pattern; public ExtendedAccessLogHeaderGenerator(String pattern) { this.pattern = pattern; } @Override public String generateHeader() { StringBuilder sb = new StringBuilder(); sb.append("#Fields: "); sb.append(pattern); sb.append(System.lineSeparator()); sb.append("#Version: 2.0"); sb.append(System.lineSeparator()); sb.append("#Software: "); sb.append(Version.getFullVersionString()); sb.append(System.lineSeparator()); return sb.toString(); } } }