View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.util;
17  
18  import java.net.Inet6Address;
19  import java.net.InetAddress;
20  
21  /**
22   * Utility class for handling IP addresses, particularly IPv6 addresses in URLs.
23   * This class provides methods to properly format IPv6 addresses for use in URLs
24   * by adding brackets where necessary.
25   */
26  public final class IpAddressUtil {
27  
28      /**
29       * Private constructor to prevent instantiation of utility class.
30       */
31      private IpAddressUtil() {
32          // Utility class - no instances allowed
33      }
34  
35      /**
36       * Determines if the given address string represents an IPv6 address.
37       * This method validates the address using InetAddress to ensure it's a valid IPv6 address.
38       *
39       * @param address the IP address string to check
40       * @return true if the address is a valid IPv6 address, false otherwise
41       */
42      public static boolean isIPv6Address(final String address) {
43          if (address == null) {
44              return false;
45          }
46          try {
47              final InetAddress inetAddress = InetAddress.getByName(address);
48              return inetAddress instanceof Inet6Address;
49          } catch (final Exception e) {
50              return false;
51          }
52      }
53  
54      /**
55       * Formats an IP address string for use in a URL.
56       * IPv6 addresses are wrapped in brackets, IPv4 addresses are returned as-is.
57       *
58       * @param address the IP address string to format
59       * @return the formatted address (IPv6 with brackets, IPv4 unchanged)
60       */
61      public static String formatForUrl(final String address) {
62          if (address == null) {
63              return null;
64          }
65          if (isIPv6Address(address)) {
66              // If already has brackets, return as-is
67              if (address.startsWith("[") && address.endsWith("]")) {
68                  return address;
69              }
70              // Add brackets for IPv6
71              return "[" + address + "]";
72          }
73          return address;
74      }
75  
76      /**
77       * Compresses an IPv6 address string to its canonical compressed form.
78       * For example, "0:0:0:0:0:0:0:1" becomes "::1"
79       *
80       * @param ipv6Address the IPv6 address string to compress
81       * @return the compressed IPv6 address string
82       */
83      protected static String compressIPv6(final String ipv6Address) {
84          if (ipv6Address == null || ipv6Address.isEmpty()) {
85              return ipv6Address;
86          }
87  
88          // Expand :: if present to get full address for normalization
89          final String expandedAddress;
90          if (ipv6Address.contains("::")) {
91              final String[] parts = ipv6Address.split("::");
92              final String leftPart = parts.length > 0 && !parts[0].isEmpty() ? parts[0] : "";
93              final String rightPart = parts.length > 1 && !parts[1].isEmpty() ? parts[1] : "";
94              final int leftCount = leftPart.isEmpty() ? 0 : leftPart.split(":").length;
95              final int rightCount = rightPart.isEmpty() ? 0 : rightPart.split(":").length;
96              final int zerosCount = 8 - leftCount - rightCount;
97              final StringBuilder expanded = new StringBuilder(leftPart);
98              for (int i = 0; i < zerosCount; i++) {
99                  if (expanded.length() > 0) {
100                     expanded.append(":");
101                 }
102                 expanded.append("0");
103             }
104             if (!rightPart.isEmpty()) {
105                 if (expanded.length() > 0) {
106                     expanded.append(":");
107                 }
108                 expanded.append(rightPart);
109             }
110             expandedAddress = expanded.toString();
111         } else {
112             expandedAddress = ipv6Address;
113         }
114 
115         // Split address into parts
116         final String[] parts = expandedAddress.split(":");
117         if (parts.length != 8) {
118             // Not a standard IPv6 address, return as-is
119             return ipv6Address;
120         }
121 
122         // Normalize each part (remove leading zeros)
123         final String[] normalized = new String[8];
124         for (int i = 0; i < 8; i++) {
125             normalized[i] = parts[i].replaceFirst("^0+(?!$)", "");
126             if (normalized[i].isEmpty()) {
127                 normalized[i] = "0";
128             }
129         }
130 
131         // Find longest sequence of consecutive zeros
132         int longestStart = -1;
133         int longestLength = 0;
134         int currentStart = -1;
135         int currentLength = 0;
136 
137         for (int i = 0; i < 8; i++) {
138             if ("0".equals(normalized[i])) {
139                 if (currentStart == -1) {
140                     currentStart = i;
141                 }
142                 currentLength++;
143 
144                 if (currentLength > longestLength) {
145                     longestStart = currentStart;
146                     longestLength = currentLength;
147                 }
148             } else {
149                 currentStart = -1;
150                 currentLength = 0;
151             }
152         }
153 
154         // If longest sequence is only 1 zero, don't compress
155         if (longestLength <= 1) {
156             return String.join(":", normalized);
157         }
158 
159         // Build compressed address
160         final StringBuilder result = new StringBuilder();
161         boolean inCompression = false;
162 
163         for (int i = 0; i < 8; i++) {
164             if (i >= longestStart && i < longestStart + longestLength) {
165                 // We're in the compression zone
166                 if (!inCompression) {
167                     // Start of compression - add ::
168                     result.append("::");
169                     inCompression = true;
170                 }
171                 // Skip this zero
172             } else {
173                 // Normal part
174                 inCompression = false;
175                 if (result.length() > 0 && result.charAt(result.length() - 1) != ':') {
176                     result.append(":");
177                 }
178                 result.append(normalized[i]);
179             }
180         }
181 
182         return result.toString();
183     }
184 
185     /**
186      * Generates a URL-safe host string from an InetAddress.
187      * For IPv6 addresses, this wraps the address in brackets and compresses it.
188      * For IPv4 addresses, returns the address as-is.
189      *
190      * @param address the InetAddress to format
191      * @return the URL-safe host string
192      */
193     public static String getUrlHost(final InetAddress address) {
194         if (address == null) {
195             return null;
196         }
197         if (address instanceof Inet6Address) {
198             final String hostAddress = address.getHostAddress();
199             // Remove zone ID if present (e.g., %eth0)
200             final int percentIndex = hostAddress.indexOf('%');
201             final String cleanAddress = percentIndex >= 0 ? hostAddress.substring(0, percentIndex) : hostAddress;
202             // Compress the IPv6 address
203             final String compressed = compressIPv6(cleanAddress);
204             return "[" + compressed + "]";
205         }
206         return address.getHostAddress();
207     }
208 
209     /**
210      * Builds a URL from protocol, InetAddress, port, and path.
211      * Properly handles IPv6 addresses by wrapping them in brackets.
212      *
213      * @param protocol the protocol (e.g., "http", "https")
214      * @param address the InetAddress for the host
215      * @param port the port number
216      * @param path the path (should start with "/" or be empty)
217      * @return the complete URL string
218      */
219     public static String buildUrl(final String protocol, final InetAddress address, final int port, final String path) {
220         if (protocol == null || address == null) {
221             return null;
222         }
223         final String host = getUrlHost(address);
224         final StringBuilder url = new StringBuilder();
225         url.append(protocol).append("://").append(host);
226         if (port > 0) {
227             url.append(":").append(port);
228         }
229         if (path != null && !path.isEmpty()) {
230             if (!path.startsWith("/")) {
231                 url.append("/");
232             }
233             url.append(path);
234         }
235         return url.toString();
236     }
237 
238     /**
239      * Builds a URL from protocol, hostname string, port, and path.
240      * Properly handles IPv6 addresses by wrapping them in brackets if needed.
241      *
242      * @param protocol the protocol (e.g., "http", "https")
243      * @param host the hostname or IP address string
244      * @param port the port number
245      * @param path the path (should start with "/" or be empty)
246      * @return the complete URL string
247      */
248     public static String buildUrl(final String protocol, final String host, final int port, final String path) {
249         if (protocol == null || host == null) {
250             return null;
251         }
252         final String formattedHost = formatForUrl(host);
253         final StringBuilder url = new StringBuilder();
254         url.append(protocol).append("://").append(formattedHost);
255         if (port > 0) {
256             url.append(":").append(port);
257         }
258         if (path != null && !path.isEmpty()) {
259             if (!path.startsWith("/")) {
260                 url.append("/");
261             }
262             url.append(path);
263         }
264         return url.toString();
265     }
266 }