diff --git a/core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java b/core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java index a34180c2b3..712c27cf26 100644 --- a/core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java +++ b/core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -44,7 +44,9 @@ public PVA_PV(final String name, final String base_name) throws Exception // Analyze base_name, determine channel and request name_helper = PVNameHelper.forName(base_name); logger.log(Level.FINE, () -> "PVA '" + base_name + "' -> " + name_helper); - channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(), this::channelStateChanged); + channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(), + this::channelStateChanged, + this::accessRightsChanged); } private void channelStateChanged(final PVAChannel channel, final ClientChannelState state) @@ -67,6 +69,11 @@ else if (! isDisconnected(super.read())) } } + private void accessRightsChanged(final PVAChannel channel, final boolean is_writable) + { + notifyListenersOfPermissions(! is_writable); + } + private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, diff --git a/core/pva/src/main/java/org/epics/pva/PVASettings.java b/core/pva/src/main/java/org/epics/pva/PVASettings.java index 749f50d6c5..9635b012c7 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -219,7 +219,7 @@ public class PVASettings public static int EPICS_PVA_TCP_SOCKET_TMO = 5; /** Maximum number of array elements shown when printing data */ - public static int EPICS_PVA_MAX_ARRAY_FORMATTING = 256; + public static int EPICS_PVA_MAX_ARRAY_FORMATTING = 50; /** Range of beacon periods in seconds recognized as "fast, new" beacons * that re-start searches for disconnected channels. @@ -275,6 +275,11 @@ public class PVASettings EPICS_PVAS_TLS_OPTIONS = get("EPICS_PVAS_TLS_OPTIONS", EPICS_PVAS_TLS_OPTIONS); require_client_cert = EPICS_PVAS_TLS_OPTIONS.contains("client_cert=require"); EPICS_PVA_TLS_KEYCHAIN = get("EPICS_PVA_TLS_KEYCHAIN", EPICS_PVA_TLS_KEYCHAIN); + if (EPICS_PVA_TLS_KEYCHAIN.isEmpty() && !EPICS_PVAS_TLS_KEYCHAIN.isEmpty()) + { + EPICS_PVA_TLS_KEYCHAIN = EPICS_PVAS_TLS_KEYCHAIN; + logger.log(Level.CONFIG, "EPICS_PVA_TLS_KEYCHAIN (empty) updated from EPICS_PVAS_TLS_KEYCHAIN"); + } EPICS_PVA_SEND_BUFFER_SIZE = get("EPICS_PVA_SEND_BUFFER_SIZE", EPICS_PVA_SEND_BUFFER_SIZE); EPICS_PVA_FAST_BEACON_MIN = get("EPICS_PVA_FAST_BEACON_MIN", EPICS_PVA_FAST_BEACON_MIN); EPICS_PVA_FAST_BEACON_MAX = get("EPICS_PVA_FAST_BEACON_MAX", EPICS_PVA_FAST_BEACON_MAX); diff --git a/core/pva/src/main/java/org/epics/pva/client/AccessRightsChangeHandler.java b/core/pva/src/main/java/org/epics/pva/client/AccessRightsChangeHandler.java new file mode 100644 index 0000000000..f5e6e7f8fc --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/client/AccessRightsChangeHandler.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.client; + +import static org.epics.pva.PVASettings.logger; + +import java.nio.ByteBuffer; +import java.util.logging.Level; + +import org.epics.pva.common.AccessRightsChange; +import org.epics.pva.common.CommandHandler; +import org.epics.pva.common.PVAHeader; + +/** Handle a server's CMD_ACL_CHANGE message + * @author Kay Kasemir + */ +class AccessRightsChangeHandler implements CommandHandler +{ + @Override + public byte getCommand() + { + return PVAHeader.CMD_ACL_CHANGE; + } + + @Override + public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) throws Exception + { + final AccessRightsChange acl = AccessRightsChange.decode(tcp.getRemoteAddress(), buffer.remaining(), buffer); + if (acl == null) + return; + final PVAChannel channel = tcp.getClient().getChannel(acl.cid); + if (channel == null) + { + logger.log(Level.WARNING, this + " received CMD_ACL_CHANGE for unknown channel ID " + acl.cid); + return; + } + + logger.log(Level.FINE, () -> "Received '" + channel.getName() + "' " + acl); + channel.updateAccessRights(acl.havePUTaccess()); + } +} diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientAccessRightsListener.java b/core/pva/src/main/java/org/epics/pva/client/ClientAccessRightsListener.java new file mode 100644 index 0000000000..9c6546679d --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/client/ClientAccessRightsListener.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.client; + +/** Listener to a {@link PVAChannel} access rights + * @author Kay Kasemir + */ +@FunctionalInterface +public interface ClientAccessRightsListener +{ + /** Invoked when the channel access rights change + * + *

