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