Skip to content

fix: support IPv6 addresses in the local Appium service URL - #2495

Open
RaphaelFakhri wants to merge 2 commits into
appium:masterfrom
RaphaelFakhri:fix/local-service-ipv6-address
Open

RaphaelFakhri wants to merge 2 commits into
appium:masterfrom
RaphaelFakhri:fix/local-service-ipv6-address

Conversation

@RaphaelFakhri

Copy link
Copy Markdown

Change list

  • AppiumDriverLocalService encloses an IPv6 literal (for example :: or ::1) in square brackets when it builds the server URL. Before this change, build() failed with MalformedURLException.
  • The broadcast address check compares the host without brackets, so a service bound to BROADCAST_IP6_ADDRESS (::) is pinged on [::1] as intended.
  • Unit tests in AppiumServiceBuilderTest cover ::1 and BROADCAST_IP6_ADDRESS.

Types of changes

  • No changes in production code.
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Details

AppiumServiceBuilder exposes BROADCAST_IP6_ADDRESS, but passing it, or any other IPv6 address, to withIPAddress breaks build():

new AppiumServiceBuilder()
        .withIPAddress(AppiumServiceBuilder.BROADCAST_IP6_ADDRESS)
        .build();
// java.net.MalformedURLException: Error at index 0 in: "::4723"

The service formats the URL as http://%s:%d/, which produces http://:::4723/. With brackets the URL becomes http://[::]:4723/. The --address value sent to the server stays unbracketed, because the server expects a plain address.

fixBroadcastAddresses compared URL.getHost() with ::, but getHost() returns [::] for IPv6 hosts, so the IPv6 branch could never match. It now strips the brackets before the comparison.

@mykola-mokhnach mykola-mokhnach left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • AppiumDriverLocalService.java:129: replaceHost does a regex replaceFirst on the whole URL string. The new IPv6 path only works because "http://[::]:4777/".replaceFirst("::", "::1") happens to hit the host. oldHost isn't quoted, so the dots in 0.0.0.0 are regex wildcards, and newHost isn't passed through Matcher.quoteReplacement. Would building the URL from getProtocol()/getHost()/getPort() be safer?

this.startupTimeout = startupTimeout;
this.url = new URL(String.format(URL_MASK, ipAddress, nodeJSPort));
// IPv6 literals must be enclosed in square brackets to be a valid URL host
String host = ipAddress.contains(":") && !ipAddress.startsWith("[") ? "[" + ipAddress + "]" : ipAddress;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bracket-wrapping check (contains(":") and not starting with [) misses some IPv6 forms:

  • A zone id like fe80::1%en0 becomes [fe80::1%en0], but % must be encoded as %25 in a URL, so the host is mis-parsed or the connection fails.
  • If a user passes a bracketed [::1] to withIPAddress, the URL is correct, but createArgs passes the literal [::1] to Appium's --address, which Node rejects. The two code paths disagree. Should the address be normalized once and the unbracketed form used for --address?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bracketed input: done in 83acd96. withIPAddress now strips the brackets once, so --address always gets the bare address ([::1] becomes ::1), and toUrlHost adds them back for the URL. buildsServiceWithBracketedIpv6Address covers both paths.

Zone ID: I kept the % unencoded. RFC 6874 does use %25, but java.net.URL and InetAddress treat everything after % as the scope. With an HTTP server bound to a link-local address on interface enp0s6, HttpURLConnection returns 200 for http://[fe80::17ff:fe01:d3f8%enp0s6]:<port>/status. With %25enp0s6 it fails with UnknownHostException, because the scope is read as 25enp0s6. buildsServiceWithIpv6AddressWithZoneId pins the unencoded form for both --address and the URL.


private URL fixBroadcastAddresses(URL url) {
var host = url.getHost();
// IPv6 hosts are returned in square brackets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the exact string :: is treated as the IPv6 any-address. 0:0:0:0:0:0:0:0 and ::0 are equivalent wildcard binds but won't be rewritten to loopback. ping then connects to the unspecified address, which some OSes block, so start() times out even though the server is up.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 83acd96. fixBroadcastAddresses now asks InetAddress.getByName(host).isAnyLocalAddress() for any host that contains :. That parses the IPv6 literal without a name lookup, so ::, ::0 and 0:0:0:0:0:0:0:0 are all rewritten to ::1.


private URL fixBroadcastAddresses(URL url) {
var host = url.getHost();
// IPv6 hosts are returned in square brackets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "^\\[(.*)]$" is recompiled on every ping. host.startsWith("[") && host.endsWith("]") with substring does the same job, or use a precompiled Pattern. It could also sit in one helper next to the bracket-wrapping at line 87.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 83acd96. The regex is gone. stripBrackets uses startsWith/endsWith and substring, and it sits next to toUrlHost, which the constructor now uses for the bracket wrapping. replaceHost also rebuilds the URL from getProtocol(), the new host, getPort() and getFile() instead of using replaceFirst on the whole URL string.

assertFalse(service.isRunning());
}

@Test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests only check URL formatting. Nothing exercises fixBroadcastAddresses or the bracket-stripping in the ping path. If [::] stops being rewritten to ::1, no test fails. An already-bracketed [::1] input and a zone-id input are also untested.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 83acd96. fixBroadcastAddresses is now package-private and tested directly: ::, ::0 and 0:0:0:0:0:0:0:0 (with a base path) map to http://[::1]:4777/wd/hub/, 0.0.0.0 maps to 127.0.0.1, and ::1, 127.0.0.1 and localhost stay unchanged. The bracketed [::1] and zone ID inputs have their own tests. With the previous behavior, the bracketed input test and the ::0 and 0:0:0:0:0:0:0:0 cases fail.

Strip square brackets once in withIPAddress, so --address always gets
the bare address. Treat any form of the unspecified IPv6 address as a
wildcard bind when pinging, and rebuild the ping URL from its parts
instead of a regex replacement.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants