Skip to content

Commit 3e53124

Browse files
committed
#828 Add support for custom SocketFactory in connection string and data sources
1 parent 4cc2d47 commit 3e53124

15 files changed

+468
-20
lines changed

devdoc/jdp/jdp-2024-09-custom-socket-factory-for-pure-java-connections.adoc

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
== Status
44

5-
* Draft
6-
* Proposed for: Jaybird 6
5+
* Published: 2024-12-18
6+
* Implemented in: Jaybird 6
77

88
== Type
99

@@ -15,7 +15,7 @@ Jaybird 5 and earlier directly create `Socket` instances.
1515
There are use-cases where it might be worthwhile to have more control over socket creation.
1616
For example, for SOCKS proxy creation with custom instead of global config (see https://github.com/FirebirdSQL/jaybird/issues/826[#826]), or to allow TLS connections with a TLS proxy (gateway) instead of relying on built-in wire-encryption.
1717

18-
Adding support for a custom `javax.net.SocketFactory` would allow users to override socket creation.
18+
Adding support for a custom `javax.net.SocketFactory` allows users to override socket creation.
1919

2020
As shown by the SOCKS proxy example of https://github.com/FirebirdSQL/jaybird/issues/826[#826], having some way to expose connection-specific information would also be useful.
2121
This should not expose *all* connection information, but only that information that the user explicitly wants to pass to the custom socket factory.
@@ -26,9 +26,8 @@ Jaybird will add a connection property `socketFactory`, which accepts the name o
2626
If the property is not set (the default), the default `SocketFactory` (`SocketFactory.getDefault()`) is used.
2727
The `SocketFactory` will be created anew for each connection.
2828

29-
The implementation either has a parameterless constructor, or a constructor accepting a `java.util.Properties` object.
30-
This `Properties` object is used to pass custom properties to the socket factory.
31-
It is populated by selecting the connection properties with the suffix `@socketFactory` and including the non-``null`` string values in the `Properties` object.
29+
The implementation either has a public single-arg constructor accepting a `java.util.Properties` object, or a public no-arg constructor.
30+
This `Properties` object is used to pass custom properties to the socket factory, and is populated by selecting the connection properties with the suffix `@socketFactory` and including the non-``null`` string values in the `Properties` object.
3231
The suffix is retained for the property names (this reduces ambiguity, and will also allow us to include other properties in the future).
3332

3433
We explicitly and intentionally do not add support to set a `SocketFactory` instance (e.g. on a `DataSource`).
@@ -41,4 +40,4 @@ This support is limited to the pure Java implementation.
4140
The `socketFactory` option and passing of configuration must be documented in the Jaybird manual.
4241

4342
Currently, Jaybird always creates unconnected sockets (that is, `SocketFactory.createSocket()`).
44-
We recommend that implementations that don't support the other `createSocket` methods to throw an `UnsupportedOperationException` or an `IOException` with a clear message that the socket factory does not support the method.
43+
We recommend that implementations that don't support the other `createSocket` methods to throw an `UnsupportedOperationException` with a clear message that the socket factory does not support the method.

src/docs/asciidoc/release_notes.adoc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,52 @@ We're considering to make server-side scrollable cursors the default in a future
10421042

10431043
See also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2024-05-behavior-of-updatable-result-sets.adoc[jdp-2024-05: Behaviour of Updatable Result Sets^].
10441044

1045+
[#custom-socket-factory]
1046+
=== Custom socket factory for pure Java connections
1047+
1048+
A custom socket factory can now be specified, to customize the creation of the `java.net.Socket` instance of a pure Java database or service connection.
1049+
1050+
The connection property `socketFactory` accepts the class name of an implementation of `javax.net.SocketFactory`.
1051+
This socket factory is created anew for each connection.
1052+
If `socketFactory` is not specified, Jaybird will use `SocketFactory.getDefault()` as its factory.
1053+
1054+
The `SocketFactory` implementation must adhere to the following rules:
1055+
1056+
- The class must have a public constructor accepting a `java.util.Properties` object, or a public no-arg constructor.
1057+
- The implementation of `SocketFactory#createSocket()` must return an unconnected socket;
1058+
the other `createSocket` methods are not called by Jaybird.
1059+
+
1060+
If you don't want to implement the other `createSocket` methods, we recommend throwing `java.lang.UnsupportedOperationException` with a clear message from those methods.
1061+
1062+
It is possible to pass custom connection properties to the socket factory if it has a public single-arg constructor accepting a `Properties` object.
1063+
Jaybird will instantiate the socket factory with a `Properties` object containing _only_ the connection properties with the suffix `@socketFactory` and a non-``null`` values;
1064+
non-string values are converted to string.
1065+
In the future, we may also -- selectively -- pass other connection properties, but for now we only expose those properties that are explicitly set for the socket factory.
1066+
1067+
For example, say we have some custom socket factory called `org.example.CustomProxySocketFactory` with a `CustomProxySocketFactory(Properties)` constructor:
1068+
1069+
[source,java]
1070+
----
1071+
var props = new Properties()
1072+
props.setProperty("user", "sysdba");
1073+
props.setProperty("password", "masterkey");
1074+
props.setProperty("socketFactory", "org.example.CustomProxySocketFactory");
1075+
props.setProperty("proxyHost@socketFactory", "localhost");
1076+
props.setProperty("proxyPort@socketFactory", "1234");
1077+
props.setProperty("proxyUser@socketFactory", "proxy-user");
1078+
props.setProperty("proxyPassword@socketFactory", "proxy-password");
1079+
1080+
try (var connection = DriverManager.getConnection(
1081+
"jdbc:firebird://remoteserver.example.org/db", props)) {
1082+
// use connection
1083+
}
1084+
----
1085+
1086+
This will create the specified socket factory, passing a `Properties` object containing *only* the four custom properties ending in `@socketFactory`.
1087+
The other properties -- here `user`, `password` and `socketFactory` -- are *not* passed to the socket factory.
1088+
1089+
See also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2024-09-custom-socket-factory-for-pure-java-connections.adoc[jdp-2024-09: Custom socket factory for pure Java connections]
1090+
10451091
// TODO add major changes
10461092

10471093
[#other-fixes-and-changes]

src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,16 @@ public void setParallelWorkers(int parallelWorkers) {
446446
FirebirdConnectionProperties.super.setParallelWorkers(parallelWorkers);
447447
}
448448

449+
@Override
450+
public String getSocketFactory() {
451+
return FirebirdConnectionProperties.super.getSocketFactory();
452+
}
453+
454+
@Override
455+
public void setSocketFactory(String socketFactory) {
456+
FirebirdConnectionProperties.super.setSocketFactory(socketFactory);
457+
}
458+
449459
@Override
450460
public boolean isUseCatalogAsPackage() {
451461
return FirebirdConnectionProperties.super.isUseCatalogAsPackage();

src/main/org/firebirdsql/gds/JaybirdErrorCodes.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ public interface JaybirdErrorCodes {
152152
int jb_noAuthenticationPlugin = 337248344;
153153
int jb_asyncChannelAlreadyEstablished = 337248345;
154154
int jb_asyncChannelNotConnected = 337248346;
155+
int jb_socketFactoryClassNotFound = 337248347;
156+
int jb_socketFactoryConstructorNotFound = 337248348;
157+
int jb_socketFactoryFailedToCreateSocket = 337248349;
155158

156159
@SuppressWarnings("unused")
157160
int jb_range_end = 337264639;

src/main/org/firebirdsql/gds/ng/wire/WireConnection.java

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,14 @@
3636
import org.firebirdsql.gds.ng.dbcrypt.DbCryptCallback;
3737
import org.firebirdsql.gds.ng.wire.auth.ClientAuthBlock;
3838
import org.firebirdsql.gds.ng.wire.crypt.KnownServerKey;
39+
import org.firebirdsql.jaybird.props.def.ConnectionProperty;
3940
import org.firebirdsql.jaybird.util.ByteArrayHelper;
4041

42+
import javax.net.SocketFactory;
4143
import java.io.ByteArrayOutputStream;
4244
import java.io.Closeable;
4345
import java.io.IOException;
46+
import java.lang.reflect.Constructor;
4447
import java.net.*;
4548
import java.nio.charset.StandardCharsets;
4649
import java.security.AccessController;
@@ -54,6 +57,7 @@
5457
import java.util.List;
5558
import java.util.Map;
5659
import java.util.Optional;
60+
import java.util.Properties;
5761
import java.util.concurrent.TimeUnit;
5862

5963
import static java.lang.System.Logger.Level.DEBUG;
@@ -229,30 +233,25 @@ public final void resetSocketTimeout() throws SQLException {
229233
}
230234

231235
/**
232-
* Establishes the TCP/IP connection to serverName and portNumber of this
233-
* Connection
236+
* Establishes the TCP/IP connection to serverName and portNumber of this connection.
234237
*
235238
* @throws SQLTimeoutException
236-
* If the connection cannot be established within the connect
237-
* timeout (either explicitly set or implied by the OS timeout
238-
* of the socket)
239+
* if the connection cannot be established within the connect timeout (either explicitly set or implied by
240+
* the OS timeout of the socket)
239241
* @throws SQLException
240-
* If the connection cannot be established.
242+
* if the connection cannot be established.
241243
*/
242244
public final void socketConnect() throws SQLException {
243245
try {
244-
socket = new Socket();
246+
socket = createSocket();
245247
socket.setTcpNoDelay(true);
246248
final int connectTimeout = attachProperties.getConnectTimeout();
247-
final int socketConnectTimeout;
248-
if (connectTimeout != -1) {
249-
// connectTimeout is in seconds, need milliseconds
250-
socketConnectTimeout = (int) TimeUnit.SECONDS.toMillis(connectTimeout);
249+
// connectTimeout is in seconds, need milliseconds, lower bound 0 (indefinite, for overflow or not set)
250+
final int socketConnectTimeout = Math.max(0, (int) TimeUnit.SECONDS.toMillis(connectTimeout));
251+
if (socketConnectTimeout != 0) {
251252
// Blocking timeout initially identical to connect timeout
252253
socket.setSoTimeout(socketConnectTimeout);
253254
} else {
254-
// socket connect timeout is not set, so indefinite (0)
255-
socketConnectTimeout = 0;
256255
// Blocking timeout to normal socket timeout, 0 if not set
257256
socket.setSoTimeout(Math.max(attachProperties.getSoTimeout(), 0));
258257
}
@@ -277,6 +276,70 @@ public final void socketConnect() throws SQLException {
277276
}
278277
}
279278

279+
private Socket createSocket() throws IOException, SQLException {
280+
try {
281+
return createSocketFactory().createSocket();
282+
} catch (RuntimeException e) {
283+
throw FbExceptionBuilder
284+
.forNonTransientConnectionException(JaybirdErrorCodes.jb_socketFactoryFailedToCreateSocket)
285+
.messageParameter(attachProperties.getSocketFactory())
286+
.cause(e)
287+
.toSQLException();
288+
}
289+
}
290+
291+
private SocketFactory createSocketFactory() throws SQLException {
292+
String socketFactoryName = attachProperties.getSocketFactory();
293+
if (socketFactoryName == null) {
294+
return SocketFactory.getDefault();
295+
}
296+
return createSocketFactory0(socketFactoryName);
297+
}
298+
299+
private SocketFactory createSocketFactory0(String socketFactoryName) throws SQLException {
300+
log.log(DEBUG, "Attempting to create custom socket factory {0}", socketFactoryName);
301+
try {
302+
Class<? extends SocketFactory> socketFactoryClass =
303+
Class.forName(socketFactoryName).asSubclass(SocketFactory.class);
304+
try {
305+
Constructor<? extends SocketFactory> propsConstructor =
306+
socketFactoryClass.getConstructor(Properties.class);
307+
return propsConstructor.newInstance(getSocketFactoryProperties());
308+
} catch (ReflectiveOperationException e) {
309+
log.log(DEBUG, socketFactoryName
310+
+ "has no Properties constructor, or constructor execution resulted in an exception", e);
311+
}
312+
try {
313+
Constructor<? extends SocketFactory> noArgConstructor = socketFactoryClass.getConstructor();
314+
return noArgConstructor.newInstance();
315+
} catch (ReflectiveOperationException e) {
316+
log.log(DEBUG, socketFactoryName
317+
+ "has no no-arg constructor, or constructor execution resulted in an exception", e);
318+
}
319+
throw FbExceptionBuilder
320+
.forNonTransientConnectionException(JaybirdErrorCodes.jb_socketFactoryConstructorNotFound)
321+
.messageParameter(socketFactoryName)
322+
.toSQLException();
323+
} catch (ClassNotFoundException | ClassCastException e) {
324+
throw FbExceptionBuilder.forNonTransientConnectionException(JaybirdErrorCodes.jb_socketFactoryClassNotFound)
325+
.messageParameter(socketFactoryName)
326+
.cause(e)
327+
.toSQLException();
328+
}
329+
}
330+
331+
private Properties getSocketFactoryProperties() {
332+
var props = new Properties();
333+
attachProperties.connectionPropertyValues().entrySet().stream()
334+
.filter(e ->
335+
e.getValue() != null && e.getKey().name().endsWith("@socketFactory"))
336+
.forEach(e -> {
337+
ConnectionProperty connectionProperty = e.getKey();
338+
props.setProperty(connectionProperty.name(), connectionProperty.type().asString(e.getValue()));
339+
});
340+
return props;
341+
}
342+
280343
public final XdrStreamAccess getXdrStreamAccess() {
281344
return streamAccess;
282345
}

src/main/org/firebirdsql/jaybird/props/AttachmentProperties.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,34 @@ default void setParallelWorkers(int parallelWorkers) {
465465
setIntProperty(PropertyNames.parallelWorkers, parallelWorkers);
466466
}
467467

468+
/**
469+
* The class name of a custom socket factory to be used for pure Java connections.
470+
*
471+
* @return fully-qualified class name of a {@link javax.net.SocketFactory} implementation, or (default) {@code null}
472+
* for the default socket factory
473+
* @since 6
474+
* @see #setSocketFactory(String)
475+
*/
476+
default String getSocketFactory() {
477+
return getProperty(PropertyNames.socketFactory);
478+
}
479+
480+
/**
481+
* Sets the class name of a custom socket factory to be used for pure Java connections.
482+
* <p>
483+
* The class must extend {@link javax.net.SocketFactory} and have a public single-arg constructor accepting
484+
* a {@link java.util.Properties}, or a public no-arg constructor. The {@code Properties} object passed in the first
485+
* case contains custom connection properties with the suffix {@code @socketFactory}, and &mdash; possibly &mdash;
486+
* other selected properties.
487+
* </p>
488+
*
489+
* @param socketFactory
490+
* fully-qualified class name of a {@link javax.net.SocketFactory} implementation, or {@code null} for
491+
* the default socket factory
492+
* @since 6
493+
*/
494+
default void setSocketFactory(String socketFactory) {
495+
setProperty(PropertyNames.socketFactory, socketFactory);
496+
}
497+
468498
}

src/main/org/firebirdsql/jaybird/props/PropertyNames.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public final class PropertyNames {
6262
public static final String wireCompression = "wireCompression";
6363
public static final String enableProtocol = "enableProtocol";
6464
public static final String parallelWorkers = "parallelWorkers";
65+
public static final String socketFactory = "socketFactory";
6566

6667
// database connection
6768
public static final String sqlDialect = "sqlDialect";

src/main/org/firebirdsql/jaybird/props/internal/StandardConnectionPropertyDefiner.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public Stream<ConnectionProperty> defineProperties() {
7878
builder(enableProtocol),
7979
builder(parallelWorkers).type(INT).aliases("parallel_workers", "isc_dpb_parallel_workers")
8080
.dpbItem(isc_dpb_parallel_workers),
81+
builder(socketFactory),
8182

8283
// Database properties
8384
builder(charSet).aliases("charset", "localEncoding", "local_encoding"),

src/resources/org/firebirdsql/jaybird_error_msg.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,6 @@
8787
337248344=No authentication plugin available
8888
337248345=Asynchronous channel already established
8989
337248346=Asynchronous channel not connected
90+
337248347=Could not find socket factory class {0}, or class does not extend javax.net.SocketFactory
91+
337248348=No suitable socket factory constructor found in class {0}: a public constructor accepting java.util.Properties or a public no-arg constructor is required
92+
337248349=Socket factory {0} failed to create a socket

src/resources/org/firebirdsql/jaybird_error_sqlstates.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,6 @@
9595
337248344=08000
9696
337248345=08002
9797
337248346=08006
98+
337248347=08001
99+
337248348=08001
100+
337248349=08001

0 commit comments

Comments
 (0)