Will be called as soon as possible, i.e. within + * the thread that handles the network communication. + * + *

Client code must not block. + * + * @param channel Channel with updated permissions + * @param is_writable May we write to the channel? + */ + public void channelAccessRightsChanged(PVAChannel channel, boolean is_writable); +} diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientChannelListener.java b/core/pva/src/main/java/org/epics/pva/client/ClientChannelListener.java index 6614b783c2..3bca80b887 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientChannelListener.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientChannelListener.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,10 +7,10 @@ ******************************************************************************/ package org.epics.pva.client; -/** Listener to a {@link PVAChannel} - * +/** Listener to a {@link PVAChannel} state * @author Kay Kasemir */ +@FunctionalInterface public interface ClientChannelListener { /** Invoked when the channel state changes diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java index b94a9e29ce..10e1eb1c11 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java @@ -48,6 +48,7 @@ class ClientTCPHandler extends TCPHandler new ValidatedHandler(), new EchoHandler(), new CreateChannelHandler(), + new AccessRightsChangeHandler(), new DestroyChannelHandler(), new GetHandler(), new PutHandler(), diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java b/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java index 610ab5fc5b..e79bae9ca0 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java @@ -12,6 +12,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; @@ -38,13 +39,13 @@ * *

When no longer in use, the channel should be {@link #close()}d. * - * + * * Note that several methods return a CompletableFuture. * This has been done because at this time the Futures used internally are indeed CompletableFutures * and this type offers an extensive API for composition and chaining of futures. * But note that user code must never call 'complete(..)' nor 'completeExceptionally()' * on the provided CompletableFutures. - * + * * @author Kay Kasemir */ @SuppressWarnings("nls") @@ -63,7 +64,11 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable private final PVAClient client; private final ClientChannelListener listener; + private final ClientAccessRightsListener access_rights_listener; private volatile int sid = -1; + // For compatibility with earlier implementation, + // assume channels are writable until access_rights_listener tells otherwise + private AtomicBoolean is_writable = new AtomicBoolean(true); /** State * @@ -79,11 +84,14 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable private final CopyOnWriteArrayList subscriptions = new CopyOnWriteArrayList<>(); - PVAChannel(final PVAClient client, final String name, final ClientChannelListener listener) + PVAChannel(final PVAClient client, final String name, + final ClientChannelListener listener, + final ClientAccessRightsListener access_rights_listener) { super(CID_Provider.incrementAndGet(), name); this.client = client; this.listener = listener; + this.access_rights_listener = access_rights_listener; } PVAClient getClient() @@ -121,6 +129,21 @@ public boolean isConnected() return getState() == ClientChannelState.CONNECTED; } + /** @return true if channel has write permissions */ + public boolean isWritable() + { + return is_writable.get(); + } + + /** Called by AccessRightsChangeHandler + * @param may_write Is channel writable? + */ + void updateAccessRights(final boolean may_write) + { + if (is_writable.getAndSet(may_write) != may_write) + access_rights_listener.channelAccessRightsChanged(this, may_write); + } + /** Wait for channel to connect * @return {@link CompletableFuture} to await connection. * true on success, diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java index a51a711342..517528bfbc 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java @@ -40,12 +40,14 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PVAClient implements AutoCloseable { - /** Default channel listener logs state changes */ + /** Default channel state listener logs state changes */ private static final ClientChannelListener DEFAULT_CHANNEL_LISTENER = (ch, state) -> logger.log(Level.INFO, ch.toString()); + /** Default channel access rights listener does nothing */ + private static final ClientAccessRightsListener DEFAULT_ACCESS_RIGHTS_LISTENER = (ch, write) -> {}; + private final ClientUDPHandler udp; private final BeaconTracker beacons = new BeaconTracker(); @@ -166,7 +168,23 @@ public PVAChannel getChannel(final String channel_name) */ public PVAChannel getChannel(final String channel_name, final ClientChannelListener listener) { - final PVAChannel channel = new PVAChannel(this, channel_name, listener); + return getChannel(channel_name, listener, DEFAULT_ACCESS_RIGHTS_LISTENER); + } + + /** Create channel by name + * + *

Starts search. + * + * @param channel_name PVA channel name + * @param state_listener {@link ClientChannelListener} that will be invoked with connection state updates + * @param access_rights_listener {@link ClientAccessRightsListener} that will be invoked with access rights updates + * @return {@link PVAChannel} + */ + public PVAChannel getChannel(final String channel_name, + final ClientChannelListener state_listener, + final ClientAccessRightsListener access_rights_listener) + { + final PVAChannel channel = new PVAChannel(this, channel_name, state_listener, access_rights_listener); channels_by_id.putIfAbsent(channel.getCID(), channel); // Register with search diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java index cebbede602..0877bc3bd8 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java @@ -71,7 +71,10 @@ private static void help() private static void setLogLevel(final Level level) { - PVASettings.logger.setLevel(level); + // Cannot use PVASettings.logger here because that would + // construct it and log CONFIG messages before we might be + // able to disable them + Logger.getLogger("org.epics.pva").setLevel(level); Logger.getLogger("jdk.event.security").setLevel(level); } @@ -97,8 +100,8 @@ private static void info(final List names) throws Exception final PVAChannel pv = iter.next(); if (pv.getState() == ClientChannelState.CONNECTED) { - PVASettings.logger.log(Level.INFO, "Server: " + pv.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client: " + pv.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name()); final PVAData data = pv.info(request).get(timeout_ms, TimeUnit.MILLISECONDS); System.out.println(pv.getName() + " = " + data.formatType()); @@ -142,8 +145,8 @@ private static void get(final List names) throws Exception final PVAChannel pv = iter.next(); if (pv.getState() == ClientChannelState.CONNECTED) { - PVASettings.logger.log(Level.INFO, "Server: " + pv.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client: " + pv.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name()); final PVAData data = pv.read(request).get(timeout_ms, TimeUnit.MILLISECONDS); System.out.println(pv.getName() + " = " + data); @@ -188,8 +191,8 @@ private static void monitor(final List names) throws Exception { try { - PVASettings.logger.log(Level.INFO, "Server: " + ch.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client: " + ch.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.INFO, "Server X509 Name: " + ch.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.INFO, "Client X509 Name: " + ch.getTCP().getClientX509Name()); ch.subscribe(request, listener); } catch (Exception ex) diff --git a/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java b/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java new file mode 100644 index 0000000000..699956dd9b --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.common; + +import static org.epics.pva.PVASettings.logger; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.logging.Level; + +/** Helper for CMD_ACL_CHANGE + * @author Kay Kasemir + */ +public class AccessRightsChange +{ + /** Size of payload */ + public static final int PAYLOAD_SIZE = Integer.BYTES + 1; + + /** Client channel ID */ + public int cid; + + /** Access rights bits */ + public byte access_rights; + + /** Access rights bit definitions + * + * May client write (PUT), + * perform a write with read-back (PUT-GET) + * call a remote procedure (RPC)? + */ + public static final byte READ_ONLY = 0x00, + PUT_ACCESS = (1 << 0), + PUT_GET_ACCESS = (1 << 1), + RPC_ACCESS = (1 << 2); + + + private AccessRightsChange(final int cid, final byte access_rights) + { + this.cid = cid; + this.access_rights = access_rights; + } + + // TODO Add API for PUT_GET and RPC once PVXS has a reference implementation + + /** @return Do the access rights include write ('PUT') access? */ + public boolean havePUTaccess() + { + return (access_rights & PUT_ACCESS) == PUT_ACCESS; + } + + /** Encode access rights change + * @param buffer Buffer into which to encode + * @param cid Client channel ID + * @param b Access rights + */ + public static void encode(final ByteBuffer buffer, final int cid, final boolean writable) + { + PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_ACL_CHANGE, PAYLOAD_SIZE); + buffer.putInt(cid); + buffer.put(writable ? PUT_ACCESS : READ_ONLY); + } + + /** Decode access rights change + * @param from Peer address + * @param payload Payload size + * @param buffer Buffer positioned on payload + * @return Decoded access rights change or null if not a valid + */ + public static AccessRightsChange decode(final InetSocketAddress from, + final int payload, final ByteBuffer buffer) + { + if (payload < PAYLOAD_SIZE) + { + logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for access rights change"); + return null; + } + final AccessRightsChange acl = new AccessRightsChange(buffer.getInt(), buffer.get()); + logger.log(Level.FINER, () -> "PVA client " + from + " sent " + acl); + return acl; + } + + @Override + public String toString() + { + return String.format("CID %d access rights %s (0x%02X)", + cid, + havePUTaccess() ? "writeable" : "read-only", + access_rights); + } +} diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java index 5f11e90375..7a868ff1be 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java @@ -72,6 +72,9 @@ public class PVAHeader /** Application command: Reply to search */ public static final byte CMD_SEARCH_RESPONSE = 0x04; + /** Application command: Access Control (List) Channel */ + public static final byte CMD_ACL_CHANGE = 0x06; + /** Application command: Create Channel */ public static final byte CMD_CREATE_CHANNEL = 0x07; diff --git a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java index 31b88c3e26..092c39ff66 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java +++ b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java @@ -191,7 +191,7 @@ public static Socket createClientSocket(final InetSocketAddress address, final b // // #1: ObjectId: 1.3.6.1.4.1.37427.1 Criticality=false // 0000: 43 45 52 54 3A 53 54 41 54 55 53 3A 64 30 62 62 CERT:STATUS:d0bb... - // + // // Certificate[2]: // Owner: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA // Issuer: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA @@ -213,7 +213,7 @@ public static Socket createClientSocket(final InetSocketAddress address, final b logger.log(Level.FINE, " - Self-signed"); byte[] value = x509.getExtensionValue("1.3.6.1.4.1.37427.1"); - logger.log(Level.FINE, " - Status PV: " + decodeDERString(value)); + logger.log(Level.FINE, " - Status PV: '" + decodeDERString(value) + "'"); } } catch (Exception ex) @@ -227,13 +227,13 @@ public static Socket createClientSocket(final InetSocketAddress address, final b /** Decode DER String * @param der_value - * @return + * @return String, never null * @throws Exception on error */ public static String decodeDERString(final byte[] der_value) throws Exception { if (der_value == null) - return null; + return ""; // https://en.wikipedia.org/wiki/X.690#DER_encoding: // Type 4, length 0..127, characters if (der_value.length < 2) @@ -276,6 +276,14 @@ public static class TLSHandshakeInfo /** Host of the peer */ public String hostname; + /** PV for client certificate status */ + public String status_pv_name; + + @Override + public String toString() + { + return "Name " + name + ", host " + hostname + ", cert status PV " + status_pv_name; + } /** Get TLS/SSH info from socket * @param socket {@link SSLSocket} @@ -296,9 +304,40 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti try { + // Log certificate chain, grep cert status PV name + String status_pv_name = ""; + final SSLSession session = socket.getSession(); + logger.log(Level.FINER, "Client name: '" + SecureSockets.getPrincipalCN(session.getPeerPrincipal()) + "'"); + for (Certificate cert : session.getPeerCertificates()) + if (cert instanceof X509Certificate x509) + { + // Is this the cert for the client principal, or one of the authorities? + boolean is_principal_cert = false; + + logger.log(Level.FINER, "* " + x509.getSubjectX500Principal()); + if (session.getPeerPrincipal().equals(x509.getSubjectX500Principal())) + { + logger.log(Level.FINER, " - Client CN"); + is_principal_cert = true; + } + if (x509.getBasicConstraints() >= 0) + logger.log(Level.FINER, " - Certificate Authority"); + logger.log(Level.FINER, " - Expires " + x509.getNotAfter()); + if (x509.getSubjectX500Principal().equals(x509.getIssuerX500Principal())) + logger.log(Level.FINER, " - Self-signed"); + + byte[] value = x509.getExtensionValue("1.3.6.1.4.1.37427.1"); + String pv_name = SecureSockets.decodeDERString(value); + logger.log(Level.FINER, " - Status PV: '" + pv_name + "'"); + + if (is_principal_cert && pv_name != null && !pv_name.isBlank()) + status_pv_name = pv_name; + } + + // No way to check if there is peer info (certificates, principal, ...) // other then success vs. exception.. - final Principal principal = socket.getSession().getPeerPrincipal(); + final Principal principal = session.getPeerPrincipal(); String name = getPrincipalCN(principal); if (name == null) { @@ -309,6 +348,7 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti final TLSHandshakeInfo info = new TLSHandshakeInfo(); info.name = name; info.hostname = socket.getInetAddress().getHostName(); + info.status_pv_name = status_pv_name; return info; } diff --git a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java index 3e2642be64..f1ccdd9f52 100644 --- a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,6 +12,7 @@ import java.nio.ByteBuffer; import java.util.logging.Level; +import org.epics.pva.common.AccessRightsChange; import org.epics.pva.common.CommandHandler; import org.epics.pva.common.PVAHeader; import org.epics.pva.data.PVAStatus; @@ -20,7 +21,6 @@ /** Handle response to client's CREATE CHANNEL command * @author Kay Kasemir */ -@SuppressWarnings("nls") class CreateChannelHandler implements CommandHandler { @Override @@ -55,6 +55,13 @@ private void sendChannelCreated(final ServerTCPHandler tcp, final ServerPV pv, i { tcp.submit((version, buffer) -> { + // Send initial access rights with (before) the channel confirmation, + // so client knows permissions when channel is confirmed + final boolean writable = pv.isWritable(); + logger.log(Level.FINE, () -> "Send ACL " + pv + " [CID " + cid + "]" + (writable ? " writable" : " read-only")); + AccessRightsChange.encode(buffer, cid, writable); + + // Confirm channel creation logger.log(Level.FINE, () -> "Confirm channel creation " + pv + " [CID " + cid + "]"); PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, diff --git a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java index 300e1368a8..57a1f890ac 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java @@ -261,10 +261,7 @@ void register(final ServerTCPHandler tcp_connection) void shutdownConnection(final ServerTCPHandler tcp_connection) { for (ServerPV pv : pv_by_name.values()) - { pv.removeClient(tcp_connection, -1); - pv.unregisterSubscription(tcp_connection, -1); - } // If this is still a known handler, close it, but don't wait if (tcp_handlers.remove(tcp_connection)) diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java index 5f9c43f6eb..2f4eaa3a7d 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java @@ -26,7 +26,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ServerDemo { public static void main(String[] args) throws Exception @@ -97,6 +96,10 @@ public static void main(String[] args) throws Exception if (! ex.getMessage().toLowerCase().contains("incompatible")) throw ex; } + + write_pv.close(); + pv2.close(); + pv1.close(); } } } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerPV.java b/core/pva/src/main/java/org/epics/pva/server/ServerPV.java index 954fa85a0d..5329bc3c14 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerPV.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerPV.java @@ -10,12 +10,13 @@ import static org.epics.pva.PVASettings.logger; import java.util.BitSet; -import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap.KeySetView; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; +import org.epics.pva.common.AccessRightsChange; import org.epics.pva.common.PVAHeader; import org.epics.pva.data.PVAString; import org.epics.pva.data.PVAStructure; @@ -74,9 +75,12 @@ public class ServerPV implements AutoCloseable /** Handler for write access. May be READONLY_WRITE_HANDLER */ private final WriteEventHandler write_handler; - /** Map of TCP handlers, - * i.e. TCP connections to clients that access this PV, - * by client ID + /** Is the PV writable? */ + private final AtomicBoolean writable; + + /** Map of TCP handlers and client IDs. + * PV has one server ID. + * Client ID is provided by client for each TCP connection. */ private final ConcurrentHashMap cid_by_client = new ConcurrentHashMap<>(); @@ -96,6 +100,7 @@ public class ServerPV implements AutoCloseable this.data = data.cloneData(); rpc = DEFAULT_RPC_SERVICE; this.write_handler = write_handler; + writable = new AtomicBoolean(write_handler != READONLY_WRITE_HANDLER); } /** Create PV for handling RPC calls @@ -110,6 +115,7 @@ public class ServerPV implements AutoCloseable this.data = RPC_SERVICE_VALUE; this.rpc = rpc; write_handler = READONLY_WRITE_HANDLER; + writable = new AtomicBoolean(false); } @@ -131,7 +137,7 @@ public int getSID() */ void addClient(final ServerTCPHandler tcp, final int cid) { - // A client should create a PV just once. + // Each client should create a PV just once. // If client creates PV several times, we only track the last CID // and issue a warning. final Integer other = cid_by_client.put(tcp, cid); @@ -226,9 +232,26 @@ PVAStructure getData() } } - boolean isWritable() + /** @return Is the PV writable? */ + public boolean isWritable() + { + return writable.get(); + } + + /** Update write access + * + * To enable write access, PV must have been created with {@link WriteEventHandler} + * + * @param writable Should the PV be writable? + */ + public void setWritable(final boolean writable) { - return write_handler != READONLY_WRITE_HANDLER; + if (write_handler != READONLY_WRITE_HANDLER && this.writable.compareAndSet(!writable, writable)) + { + logger.log(Level.FINE, () -> "Update ACL " + this + (writable ? " to writable" : " to read-only")); + cid_by_client.forEach((tcp, cid) -> + tcp.submit((version, buffer) -> AccessRightsChange.encode(buffer, cid, writable))); + } } /** Notification that a client wrote to the PV @@ -256,18 +279,14 @@ PVAStructure call(final PVAStructure parameters) throws Exception @Override public void close() { - for (Entry client : cid_by_client.entrySet()) - { - final ServerTCPHandler tcp = client.getKey(); - final int cid = client.getValue(); + cid_by_client.forEach((tcp, cid) -> tcp.submit( (version, buffer) -> - { + { // Send CMD_DESTROY_CHANNEL for this PV to all clients logger.log(Level.FINE, () -> "Sending destroy channel command for SID " + sid + ", CID " + cid); PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_DESTROY_CHANNEL, 4+4); buffer.putInt(sid); buffer.putInt(cid); - }); - } + })); server.deletePV(this); } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java index 5ee135542c..ab9a00f534 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java @@ -34,7 +34,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") class ServerTCPListener { private final ExecutorService thread_pool = Executors.newCachedThreadPool(runnable -> @@ -222,12 +221,13 @@ private void listen() { // Check TLS final Socket client = tls_server_socket.accept(); TLSHandshakeInfo tls_info = null; - if (client instanceof SSLSocket) + if (client instanceof SSLSocket ssl_client) { logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TLS client " + client.getRemoteSocketAddress()); try { - tls_info = TLSHandshakeInfo.fromSocket((SSLSocket) client); + tls_info = TLSHandshakeInfo.fromSocket(ssl_client); + logger.log(Level.FINE, "Client TLS info: " + tls_info); } catch (SSLHandshakeException ssl) { diff --git a/core/pva/src/main/resources/pva_logging.properties b/core/pva/src/main/resources/pva_logging.properties index 9ec1ae1bc5..c61023d9f3 100644 --- a/core/pva/src/main/resources/pva_logging.properties +++ b/core/pva/src/main/resources/pva_logging.properties @@ -17,7 +17,7 @@ java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1t # java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s [%2$s] %5$s%6$s%n # General library logging -org.epics.pva.level = INFO +org.epics.pva.level = CONFIG # Proxy logging org.epics.pva.proxy.level = INFO \ No newline at end of file