--- java/org/apache/catalina/valves/AccessLogValve.java (revision 1150602) +++ java/org/apache/catalina/valves/AccessLogValve.java (working copy) @@ -53,6 +53,7 @@ import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.net.IPv6Utils; /** @@ -222,6 +223,12 @@ /** + * Use IPv6 canonical representation format as defined by RFC 5952. + */ + protected boolean canonical = true; + + + /** * The PrintWriter to which we are currently logging, if any. */ protected PrintWriter writer = null; @@ -232,8 +239,8 @@ * "yyyy-MM-dd". */ protected SimpleDateFormat fileDateFormatter = null; + - /** * The system timezone. */ @@ -748,6 +755,24 @@ /** + * 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; + } + + + /** * Set the resolve hosts flag. * * @param resolveHosts The new resolve hosts value @@ -1217,11 +1242,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(); @@ -1229,13 +1254,20 @@ 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(StringBuilder buf, Date date, Request request, Response response, long time) { - buf.append(LOCAL_ADDR_VALUE); + if (localAddrValue == null) { + lazyInit(); + } + buf.append(localAddrValue); } } @@ -1246,16 +1278,22 @@ @Override public void addElement(StringBuilder 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); + value = addr.toString(); } } else { - buf.append(request.getRemoteAddr()); + value = request.getRemoteAddr(); } + + if (canonical) { + value = IPv6Utils.canonize(value); + } + buf.append(value); } } @@ -1279,6 +1317,9 @@ if (value == null || value.length() == 0) { value = "-"; } + if (canonical) { + value = IPv6Utils.canonize(value); + } buf.append(value); } } @@ -1691,11 +1732,15 @@ /** * write local server name - %v */ - protected static class LocalServerNameElement implements AccessLogElement { + protected class LocalServerNameElement implements AccessLogElement { @Override public void addElement(StringBuilder 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 (revision 0) @@ -0,0 +1,165 @@ +/* + * 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 java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + *

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). + * Function is null safe, and if IPv4 address or host name is passed to the + * function it is returned wihout any processing. + * + * @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; + } + + // Definitly not an IPv6, return untouched input. + if (!mayBeIPv6Address(ipv6Address)) { + return ipv6Address; + } + + 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; + String expanded = null; + try { + InetAddress inetAddress = InetAddress.getByName(ipv6Address); + if (inetAddress instanceof Inet6Address) { + expanded = ((Inet6Address) inetAddress).getHostAddress(); + } else { + // Probably IPv4 address, leave it as it is + return ipv6Address; + } + } catch (UnknownHostException uhe) { + // Malformed input + throw new IllegalArgumentException(uhe); + } + + int len = expanded.length(); + + // Processing one char at the time + for (int charCounter = 0; charCounter < len; charCounter++) { + char c = expanded.charAt(charCounter); + //System.out.println(c); + if (c != ':') { + groups[groupCounter][charInGroupCounter] = c; + ++charInGroupCounter; + if (c != '0') { + isZero = false; + } + } + if (c == ':' || charCounter == (len - 1)) { + // We reached end of current group + if (isZero && zeroGroupIndex == -1) { + zeroGroupIndex = groupCounter; + zeroGroupLength = 1; + } else if (!isZero) { + // We reached end of zero group + if (zeroGroupLength > maxZeroGroupLength) { + maxZeroGroupLength = zeroGroupLength; + maxZeroGroupIndex = zeroGroupIndex; + } + zeroGroupLength = 0; + zeroGroupIndex = -1; + } else { + ++zeroGroupLength; + } + ++groupCounter; + charInGroupCounter = 0; + isZero = true; + } + } + + // Output results + for (groupCounter = 0; groupCounter < MAX_NUMBER_OF_GROUPS; 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 < (MAX_NUMBER_OF_GROUPS - 1) + && (groupCounter != maxZeroGroupIndex - 1 + || maxZeroGroupLength <= 1)) { + result.append(':'); + } + } else if (groupCounter == maxZeroGroupIndex) { + result.append("::"); + } + } + + return result.toString(); + } + + /** + * Heuristic check if string might be an IPv6 address. + * + * @param address Any string or null + * @return true, if string contains only hex digits and at least two colons. + */ + private static boolean mayBeIPv6Address(String input) { + if (input == null) { + return false; + } + boolean result = true; + int colonsCounter = 0; + int length = input.length(); + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') || c == ':')) { + result = false; + } else if (c == ':') { + colonsCounter++; + } + } + return result; + + } +} --- test/org/apache/tomcat/util/net/IPv6UtilsTest.java (revision 0) +++ test/org/apache/tomcat/util/net/IPv6UtilsTest.java (revision 0) @@ -0,0 +1,126 @@ +/* + * 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 junit.framework.TestCase; + +/** + * Mostly examples from RFC 5952 + */ +public class IPv6UtilsTest extends TestCase { + + public void testCanonize() { + assertTrue(IPv6Utils.canonize(null) == null); + assertTrue(IPv6Utils.canonize("").equals("")); + + // IPv4-safe + assertTrue(IPv6Utils.canonize("123.123.123.123").equals( + "123.123.123.123")); + assertTrue(IPv6Utils.canonize("123.1.2.23").equals("123.1.2.23")); + + // Introductory RFC 5952 examples + assertTrue(IPv6Utils.canonize("2001:db8:0:0:1:0:0:1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:0db8:0:0:1:0:0:1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8::1:0:0:1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8::0:1:0:0:1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:0db8::1:0:0:1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8:0:0:1::1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8:0000:0:1::1").equals( + "2001:db8::1:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:DB8:0:0:1::1").equals( + "2001:db8::1:0:0:1")); + + // Strip leading zeros (2.1) + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:0001") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:001") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:01") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + + // Zero compression (2.2) + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd::1") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:0:1") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:0:1")); + + assertTrue(IPv6Utils.canonize("2001:db8:0:0:0::1") + .equals("2001:db8::1")); + assertTrue(IPv6Utils.canonize("2001:db8:0:0::1").equals("2001:db8::1")); + assertTrue(IPv6Utils.canonize("2001:db8:0::1").equals("2001:db8::1")); + assertTrue(IPv6Utils.canonize("2001:db8::1").equals("2001:db8::1")); + + assertTrue(IPv6Utils.canonize("2001:db8::aaaa:0:0:1").equals( + "2001:db8::aaaa:0:0:1")); + assertTrue(IPv6Utils.canonize("2001:db8:0:0:aaaa::1").equals( + "2001:db8::aaaa:0:0:1")); + + // Uppercase or lowercase (2.3) + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AAAA") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa")); + assertTrue(IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AaAa") + .equals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa")); + + // Some more zero compression for localhost addresses + assertTrue(IPv6Utils.canonize("0:0:0:0:0:0:0:1").equals("::1")); + assertTrue(IPv6Utils.canonize("0000:0:0:0:0:0:0:0001").equals("::1")); + assertTrue(IPv6Utils.canonize("00:00:0:0:00:00:0:01").equals("::1")); + assertTrue(IPv6Utils.canonize("::0001").equals("::1")); + assertTrue(IPv6Utils.canonize("::1").equals("::1")); + + // Leading zeros (4.1) + assertTrue(IPv6Utils.canonize("2001:0db8::0001").equals("2001:db8::1")); + + // Shorten as much as possible (4.2.1) + assertTrue(IPv6Utils.canonize("2001:db8:0:0:0:0:2:1").equals( + "2001:db8::2:1")); + + // Handling One 16-Bit 0 Field (4.2.2) + assertTrue(IPv6Utils.canonize("2001:db8:0:1:1:1:1:1").equals( + "2001:db8:0:1:1:1:1:1")); + assertTrue(IPv6Utils.canonize("2001:db8::1:1:1:1:1").equals( + "2001:db8:0:1:1:1:1:1")); + + // Choice in Placement of "::" (4.2.3) + assertTrue(IPv6Utils.canonize("2001:0:0:1:0:0:0:1").equals( + "2001:0:0:1::1")); + assertTrue(IPv6Utils.canonize("2001:db8:0:0:1:0:0:1").equals( + "2001:db8::1:0:0:1")); + + // IPv4 inside IPv6 + assertTrue(IPv6Utils.canonize("::ffff:192.0.2.1").equals( + "::ffff:192.0.2.1")); + + // Hostname safety + assertTrue(IPv6Utils.canonize("www.apache.org").equals( + "www.apache.org")); + assertTrue(IPv6Utils.canonize("ipv6.google.com").equals( + "ipv6.google.com")); + + } +} --- webapps/docs/config/valve.xml (revision 1150602) +++ webapps/docs/config/valve.xml (working copy) @@ -193,6 +193,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 +

+
+

Values for the pattern attribute are made up of literal