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 }