Skip to content

Commit

Permalink
PVA client: Handle both "tcp" and "tls" in search reply
Browse files Browse the repository at this point in the history
  • Loading branch information
kasemir committed Aug 7, 2023
1 parent ab79133 commit c9f9b70
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 52 deletions.
2 changes: 2 additions & 0 deletions core/pva/src/main/java/org/epics/pva/PVASettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ public class PVASettings
/** Path to trust store, a PKCS12 file that contains the certificates or root CA
* that the client will trust.
* When empty, PVA client does not support secure (TLS) communication.
* When configured, PVA client can reply to PVA servers that offer "tls" in a search reply,
* and searches via EPICS_PVA_NAME_SERVERS will also use TLS.
*/
public static String EPICS_PVA_TLS_KEYCHAIN = "";

Expand Down
20 changes: 16 additions & 4 deletions core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.logging.Level;

import org.epics.pva.PVASettings;
Expand Down Expand Up @@ -156,7 +156,8 @@ private class SearchedChannel

private final ClientUDPHandler udp;

private final Function<InetSocketAddress, ClientTCPHandler> tcp_provider;
/** Create ClientTCPHandler from IP address and 'tls' flag */
private final BiFunction<InetSocketAddress, Boolean, ClientTCPHandler> tcp_provider;

/** Buffer for assembling search messages */
private final ByteBuffer send_buffer = ByteBuffer.allocate(PVASettings.MAX_UDP_UNFRAGMENTED_SEND);
Expand All @@ -169,9 +170,16 @@ private class SearchedChannel
b_or_mcast_search_addresses = new ArrayList<>(),
name_server_addresses = new ArrayList<>();

/** Create channel searcher
* @param udp UDP handler
* @param udp_addresses UDP addresses to search
* @param tcp_provider Function that creates ClientTCPHandler for IP address and 'tls' flag
* @param name_server_addresses TCP addresses to search
* @throws Exception on error
*/
public ChannelSearch(final ClientUDPHandler udp,
final List<AddressInfo> udp_addresses,
final Function<InetSocketAddress, ClientTCPHandler> tcp_provider,
final BiFunction<InetSocketAddress, Boolean, ClientTCPHandler> tcp_provider,
final List<AddressInfo> name_server_addresses) throws Exception
{
this.udp = udp;
Expand Down Expand Up @@ -413,7 +421,11 @@ private void search(final Collection<SearchRequest.Channel> channels)
// Search via TCP
for (AddressInfo name_server : name_server_addresses)
{
final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress());
// TODO How to decide if TCP search should use TLS?
// Configure via EPICS_PVA_NAME_SERVERS?
// For now configuring EPICS_PVA_TLS_KEYCHAIN enables TLS for all name server lookups
final boolean tls = !PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank();
final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress(), tls);

// In case of connection errors (TCP connection blocked by firewall),
// tcp will be null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ class ClientTCPHandler extends TCPHandler
*/
private final AtomicBoolean connection_validated = new AtomicBoolean(false);

