View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2024 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  /**
30   * Hostname and port.
31   *
32   * @author Christian Kohlschütter
33   */
34  public final class HostAndPort {
35    private static final Pattern PAT_HOST_AND_PORT = Pattern.compile(
36        "^//((?<userinfo>[^/\\@]*)\\@)?(?<host>[^/\\:]+)(?:\\:(?<port>[0-9]+))?");
37    private final String hostname;
38    private final int port;
39  
40    /**
41     * Creates a new hostname and port combination.
42     *
43     * @param hostname The hostname.
44     * @param port The port, or {@code -1} for "no port".
45     */
46    public HostAndPort(String hostname, int port) {
47      this.hostname = hostname;
48      this.port = port;
49    }
50  
51    @Override
52    public int hashCode() {
53      final int prime = 31;
54      int result = 1;
55      result = prime * result + ((getHostname() == null) ? 0 : getHostname().hashCode());
56      result = prime * result + getPort();
57      return result;
58    }
59  
60    @Override
61    public boolean equals(Object obj) {
62      if (this == obj) {
63        return true;
64      }
65      if (!(obj instanceof HostAndPort)) {
66        return false;
67      }
68      HostAndPort other = (HostAndPort) obj;
69      if (getHostname() == null) {
70        if (other.getHostname() != null) {
71          return false;
72        }
73      } else if (!getHostname().equals(other.getHostname())) {
74        return false;
75      }
76  
77      return getPort() == other.getPort();
78    }
79  
80    @Override
81    public String toString() {
82      if (getPort() == -1) {
83        return getHostname();
84      } else {
85        return getHostname() + ":" + getPort();
86      }
87    }
88  
89    /**
90     * Tries to extract hostname and port information from the given URI.
91     *
92     * @param u The URI to extract from.
93     * @return The parsed {@link HostAndPort} instance.
94     * @throws SocketException on error.
95     */
96    public static HostAndPort parseFrom(URI u) throws SocketException {
97      String host = u.getHost();
98      if (host != null) {
99        return new HostAndPort(host, u.getPort());
100     }
101     String raw = u.getRawSchemeSpecificPart();
102     Matcher m = PAT_HOST_AND_PORT.matcher(raw);
103     if (!m.find()) {
104       throw new SocketException("Cannot parse URI: " + u);
105     }
106     try {
107       host = URLDecoder.decode(m.group("host"), "UTF-8");
108     } catch (UnsupportedEncodingException e) {
109       throw new IllegalStateException(e);
110     }
111 
112     String portStr = m.group("port");
113     int port;
114     if (portStr == null) {
115       port = -1;
116     } else {
117       port = Integer.parseInt(portStr);
118     }
119 
120     return new HostAndPort(host, port);
121   }
122 
123   private static String urlEncode(String s) {
124     try {
125       return URLEncoder.encode(s, "UTF-8");
126     } catch (UnsupportedEncodingException e) {
127       throw new IllegalStateException(e);
128     }
129   }
130 
131   /**
132    * Returns the hostname.
133    *
134    * @return The hostname.
135    */
136   public String getHostname() {
137     return hostname;
138   }
139 
140   /**
141    * Returns the port, or {@code -1} for "no port specified".
142    *
143    * @return The port.
144    */
145   public int getPort() {
146     return port;
147   }
148 
149   /**
150    * Returns a URI with this hostname and port.
151    *
152    * @param scheme The scheme to use.
153    * @return The URI.
154    */
155   public URI toURI(String scheme) {
156     return toURI(scheme, null, null, null, null);
157   }
158 
159   /**
160    * Returns a URI with this hostname and port, potentially reusing other URI parameters from the
161    * given template URI (authority, path, query, fragment).
162    *
163    * @param scheme The scheme to use.
164    * @param template The template. or {@code null}.
165    * @return The URI.
166    */
167   public URI toURI(String scheme, URI template) {
168     if (template == null) {
169       return toURI(scheme, null, null, null, null);
170     }
171 
172     String rawAuthority = template.getRawAuthority();
173     int at = rawAuthority.indexOf('@');
174     if (at >= 0) {
175       rawAuthority = rawAuthority.substring(0, at);
176     } else if (rawAuthority.length() > 0 && template.getHost() == null) {
177       // encoded hostname was parsed as authority
178       rawAuthority = null;
179     } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost())) {
180       // hostname was duplicated as authority
181       rawAuthority = null;
182     } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost() + ":"
183         + template.getPort())) {
184       // hostname:port was duplicated as authority
185       rawAuthority = null;
186     }
187 
188     return toURI(scheme, rawAuthority, template.getRawPath(), template.getRawQuery(), template
189         .getRawFragment());
190   }
191 
192   /**
193    * Returns a URI with this hostname and port, potentially using other URI parameters from the
194    * given set of parameters.
195    *
196    * @param scheme The scheme to use.
197    * @param rawAuthority The raw authority field, or {@code null}.
198    * @param rawPath The raw path field, or {@code null}.
199    * @param rawQuery The raw query field, or {@code null}.
200    * @param rawFragment The raw fragment field, or {@code null}.
201    * @return The URI.
202    */
203   public URI toURI(String scheme, String rawAuthority, String rawPath, String rawQuery,
204       String rawFragment) {
205     Objects.requireNonNull(scheme);
206     if (rawPath != null && !rawPath.isEmpty()) {
207       if (!rawPath.startsWith("/")) {
208         throw new IllegalArgumentException("Path must be absolute: " + rawPath);
209       }
210     }
211 
212     return URI.create(scheme + "://" + (rawAuthority == null ? "" : rawAuthority + "@") + urlEncode(
213         getHostname()).replace("%2C", ",") + (port <= 0 ? "" : (":" + port)) + (rawPath == null ? ""
214             : rawPath) + (rawQuery == null ? "" : "?" + rawQuery) + (rawFragment == null ? "" : "#"
215                 + rawFragment));
216   }
217 }