Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support zone ID in IPv6 addresses. Issue #37107 #37547

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,72 @@
public class InetAddresses {
private static int IPV4_PART_COUNT = 4;
private static int IPV6_PART_COUNT = 8;
private static final int DEFAULT_ZONE_ID = 0;

public static boolean isInetAddress(String ipString) {
return ipStringToBytes(ipString) != null;
}

private static int ipStringToZoneId(String ipString){
int percentIndex = ipString.lastIndexOf('%');

if (percentIndex >= 0 && (percentIndex + 1) < ipString.length()){
String zoneId = ipString.substring(percentIndex + 1);
return Integer.parseInt(zoneId, 16);
}

return DEFAULT_ZONE_ID;
}

private static byte[] ipStringToBytes(String ipString) {
// Make a first pass to categorize the characters in this string.
boolean hasColon = false;
boolean hasDot = false;
boolean hasPercent = false;
int percentIndex = -1;
for (int i = 0; i < ipString.length(); i++) {
char c = ipString.charAt(i);
if (c == '.') {
if (hasPercent){
return null; // Dots must not appear after percents.
}

hasDot = true;
} else if (c == ':') {
if (hasPercent){
return null; // Colons must not appear after percents.
}

if (hasDot) {
return null; // Colons must not appear after dots.
}
hasColon = true;
} else if (c == '%'){
if (hasPercent){
return null; // There can only be one percent.
}

if (!hasColon){
return null; // Percents can only appear if there are colons.
}

hasPercent = true;
percentIndex = i;
} else if (Character.digit(c, 16) == -1) {
return null; // Everything else must be a decimal or hex digit.
}
}

// strip zoneId from the address
if (hasPercent){
String ipStringWithoutZoneId = ipString.substring(0, percentIndex);
return ipStringToBytes(ipStringWithoutZoneId);
}

return ipStringToBytes(ipString, hasColon, hasDot);
}

private static byte[] ipStringToBytes(String ipString, boolean hasColon, boolean hasDot) {
// Now decide which address family to parse.
if (hasColon) {
if (hasDot) {
Expand Down Expand Up @@ -243,12 +286,13 @@ public static String toAddrString(InetAddress ip) {
throw new IllegalArgumentException("ip");
}
byte[] bytes = ip.getAddress();
int zoneId = ((Inet6Address)ip).getScopeId();
int[] hextets = new int[IPV6_PART_COUNT];
for (int i = 0; i < hextets.length; i++) {
hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255;
}
compressLongestRunOfZeroes(hextets);
return hextetsToIPv6String(hextets);
return hextetsToIPv6String(hextets, zoneId);
}

/**
Expand Down Expand Up @@ -290,8 +334,9 @@ private static void compressLongestRunOfZeroes(int[] hextets) {
* sentinel values in place of the elided zeroes.
*
* @param hextets {@code int[]} array of eight 16-bit hextets, or -1s
* @param zoneId {@code int} the zone ID of an IPv6 address. Ignored from output if zero.
*/
private static String hextetsToIPv6String(int[] hextets) {
private static String hextetsToIPv6String(int[] hextets, int zoneId) {
/*
* While scanning the array, handle these state transitions:
* start->num => "num" start->gap => "::"
Expand All @@ -314,6 +359,12 @@ private static String hextetsToIPv6String(int[] hextets) {
}
lastWasNumber = thisIsNumber;
}

if (zoneId != DEFAULT_ZONE_ID){
buf.append('%');
buf.append(Integer.toString(zoneId, 16));
}

return buf.toString();
}

Expand All @@ -335,7 +386,9 @@ public static InetAddress forString(String ipString) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString));
}

return bytesToInetAddress(addr);
int zoneId = ipStringToZoneId(ipString);

return bytesToInetAddress(addr, zoneId);
}

/**
Expand All @@ -347,10 +400,16 @@ public static InetAddress forString(String ipString) {
* is an array of length 4 or 16.
*
* @param addr the raw 4-byte or 16-byte IP address in big-endian order
* @param zoneId the IPv6 zone ID. Zero by default.
* Uses {@code Inet6Address.getByAddress} for non-zero values.
* @return an InetAddress object created from the raw IP address
*/
private static InetAddress bytesToInetAddress(byte[] addr) {
private static InetAddress bytesToInetAddress(byte[] addr, int zoneId) {
try {
if (zoneId != DEFAULT_ZONE_ID){
return Inet6Address.getByAddress(null, addr, zoneId);
}

return InetAddress.getByAddress(addr);
} catch (UnknownHostException e) {
throw new AssertionError(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

Expand Down Expand Up @@ -189,12 +190,18 @@ public void testToAddrStringIPv6() {
assertEquals("1::4:0:0:7:8",
InetAddresses.toAddrString(
InetAddresses.forString("1:0:0:4:0:0:7:8")));
assertEquals("::",
InetAddresses.toAddrString(
InetAddresses.forString("::")));
assertEquals("::",
InetAddresses.toAddrString(
InetAddresses.forString("0:0:0:0:0:0:0:0")));
assertEquals("::1",
InetAddresses.toAddrString(
InetAddresses.forString("0:0:0:0:0:0:0:1")));
assertEquals("::1",
InetAddresses.toAddrString(
InetAddresses.forString("::1")));
assertEquals("2001:658:22a:cafe::",
InetAddresses.toAddrString(
InetAddresses.forString("2001:0658:022a:cafe::")));
Expand All @@ -203,6 +210,67 @@ public void testToAddrStringIPv6() {
InetAddresses.forString("::1.2.3.4")));
}

public void testToAddrStringIPv6WithZoneId(){
assertEquals("::1",
InetAddresses.toAddrString(
InetAddresses.forString("::1%0")));
assertEquals("::1%1",
InetAddresses.toAddrString(
InetAddresses.forString("::1%1")));
assertEquals("::1%ff",
InetAddresses.toAddrString(
InetAddresses.forString("::1%ff")));
assertEquals("::102:304",
InetAddresses.toAddrString(
InetAddresses.forString("::1.2.3.4%0")));
assertEquals("::102:304%b",
InetAddresses.toAddrString(
InetAddresses.forString("::1.2.3.4%b")));
}

public void testToAddrStringIPv6ReadsZoneId(){
assertEquals(1,
((Inet6Address)InetAddresses.forString("::1%1")).getScopeId());
assertEquals(10,
((Inet6Address)InetAddresses.forString("::1%A")).getScopeId());
}

public void testToAddrStringIPv6WithInvalidZoneId(){
IllegalArgumentException e = null;

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%fred"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%%"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("%::1"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("0::0:0:0:0:0:0%:1"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%1.2.3.4"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));
}

public void testToAddrStringZoneIdDelimiterCannotAppearRightAfterOtherDelimiters(){
IllegalArgumentException e = null;

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1:%0"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1:1.2.3.%0"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));
}

public void testToAddrStringIPv4DoesNotAllowZoneId(){
IllegalArgumentException e = null;

e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("1.2.3.4%0"));
assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal"));
}

public void testToUriStringIPv4() {
String ipStr = "1.2.3.4";
InetAddress ip = InetAddresses.forString(ipStr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ public void testFormatPortV6() throws Exception {
}

public void testNoScopeID() throws Exception {
assertEquals("::1", NetworkAddress.format(forgeScoped(null, "::1", 5)));
assertEquals("::1", NetworkAddress.format(forgeScoped("localhost", "::1", 5)));
assertEquals("::1%5", NetworkAddress.format(forgeScoped(null, "::1", 5)));
assertEquals("::1%5", NetworkAddress.format(forgeScoped("localhost", "::1", 5)));

assertEquals("[::1]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped(null, "::1", 5), 1234)));
assertEquals("[::1]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped("localhost", "::1", 5), 1234)));
assertEquals("[::1%5]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped(null, "::1", 5), 1234)));
assertEquals("[::1%5]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped("localhost", "::1", 5), 1234)));
}

/** Test that ipv4 address formatting round trips */
Expand Down