View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix;
19  
20  import java.io.UnsupportedEncodingException;
21  import java.net.SocketException;
22  import java.net.URI;
23  import java.net.URLDecoder;
24  import java.net.URLEncoder;
25  import java.util.Objects;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
30  
31  /**
32   * Hostname and port.
33   *
34   * @author Christian Kohlschütter
35   */
36  @SuppressFBWarnings("REDOS")
37  public final class HostAndPort {
38    private static final Pattern PAT_HOST_AND_PORT = Pattern.compile(
39        "^//((?<userinfo>[^/\\@]*)\\@)?(?<host>[^/\\:]+)(?:\\:(?<port>[0-9]+))?");
40    private final String hostname;
41    private final int port;
42  
43    /**
44     * Creates a new hostname and port combination.
45     *
46     * @param hostname The hostname.
47     * @param port The port, or {@code -1} for "no port".
48     */
49    public HostAndPort(String hostname, int port) {
50      this.hostname = hostname;
51      this.port = port;
52    }
53  
54    @Override
55    public int hashCode() {
56      final int prime = 31;
57      int result = 1;
58      result = prime * result + ((getHostname() == null) ? 0 : getHostname().hashCode());
59      result = prime * result + getPort();
60      return result;
61    }
62  
63    @Override
64    public boolean equals(Object obj) {
65      if (this == obj) {
66        return true;
67      }
68      if (!(obj instanceof HostAndPort)) {
69        return false;
70      }
71      HostAndPort other = (HostAndPort) obj;
72      if (getHostname() == null) {
73        if (other.getHostname() != null) {
74          return false;
75        }
76      } else if (!getHostname().equals(other.getHostname())) {
77        return false;
78      }
79  
80      return getPort() == other.getPort();
81    }
82  
83    @Override
84    public String toString() {
85      if (getPort() == -1) {
86        return getHostname();
87      } else {
88        return getHostname() + ":" + getPort();
89      }
90    }
91  
92    /**
93     * Tries to extract hostname and port information from the given URI.
94     *
95     * @param u The URI to extract from.
96     * @return The parsed {@link HostAndPort} instance.
97     * @throws SocketException on error.
98     */
99    public static HostAndPort parseFrom(URI u) throws SocketException {
100     String host = u.getHost();
101     if (host != null) {
102       return new HostAndPort(host, u.getPort());
103     }
104     String raw = u.getRawSchemeSpecificPart();
105     Matcher m = PAT_HOST_AND_PORT.matcher(raw);
106     if (!m.find()) {
107       throw new SocketException("Cannot parse URI: " + u);
108     }
109     host = urlDecode(m.group("host"));
110 
111     String portStr = m.group("port");
112     int port;
113     if (portStr == null) {
114       port = -1;
115     } else {
116       port = Integer.parseInt(portStr);
117     }
118 
119     return new HostAndPort(host, port);
120   }
121 
122   @SuppressWarnings("PMD.UseStandardCharsets")
123   private static String urlDecode(String s) {
124     try {
125       return URLDecoder.decode(s, "UTF-8");
126     } catch (UnsupportedEncodingException e) {
127       throw new IllegalStateException(e);
128     }
129   }
130 
131   @SuppressWarnings("PMD.UseStandardCharsets")
132   private static String urlEncode(String s) {
133     try {
134       return URLEncoder.encode(s, "UTF-8");
135     } catch (UnsupportedEncodingException e) {
136       throw new IllegalStateException(e);
137     }
138   }
139 
140   /**
141    * Returns the hostname.
142    *
143    * @return The hostname.
144    */
145   public String getHostname() {
146     return hostname;
147   }
148 
149   /**
150    * Returns the port, or {@code -1} for "no port specified".
151    *
152    * @return The port.
153    */
154   public int getPort() {
155     return port;
156   }
157 
158   /**
159    * Returns a URI with this hostname and port.
160    *
161    * @param scheme The scheme to use.
162    * @return The URI.
163    */
164   public URI toURI(String scheme) {
165     return toURI(scheme, null, null, null, null);
166   }
167 
168   /**
169    * Returns a URI with this hostname and port, potentially reusing other URI parameters from the
170    * given template URI (authority, path, query, fragment).
171    *
172    * @param scheme The scheme to use.
173    * @param template The template. or {@code null}.
174    * @return The URI.
175    */
176   public URI toURI(String scheme, URI template) {
177     if (template == null) {
178       return toURI(scheme, null, null, null, null);
179     }
180 
181     String rawAuthority = template.getRawAuthority();
182     int at = rawAuthority.indexOf('@');
183     if (at >= 0) {
184       rawAuthority = rawAuthority.substring(0, at);
185     } else if (rawAuthority.length() > 0 && template.getHost() == null) {
186       // encoded hostname was parsed as authority
187       rawAuthority = null;
188     } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost())) {
189       // hostname was duplicated as authority
190       rawAuthority = null;
191     } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost() + ":"
192         + template.getPort())) {
193       // hostname:port was duplicated as authority
194       rawAuthority = null;
195     }
196 
197     return toURI(scheme, rawAuthority, template.getRawPath(), template.getRawQuery(), template
198         .getRawFragment());
199   }
200 
201   /**
202    * Returns a URI with this hostname and port, potentially using other URI parameters from the
203    * given set of parameters.
204    *
205    * @param scheme The scheme to use.
206    * @param rawAuthority The raw authority field, or {@code null}.
207    * @param rawPath The raw path field, or {@code null}.
208    * @param rawQuery The raw query field, or {@code null}.
209    * @param rawFragment The raw fragment field, or {@code null}.
210    * @return The URI.
211    */
212   public URI toURI(String scheme, String rawAuthority, String rawPath, String rawQuery,
213       String rawFragment) {
214     Objects.requireNonNull(scheme);
215     if (rawPath != null && !rawPath.isEmpty()) {
216       if (!rawPath.startsWith("/")) {
217         throw new IllegalArgumentException("Path must be absolute: " + rawPath);
218       }
219     }
220 
221     return URI.create(scheme + "://" + (rawAuthority == null ? "" : rawAuthority + "@") + urlEncode(
222         getHostname()).replace("%2C", ",") + (port <= 0 ? "" : (":" + port)) + (rawPath == null ? ""
223             : rawPath) + (rawQuery == null ? "" : "?" + rawQuery) + (rawFragment == null ? "" : "#"
224                 + rawFragment));
225   }
226 }