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;
public class ExtendedAccessLogParser {
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 {
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)) {
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)) {
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();
}
}
}