From 3c16783839dda3e8a702faba909978d801fab998 Mon Sep 17 00:00:00 2001 From: "slominskir-coding-agent[bot]" <335168828+slominskir-coding-agent[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:02:52 -0400 Subject: [PATCH] Read clientName among other query parameters The WebSocket endpoint read the client's name by splitting the whole query string on "=", and kept it only if that gave exactly two parts, without checking the parameter's name. A client that sent any other parameter lost its name, and a malformed escape threw out of onOpen. It now finds the clientName parameter among the others, and a malformed one gives an empty name. epics2web.js appended "?clientName=" even to a url option that already had a query string; it now appends with "&" then. Co-Authored-By: Claude Opus 5.5 --- .../org/jlab/epics2web/ClientNameTest.java | 60 +++++++++++++++++++ .../epics2web/websocket/MonitorEndpoint.java | 49 +++++++++------ src/main/webapp/resources/js/epics2web.js | 2 +- .../websocket/MonitorEndpointTest.java | 29 +++++++++ 4 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 src/integration/java/org/jlab/epics2web/ClientNameTest.java diff --git a/src/integration/java/org/jlab/epics2web/ClientNameTest.java b/src/integration/java/org/jlab/epics2web/ClientNameTest.java new file mode 100644 index 0000000..44bcfcc --- /dev/null +++ b/src/integration/java/org/jlab/epics2web/ClientNameTest.java @@ -0,0 +1,60 @@ +package org.jlab.epics2web; + +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.WebSocket; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import org.junit.Test; + +/** Tests that clients are named by the clientName parameter of the WebSocket handshake. */ +public class ClientNameTest { + + private static final HttpClient HTTP = HttpClient.newHttpClient(); + + /** The name is read even when the query has other parameters, and may contain = and &. */ + @Test + public void consoleShowsClientNameAmongOtherParameters() throws Exception { + String name = "name-test-" + UUID.randomUUID() + "?a=1&b=2"; + WebSocket socket = + HTTP.newWebSocketBuilder() + .buildAsync( + URI.create( + "ws://localhost:8080/epics2web/monitor?other=1&clientName=" + + URLEncoder.encode(name, StandardCharsets.UTF_8) + + "&another=2"), + new WebSocket.Listener() {}) + .join(); + + try { + assertTrue("Client name not on the console", consoleShows(name.replace("&", "&"))); + } finally { + socket.abort(); + } + } + + /** Waits for the console to show the text; the server may list the client after the handshake. */ + private static boolean consoleShows(String text) throws Exception { + long deadline = System.currentTimeMillis() + 5_000; + while (true) { + if (get("console").body().contains("" + text + "")) { + return true; + } + if (System.currentTimeMillis() > deadline) { + return false; + } + Thread.sleep(100); + } + } + + private static HttpResponse get(String path) throws Exception { + return HTTP.send( + HttpRequest.newBuilder().uri(URI.create("http://localhost:8080/epics2web/" + path)).build(), + HttpResponse.BodyHandlers.ofString()); + } +} diff --git a/src/main/java/org/jlab/epics2web/websocket/MonitorEndpoint.java b/src/main/java/org/jlab/epics2web/websocket/MonitorEndpoint.java index 9104401..6c42e7c 100644 --- a/src/main/java/org/jlab/epics2web/websocket/MonitorEndpoint.java +++ b/src/main/java/org/jlab/epics2web/websocket/MonitorEndpoint.java @@ -14,8 +14,8 @@ import jakarta.websocket.Session; import jakarta.websocket.server.ServerEndpoint; import java.io.StringReader; -import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; import java.util.Map; @@ -77,24 +77,7 @@ public void onOpen(Session session, EndpointConfig config) { } } - String name = ""; - String q = session.getQueryString(); - if (q != null) { - String[] tokens = q.split("="); - if (tokens.length == 2) { - try { - name = URLDecoder.decode(tokens[1], "UTF-8"); - } catch (UnsupportedEncodingException e) { - LOGGER.log( - Level.WARNING, - "JVM doesn't support UTF-8 so can't decode clientName parameter", - e); - } - } - if (name == null) { - name = ""; - } - } + String name = clientName(session.getQueryString()); session.getUserProperties().put("agent", agent); session.getUserProperties().put("ip", ip); @@ -126,6 +109,34 @@ public void onOpen(Session session, EndpointConfig config) { } } + /** + * Read the clientName parameter from a handshake's query string. The query may have other + * parameters, in any order. + * + * @param query The raw query string, or null + * @return The decoded name, or empty if there's none or it's malformed + */ + static String clientName(String query) { + if (query == null) { + return ""; + } + + for (String parameter : query.split("&")) { + int equals = parameter.indexOf('='); + + if (equals > 0 && "clientName".equals(parameter.substring(0, equals))) { + try { + return URLDecoder.decode(parameter.substring(equals + 1), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.FINE, "Malformed clientName parameter: {0}", parameter); + return ""; + } + } + } + + return ""; + } + @OnClose public void onClose(Session session, CloseReason reason) { // LOGGER.log(Level.FINEST, "close; Reason: {0}", reason); diff --git a/src/main/webapp/resources/js/epics2web.js b/src/main/webapp/resources/js/epics2web.js index caa1060..f8f796c 100644 --- a/src/main/webapp/resources/js/epics2web.js +++ b/src/main/webapp/resources/js/epics2web.js @@ -138,7 +138,7 @@ jlab.epics2web.ClientConnection = function (options) { let u = this.url; if(this.clientName !== null) { - u = u + '?clientName=' + encodeURIComponent(this.clientName); + u = u + (u.indexOf('?') === -1 ? '?' : '&') + 'clientName=' + encodeURIComponent(this.clientName); } socket = new WebSocket(u); diff --git a/src/test/java/org/jlab/epics2web/websocket/MonitorEndpointTest.java b/src/test/java/org/jlab/epics2web/websocket/MonitorEndpointTest.java index 6bd9a28..d02d771 100644 --- a/src/test/java/org/jlab/epics2web/websocket/MonitorEndpointTest.java +++ b/src/test/java/org/jlab/epics2web/websocket/MonitorEndpointTest.java @@ -1,5 +1,6 @@ package org.jlab.epics2web.websocket; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -55,4 +56,32 @@ public void onCloseStopsWriterWithoutInterrupting() throws Exception { assertNull("Write queue not closed", client.writeQueue.take()); assertTrue(manager.toSet().isEmpty()); } + + @Test + public void clientNameIsReadAmongOtherParameters() { + assertEquals("screen", MonitorEndpoint.clientName("clientName=screen")); + assertEquals("screen", MonitorEndpoint.clientName("a=1&clientName=screen")); + assertEquals("screen", MonitorEndpoint.clientName("clientName=screen&a=1&b")); + assertEquals("screen", MonitorEndpoint.clientName("a&clientName=screen")); + } + + /** The JavaScript client sends its page's address, which may have a query string of its own. */ + @Test + public void clientNameIsDecoded() { + assertEquals( + "https://example.org/wedm/screen?edl=a b.edl&x=1", + MonitorEndpoint.clientName( + "clientName=https%3A%2F%2Fexample.org%2Fwedm%2Fscreen%3Fedl%3Da%20b.edl%26x%3D1")); + } + + @Test + public void clientNameIsEmptyWhenMissingOrMalformed() { + assertEquals("", MonitorEndpoint.clientName(null)); + assertEquals("", MonitorEndpoint.clientName("")); + assertEquals("", MonitorEndpoint.clientName("a=1")); + assertEquals("", MonitorEndpoint.clientName("xclientName=screen")); + assertEquals("", MonitorEndpoint.clientName("clientName")); + assertEquals("", MonitorEndpoint.clientName("clientName=")); + assertEquals("", MonitorEndpoint.clientName("clientName=%zz")); + } }