Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/integration/java/org/jlab/epics2web/ClientNameTest.java
Original file line number Diff line number Diff line change
@@ -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("<td>" + text + "</td>")) {
return true;
}
if (System.currentTimeMillis() > deadline) {
return false;
}
Thread.sleep(100);
}
}

private static HttpResponse<String> get(String path) throws Exception {
return HTTP.send(
HttpRequest.newBuilder().uri(URI.create("http://localhost:8080/epics2web/" + path)).build(),
HttpResponse.BodyHandlers.ofString());
}
}
49 changes: 30 additions & 19 deletions src/main/java/org/jlab/epics2web/websocket/MonitorEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/resources/js/epics2web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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"));
}
}
Loading