--- java/org/apache/catalina/valves/AbstractAccessLogValve.java (revision 1566763) +++ java/org/apache/catalina/valves/AbstractAccessLogValve.java (working copy) @@ -46,6 +46,7 @@ import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.net.IPv6Utils; /** @@ -148,6 +149,11 @@ */ protected boolean enabled = true; + /** + * Use IPv6 canonical representation format as defined by RFC 5952. + */ + protected boolean canonical = true; + /** * The pattern used to format our access log lines. */ @@ -484,6 +490,22 @@ } /** + * Are IPv6 addresses represented in canonical representation format? + */ + public boolean isCanonical() { + return canonical; + } + + /** + * Set the value if IPv6 addresses should be represented in canonical representation format. + * + * @param canonical true if canonical. + */ + public void setCanonical(boolean canonical) { + this.canonical = canonical; + } + + /** * Return the format pattern. */ public String getPattern() { @@ -748,11 +770,11 @@ /** * write local IP address - %A */ - protected static class LocalAddrElement implements AccessLogElement { + protected class LocalAddrElement implements AccessLogElement { - private static final String LOCAL_ADDR_VALUE; + private String localAddrValue; - static { + private void lazyInit() { String init; try { init = InetAddress.getLocalHost().getHostAddress(); @@ -760,13 +782,21 @@ ExceptionUtils.handleThrowable(e); init = "127.0.0.1"; } - LOCAL_ADDR_VALUE = init; + + if (canonical) { + localAddrValue = IPv6Utils.canonize(init); + } else { + localAddrValue = init; + } } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { - buf.append(LOCAL_ADDR_VALUE); + if (localAddrValue == null) { + lazyInit(); + } + buf.append(localAddrValue); } } @@ -777,15 +807,16 @@ @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + String value = null; if (requestAttributesEnabled) { Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE); if (addr == null) { - buf.append(request.getRemoteAddr()); + value = request.getRemoteAddr(); } else { - buf.append(addr.toString()); + value = addr.toString(); } } else { - buf.append(request.getRemoteAddr()); + value = request.getRemoteAddr(); } } } @@ -810,6 +841,10 @@ if (value == null || value.length() == 0) { value = "-"; } + + if (canonical) { + value = IPv6Utils.canonize(value); + } buf.append(value); } } @@ -1268,11 +1303,15 @@ /** * write local server name - %v */ - protected static class LocalServerNameElement implements AccessLogElement { + protected class LocalServerNameElement implements AccessLogElement { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { - buf.append(request.getServerName()); + if (canonical) { + buf.append(IPv6Utils.canonize(request.getServerName())); + } else { + buf.append(request.getServerName()); + } } } --- java/org/apache/tomcat/util/net/IPv6Utils.java (revision 0) +++ java/org/apache/tomcat/util/net/IPv6Utils.java (working copy) @@ -0,0 +1,252 @@ +/* + * 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.tomcat.util.net; + +/** + *

IPv6 utilities. + *

For the moment, it only contains function to canonize IPv6 address into + * RFC 5952 form. + */ +public class IPv6Utils { + + private static final int MAX_NUMBER_OF_GROUPS = 8; + private static final int MAX_GROUP_LENGTH = 4; + + /** + *

Convert IPv6 adress into RFC 5952 form. + * E.g. 2001:db8:0:1:0:0:0:1 -> 2001:db8:0:1::1

+ * + *

Method is null safe, and if IPv4 address or host name is passed to the + * method it is returned wihout any processing.

+ * + *

Method also supports IPv4 in IPv6 (e.g. 0:0:0:0:0:ffff:192.0.2.1 -> + * ::ffff:192.0.2.1), and zone ID (e.g. fe80:0:0:0:f0f0:c0c0:1919:1234%4 + * -> fe80::f0f0:c0c0:1919:1234%4).

+ * + * @param ipv6Address String representing valid IPv6 address. + * @return String representing IPv6 in canonical form. + * @throws IllegalArgumentException if IPv6 format is unacceptable. + */ + public static String canonize(String ipv6Address) + throws IllegalArgumentException { + + if (ipv6Address == null) { + return null; + } + + // Definitely not an IPv6, return untouched input. + if (!mayBeIPv6Address(ipv6Address)) { + return ipv6Address; + } + + // Lenght without zone ID (%zone) or IPv4 address + int ipv6AddressLength = ipv6Address.length(); + if (ipv6Address.contains(":") && ipv6Address.contains(".")) { + // IPv4 in IPv6 + // e.g. 0:0:0:0:0:FFFF:127.0.0.1 + int lastColonPos = ipv6Address.lastIndexOf(":"); + int lastColonsPos = ipv6Address.lastIndexOf("::"); + if (lastColonsPos >= 0 && lastColonPos == lastColonsPos + 1) { + /* + * IPv6 part ends with two consecutive colons, + * last colon is part of IPv6 format. + * e.g. ::127.0.0.1 + */ + ipv6AddressLength = lastColonPos + 1; + } else { + /* + * IPv6 part ends with only one colon, + * last colon is not part of IPv6 format. + * e.g. ::FFFF:127.0.0.1 + */ + ipv6AddressLength = lastColonPos; + } + } else if (ipv6Address.contains(":") && ipv6Address.contains("%")) { + // Zone ID + // e.g. fe80:0:0:0:f0f0:c0c0:1919:1234%4 + ipv6AddressLength = ipv6Address.lastIndexOf("%"); + } + + StringBuilder result = new StringBuilder(); + char [][] groups = new char[MAX_NUMBER_OF_GROUPS][MAX_GROUP_LENGTH]; + int groupCounter = 0; + int charInGroupCounter = 0; + + // Index of the current zeroGroup, -1 means not found. + int zeroGroupIndex = -1; + int zeroGroupLength = 0; + + // maximum length zero group, if there is more then one, then first one + int maxZeroGroupIndex = -1; + int maxZeroGroupLength = 0; + + boolean isZero = true; + boolean groupStart = true; + + /* + * Two consecutive colons, initial expansion. + * e.g. 2001:db8:0:0:1::1 -> 2001:db8:0:0:1:0:0:1 + */ + + StringBuilder expanded = new StringBuilder(ipv6Address); + int colonsPos = ipv6Address.indexOf("::"); + int length = ipv6AddressLength; + int change = 0; + + if (colonsPos >= 0 && colonsPos < ipv6AddressLength - 2) { + int colonCounter = 0; + for (int i = 0; i < ipv6AddressLength; i++) { + if (ipv6Address.charAt(i) == ':') { + colonCounter++; + } + } + + if (colonsPos == 0) { + expanded.insert(0, "0"); + change = change + 1; + } + + for (int i = 0; i < MAX_NUMBER_OF_GROUPS - colonCounter; i++) { + expanded.insert(colonsPos + 1, "0:"); + change = change + 2; + } + + + if (colonsPos == ipv6AddressLength - 2) { + expanded.setCharAt(colonsPos + change + 1, '0'); + } else { + expanded.deleteCharAt(colonsPos + change + 1); + change = change - 1; + } + length = length + change; + } + + + // Processing one char at the time + for (int charCounter = 0; charCounter < length; charCounter++) { + char c = expanded.charAt(charCounter); + if (c >= 'A' && c <= 'F') { + c = (char) (c + 32); + } + if (c != ':') { + groups[groupCounter][charInGroupCounter] = c; + if (!(groupStart && c == '0')) { + ++charInGroupCounter; + groupStart = false; + } + if (c != '0') { + isZero = false; + } + } + if (c == ':' || charCounter == (length - 1)) { + // We reached end of current group + if (isZero) { + ++zeroGroupLength; + if (zeroGroupIndex == -1) { + zeroGroupIndex = groupCounter; + } + } + + if (!isZero || charCounter == (length - 1)) { + // We reached end of zero group + if (zeroGroupLength > maxZeroGroupLength) { + maxZeroGroupLength = zeroGroupLength; + maxZeroGroupIndex = zeroGroupIndex; + } + zeroGroupLength = 0; + zeroGroupIndex = -1; + } + ++groupCounter; + charInGroupCounter = 0; + isZero = true; + groupStart = true; + } + } + + int numberOfGroups = groupCounter; + + // Output results + for (groupCounter = 0; groupCounter < numberOfGroups; groupCounter++) { + if (maxZeroGroupLength <= 1 || groupCounter < maxZeroGroupIndex + || groupCounter >= maxZeroGroupIndex + maxZeroGroupLength) { + for (int j = 0; j < MAX_GROUP_LENGTH; j++) { + if (groups[groupCounter][j] != 0) { + result.append(groups[groupCounter][j]); + } + } + if (groupCounter < (numberOfGroups - 1) + && (groupCounter != maxZeroGroupIndex - 1 + || maxZeroGroupLength <= 1)) { + result.append(':'); + } + } else if (groupCounter == maxZeroGroupIndex) { + result.append("::"); + } + } + + // Solve problem with three colons in IPv4 in IPv6 format + // e.g. 0:0:0:0:0:0:127.0.0.1 -> :::127.0.0.1 -> ::127.0.0.1 + int resultLength = result.length(); + if (result.charAt(resultLength - 1) == ':' && ipv6AddressLength < ipv6Address.length() + && ipv6Address.charAt(ipv6AddressLength) == ':') { + result.delete(resultLength - 1, resultLength); + } + + /* + * Append IPv4 from IPv4-in-IPv6 format or Zone ID + */ + for (int i = ipv6AddressLength; i < ipv6Address.length(); i++) { + result.append(ipv6Address.charAt(i)); + } + + return result.toString(); + } + + /** + * Heuristic check if string might be an IPv6 address. + * + * @param address Any string or null + * @return true, if input string contains only hex digits and at least two colons, before '.' or '%' charachter + */ + protected static boolean mayBeIPv6Address(String input) { + if (input == null) { + return false; + } + + boolean result = false; + int colonsCounter = 0; + int length = input.length(); + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (c == '.' || c == '%') { + // IPv4 in IPv6 or Zone ID detected, end of checking. + break; + } + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') || c == ':')) { + return false; + } else if (c == ':') { + colonsCounter++; + } + } + if (colonsCounter >= 2) { + result = true; + } + return result; + } + +} --- test/org/apache/tomcat/util/net/IPv6UtilsTest.java (revision 0) +++ test/org/apache/tomcat/util/net/IPv6UtilsTest.java (working copy) @@ -0,0 +1,147 @@ +/* + * 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.tomcat.util.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + + +/** + * Mostly examples from RFC 5952 + */ +public class IPv6UtilsTest { + + @Test + public void testMayBeIPv6Address() { + assertFalse(IPv6Utils.mayBeIPv6Address(null)); + + assertTrue(IPv6Utils.mayBeIPv6Address("::1")); + assertTrue(IPv6Utils.mayBeIPv6Address("::")); + assertTrue(IPv6Utils.mayBeIPv6Address("2001:db8:0:0:1:0:0:1")); + + assertFalse(IPv6Utils.mayBeIPv6Address("")); + assertFalse(IPv6Utils.mayBeIPv6Address(":1")); + assertFalse(IPv6Utils.mayBeIPv6Address("123.123.123.123")); + assertFalse(IPv6Utils.mayBeIPv6Address("tomcat.eu.apache.org:443")); + } + + @Test + public void testCanonize() { + assertNull(IPv6Utils.canonize(null)); + assertEquals("", IPv6Utils.canonize("")); + + // IPv4-safe + assertEquals("123.123.123.123", IPv6Utils.canonize("123.123.123.123")); + assertEquals("123.1.2.23", IPv6Utils.canonize("123.1.2.23")); + + // Introductory RFC 5952 examples + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:0db8:0:0:1:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8::1:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8::0:1:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:0db8::1:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1::1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0000:0:1::1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:DB8:0:0:1::1")); + + // Strip leading zeros (2.1) + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:0001")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:001")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:01")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + + // Zero compression (2.2) + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:0:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd::1")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:0:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:0:1")); + + assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0:0:0::1")); + assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0:0::1")); + assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0::1")); + assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8::1")); + + assertEquals("2001:db8::aaaa:0:0:1", IPv6Utils.canonize("2001:db8::aaaa:0:0:1")); + assertEquals("2001:db8::aaaa:0:0:1", IPv6Utils.canonize("2001:db8:0:0:aaaa::1")); + + // Uppercase or lowercase (2.3) + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AAAA")); + assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AaAa")); + + // Some more zero compression for localhost addresses + assertEquals("::1", IPv6Utils.canonize("0:0:0:0:0:0:0:1")); + assertEquals("::1", IPv6Utils.canonize("0000:0:0:0:0:0:0:0001")); + assertEquals("::1", IPv6Utils.canonize("00:00:0:0:00:00:0:01")); + assertEquals("::1", IPv6Utils.canonize("::0001")); + assertEquals("::1", IPv6Utils.canonize("::1")); + + // IPv6 unspecified address + assertEquals("::", IPv6Utils.canonize("0:0:0:0:0:0:0:0")); + assertEquals("::", IPv6Utils.canonize("0000:0:0:0:0:0:0:0000")); + assertEquals("::", IPv6Utils.canonize("00:00:0:0:00:00:0:00")); + assertEquals("::", IPv6Utils.canonize("::0000")); + assertEquals("::", IPv6Utils.canonize("::0")); + assertEquals("::", IPv6Utils.canonize("::")); + + // Leading zeros (4.1) + assertEquals("2001:db8::1", IPv6Utils.canonize("2001:0db8::0001")); + + // Shorten as much as possible (4.2.1) + assertEquals("2001:db8::2:1", IPv6Utils.canonize("2001:db8:0:0:0:0:2:1")); + assertEquals("2001:db8::", IPv6Utils.canonize("2001:db8:0:0:0:0:0:0")); + + // Handling One 16-Bit 0 Field (4.2.2) + assertEquals("2001:db8:0:1:1:1:1:1", IPv6Utils.canonize("2001:db8:0:1:1:1:1:1")); + assertEquals("2001:db8:0:1:1:1:1:1", IPv6Utils.canonize("2001:db8::1:1:1:1:1")); + + // Choice in Placement of "::" (4.2.3) + assertEquals("2001:0:0:1::1", IPv6Utils.canonize("2001:0:0:1:0:0:0:1")); + assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1:0:0:1")); + + // IPv4 inside IPv6 + assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("::ffff:192.0.2.1")); + assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("0:0:0:0:0:ffff:192.0.2.1")); + assertEquals("::192.0.2.1", IPv6Utils.canonize("::192.0.2.1")); + assertEquals("::192.0.2.1", IPv6Utils.canonize("0:0:0:0:0:0:192.0.2.1")); + + // Zone ID + assertEquals("fe80::f0f0:c0c0:1919:1234%4", IPv6Utils.canonize("fe80::f0f0:c0c0:1919:1234%4")); + assertEquals("fe80::f0f0:c0c0:1919:1234%4", IPv6Utils.canonize("fe80:0:0:0:f0f0:c0c0:1919:1234%4")); + + assertEquals("::%4", IPv6Utils.canonize("::%4")); + assertEquals("::%4", IPv6Utils.canonize("::0%4")); + assertEquals("::%4", IPv6Utils.canonize("0:0::0%4")); + assertEquals("::%4", IPv6Utils.canonize("0:0:0:0:0:0:0:0%4")); + + assertEquals("::1%4", IPv6Utils.canonize("::1%4")); + assertEquals("::1%4", IPv6Utils.canonize("0:0::1%4")); + assertEquals("::1%4", IPv6Utils.canonize("0:0:0:0:0:0:0:1%4")); + + assertEquals("::1%eth0", IPv6Utils.canonize("::1%eth0")); + assertEquals("::1%eth0", IPv6Utils.canonize("0:0::1%eth0")); + assertEquals("::1%eth0", IPv6Utils.canonize("0:0:0:0:0:0:0:1%eth0")); + + // Hostname safety + assertEquals("www.apache.org", IPv6Utils.canonize("www.apache.org")); + assertEquals("ipv6.google.com", IPv6Utils.canonize("ipv6.google.com")); + + } +} --- webapps/docs/config/valve.xml (revision 1566763) +++ webapps/docs/config/valve.xml (working copy) @@ -240,6 +240,16 @@

+ +

Flag to determine if IPv6 addresses should be represented in canonical + representation format as defined by RFC 5952. If set to true, + then IPv6 addresses will be written in canonical format (e.g. + 2001:db8::1:0:0:1, ::1), otherwise it will be + represented in full form (e.g. 2001:db8:0:0:1:0:0:1, + 0:0:0:0:0:0:0:1). Default value: true +

+
+

Log message buffers are usually recycled and re-used. To prevent excessive memory usage, if a buffer grows beyond this size it will be