public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid) throws Exception
public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid, final boolean tls) throws Exception
{
super(createSocket(address), true);
super(createSocket(address, tls), true);
logger.log(Level.FINE, () -> "TCPHandler " + guid + " for " + address + " created ============================");
this.client = client;
this.guid = guid;
Expand All @@ -117,9 +117,9 @@ public ClientTCPHandler(final PVAClient client, final InetSocketAddress address,
// it's started when server confirms the connection.
}

private static Socket createSocket(InetSocketAddress address) throws Exception
private static Socket createSocket(InetSocketAddress address, final boolean tls) throws Exception
{
final Socket socket = SecureSockets.getClientFactory().createSocket(address.getAddress(), address.getPort());
final Socket socket = SecureSockets.createClientSocket(address, tls);
socket.setTcpNoDelay(true);
socket.setKeepAlive(true);
return socket;
Expand Down
8 changes: 4 additions & 4 deletions core/pva/src/main/java/org/epics/pva/client/PVAClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.logging.Level;

import org.epics.pva.PVASettings;
Expand Down Expand Up @@ -89,13 +89,13 @@ public PVAClient() throws Exception

// TCP traffic is handled by one ClientTCPHandler per address (IP, socket).
// Pass helper to channel search for getting such a handler.
final Function<InetSocketAddress, ClientTCPHandler> tcp_provider = the_addr ->
final BiFunction<InetSocketAddress, Boolean, ClientTCPHandler> tcp_provider = (the_addr, use_tls) ->
tcp_handlers.computeIfAbsent(the_addr, addr ->
{
try
{
// If absent, create with initial empty GUID
return new ClientTCPHandler(this, addr, Guid.EMPTY);
return new ClientTCPHandler(this, addr, Guid.EMPTY, use_tls);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -254,7 +254,7 @@ void handleSearchResponse(final int channel_id, final InetSocketAddress server,
{
try
{
return new ClientTCPHandler(this, addr, guid);
return new ClientTCPHandler(this, addr, guid, tls);
}
catch (Exception ex)
{
Expand Down
71 changes: 55 additions & 16 deletions core/pva/src/main/java/org/epics/pva/common/SecureSockets.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
import static org.epics.pva.PVASettings.logger;

import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.util.logging.Level;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;

import org.epics.pva.PVASettings;
Expand All @@ -35,13 +39,15 @@
@SuppressWarnings("nls")
public class SecureSockets
{
private static ServerSocketFactory server_sockets;
private static SocketFactory client_sockets;
private static boolean initialized = false;
private static ServerSocketFactory tls_server_sockets;
private static SocketFactory tls_client_sockets;

private static synchronized void initialize() throws Exception
{
// TODO For now always creating TLS sockets based on preference settings.
// Need to create them based on search response requesting TLS
if (initialized)
return;

final char[] password = PVASettings.EPICS_PVA_STOREPASS.isBlank() ? null : PVASettings.EPICS_PVA_STOREPASS.toCharArray();

if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank())
Expand All @@ -56,10 +62,8 @@ private static synchronized void initialize() throws Exception
final SSLContext context = SSLContext.getInstance("TLS");
context.init(key_manager.getKeyManagers(), null, null);

server_sockets = context.getServerSocketFactory();
tls_server_sockets = context.getServerSocketFactory();
}
else
server_sockets = ServerSocketFactory.getDefault();

if (! PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank())
{
Expand All @@ -73,27 +77,62 @@ private static synchronized void initialize() throws Exception
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trust_manager.getTrustManagers(), null);

client_sockets = context.getSocketFactory();
tls_client_sockets = context.getSocketFactory();
}
else
client_sockets = SocketFactory.getDefault();
initialized = true;
}

/** @return Factory for plain or secure server sockets
/** Create server socket
* @param address IP address and port to which the socket will be bound
* @param tls Use TLS socket? Otherwise plain TCP
* @return Plain or secure server socket
* @throws Exception on error
*/
public static ServerSocketFactory getServerFactory() throws Exception
public static ServerSocket createServerSocket(final InetSocketAddress address, final boolean tls) throws Exception
{
initialize();
return server_sockets;
final ServerSocket socket;
if (tls)
{
if (tls_server_sockets == null)
throw new Exception("TLS is not supported. Configure EPICS_PVAS_TLS_KEYCHAIN");
socket = tls_server_sockets.createServerSocket();
}
else
socket = new ServerSocket();

try
{
socket.setReuseAddress(true);
socket.bind(address);
}
catch (Exception ex)
{
socket.close();
throw ex;
}
return socket;
}

/** @return Factory for plain or secure client sockets
/** Create client socket
* @param address IP address and port to which the socket will be bound
* @param tls Use TLS socket? Otherwise plain TCP
* @return Plain or secure client socket
* @throws Exception on error
*/
public static SocketFactory getClientFactory() throws Exception
public static Socket createClientSocket(final InetSocketAddress address, final boolean tls) throws Exception
{
initialize();
return client_sockets;
if (! tls)
return new Socket(address.getAddress(), address.getPort());

if (tls_client_sockets == null)
throw new Exception("TLS is not supported. Configure EPICS_PVA_TLS_KEYCHAIN");
final SSLSocket socket = (SSLSocket) tls_client_sockets.createSocket(address.getAddress(), address.getPort());
// PVXS prefers 1.3
socket.setEnabledProtocols(new String[] { "TLSv1.3"});
// Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket
socket.startHandshake();
return socket;
}
}
29 changes: 5 additions & 24 deletions core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -126,13 +125,16 @@ private static boolean checkForIPv4Server(final int desired_port)
*/
private static ServerSocket createSocket() throws Exception
{
// If a PVA Server keychain has been configured, use TLS
final boolean tls = !PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank();

if (checkForIPv4Server(PVASettings.EPICS_PVA_SERVER_PORT))
logger.log(Level.FINE, "Found existing IPv4 server on port " + PVASettings.EPICS_PVA_SERVER_PORT);
else
{ // Try to bind to desired port
try
{
return createBoundSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT));
return SecureSockets.createServerSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT), tls);
}
catch (BindException ex)
{
Expand All @@ -144,35 +146,14 @@ private static ServerSocket createSocket() throws Exception
final InetSocketAddress any = new InetSocketAddress(0);
try
{
return createBoundSocket(any);
return SecureSockets.createServerSocket(any, tls);
}
catch (Exception e)
{
throw new Exception("Cannot bind to automatically assigned port " + any, e);
}
}

/** Try to create socket that's bound to an address
* @param addr Desired address
* @return {@link ServerSocketChannel}
* @throws Exception on error
*/
private static ServerSocket createBoundSocket(final InetSocketAddress addr) throws Exception
{
final ServerSocket socket = SecureSockets.getServerFactory().createServerSocket();
try
{
socket.setReuseAddress(true);
socket.bind(addr);
}
catch (Exception ex)
{
socket.close();
throw ex;
}
return socket;
}

private void listen()
{
try
Expand Down

0 comments on commit c9f9b70

Please sign in to comment.