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.File;
21  import java.io.IOException;
22  import java.net.Socket;
23  import java.net.SocketException;
24  import java.net.URLDecoder;
25  import java.util.Objects;
26  
27  import javax.net.SocketFactory;
28  
29  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
30  
31  /**
32   * The base for a SocketFactory that connects to UNIX sockets.
33   *
34   * Typically, the "hostname" is used as a reference to a socketFile on the file system. The actual
35   * mapping is left to the implementor.
36   *
37   * @see AFUNIXSocketFactory.FactoryArg
38   * @see AFUNIXSocketFactory.SystemProperty
39   * @see AFUNIXSocketFactory.URIScheme
40   */
41  public abstract class AFUNIXSocketFactory extends AFSocketFactory<AFUNIXSocketAddress> {
42    /**
43     * Creates a {@link AFUNIXSocketFactory}.
44     */
45    protected AFUNIXSocketFactory() {
46      super(AFUNIXSocketAddress.class);
47    }
48  
49    @Override
50    public Socket createSocket() throws SocketException {
51      return AFUNIXSocket.newInstance(this);
52    }
53  
54    @Override
55    protected AFUNIXSocket connectTo(AFUNIXSocketAddress addr) throws IOException {
56      return AFUNIXSocket.connectTo(addr);
57    }
58  
59    /**
60     * A socket factory that handles a custom hostname ("localhost", by default, and configured by the
61     * system property &quot;org.newsclub.net.unix.socket.hostname&quot;), forwarding all other
62     * requests to the fallback {@link SocketFactory}.
63     */
64    private abstract static class DefaultSocketHostnameSocketFactory extends AFUNIXSocketFactory {
65      private static final String PROP_SOCKET_HOSTNAME = "org.newsclub.net.unix.socket.hostname";
66  
67      /**
68       * Creates a {@link DefaultSocketHostnameSocketFactory}.
69       */
70      public DefaultSocketHostnameSocketFactory() {
71        super();
72      }
73  
74      @Override
75      public final boolean isHostnameSupported(String host) {
76        return getDefaultSocketHostname().equals(host);
77      }
78  
79      private static String getDefaultSocketHostname() {
80        return System.getProperty(PROP_SOCKET_HOSTNAME, "localhost");
81      }
82    }
83  
84    /**
85     * A socket factory that handles a custom hostname ("localhost", by default, and configured by the
86     * system property &quot;org.newsclub.net.unix.socket.hostname&quot;), forwarding all other
87     * requests to the fallback {@link SocketFactory}.
88     *
89     * The socket path is configured through an argument passed by to the constructor.
90     *
91     * This is particularly useful for JDBC drivers that take a "socketFactory" and a
92     * "socketFactoryArg". The latter will be passed as a constructor argument.
93     */
94    @SuppressFBWarnings("PATH_TRAVERSAL_IN")
95    public static final class FactoryArg extends DefaultSocketHostnameSocketFactory {
96      private final File socketFile;
97  
98      /**
99       * Constructs a new {@link FactoryArg} factory using the given socket path.
100      *
101      * @param socketPath The path to the socket.
102      */
103     public FactoryArg(String socketPath) {
104       super();
105       Objects.requireNonNull(socketPath, "Socket path was null");
106 
107       this.socketFile = new File(socketPath);
108     }
109 
110     /**
111      * Constructs a new {@link FactoryArg} factory using the given socket path.
112      *
113      * @param file The path to the socket.
114      */
115     public FactoryArg(File file) {
116       super();
117       Objects.requireNonNull(file, "File was null");
118 
119       this.socketFile = file;
120     }
121 
122     @Override
123     public AFUNIXSocketAddress addressFromHost(String host, int port) throws SocketException {
124       return AFUNIXSocketAddress.of(socketFile, port);
125     }
126   }
127 
128   /**
129    * A socket factory that handles a custom hostname ("junixsocket.localhost", by default, and
130    * configured by the system property &quot;org.newsclub.net.unix.socket.hostname&quot;),
131    * forwarding all other requests to the fallback {@link SocketFactory}.
132    *
133    * The socket path is configured through a system property,
134    * &quot;org.newsclub.net.unix.socket.default&quot;.
135    *
136    * NOTE: While it is technically possible, it is highly discouraged to programmatically change the
137    * value of the property as it can lead to concurrency issues and undefined behavior.
138    */
139   public static final class SystemProperty extends DefaultSocketHostnameSocketFactory {
140     private static final String PROP_SOCKET_DEFAULT = "org.newsclub.net.unix.socket.default";
141 
142     /**
143      * Creates a {@link SystemProperty} socket factory.
144      */
145     public SystemProperty() {
146       super();
147     }
148 
149     @Override
150     public AFUNIXSocketAddress addressFromHost(String host, int port) throws SocketException {
151       String path = System.getProperty(PROP_SOCKET_DEFAULT);
152       if (path == null || path.isEmpty()) {
153         throw new IllegalStateException("Property not configured: " + PROP_SOCKET_DEFAULT);
154       }
155       File socketFile = new File(path);
156 
157       return AFUNIXSocketAddress.of(socketFile, port);
158     }
159   }
160 
161   /**
162    * A socket factory that handles special host names formatted as file:// URIs.
163    *
164    * The file:// URI may also be specified in URL-encoded format, i.e., file:%3A%2F%2F etc.
165    *
166    * You may also surround the URL with square brackets ("[" and "]"), whereas the closing bracket
167    * may be omitted.
168    *
169    * NOTE: In some circumstances it is recommended to use "<code>[file:%3A%2F%2F</code>(...)", i.e.
170    * encoded and without the closing bracket. Since this is an invalid hostname, it will not trigger
171    * a DNS lookup, but can still be used within a JDBC Connection URL.
172    */
173   @SuppressFBWarnings("PATH_TRAVERSAL_IN")
174   public static final class URIScheme extends AFUNIXSocketFactory {
175     private static final String FILE_SCHEME_PREFIX = "file://";
176     private static final String FILE_SCHEME_PREFIX_ENCODED = "file%";
177     private static final String FILE_SCHEME_LOCALHOST = "localhost";
178 
179     /**
180      * Creates a {@link URIScheme} socket factory.
181      */
182     public URIScheme() {
183       super();
184     }
185 
186     private static String stripBrackets(String host) {
187       if (host.startsWith("[")) {
188         if (host.endsWith("]")) {
189           host = host.substring(1, host.length() - 1);
190         } else {
191           host = host.substring(1);
192         }
193       }
194       return host;
195     }
196 
197     @Override
198     public boolean isHostnameSupported(String host) {
199       host = stripBrackets(host);
200       return host.startsWith(FILE_SCHEME_PREFIX) || host.startsWith(FILE_SCHEME_PREFIX_ENCODED);
201     }
202 
203     @Override
204     @SuppressWarnings("PMD.UseStandardCharsets")
205     public AFUNIXSocketAddress addressFromHost(String host, int port) throws SocketException {
206       host = stripBrackets(host);
207       if (host.startsWith(FILE_SCHEME_PREFIX_ENCODED)) {
208         try {
209           host = URLDecoder.decode(host, "UTF-8");
210         } catch (Exception e) {
211           throw (SocketException) new SocketException().initCause(e);
212         }
213       }
214       if (!host.startsWith(FILE_SCHEME_PREFIX)) {
215         throw new SocketException("Unsupported scheme");
216       }
217 
218       String path = host.substring(FILE_SCHEME_PREFIX.length());
219       if (path.startsWith(FILE_SCHEME_LOCALHOST)) {
220         path = path.substring(FILE_SCHEME_LOCALHOST.length());
221       }
222       if (path.isEmpty()) {
223         throw new SocketException("Path is empty");
224       }
225       if (!path.startsWith("/")) {
226         throw new SocketException("Path must be absolute");
227       }
228 
229       File socketFile = new File(path);
230       return AFUNIXSocketAddress.of(socketFile, port);
231     }
232   }
233 }