From ac804438306641603f138bb023aba543be1c0f73 Mon Sep 17 00:00:00 2001 From: MCJack123 Date: Sun, 1 Oct 2023 19:41:55 -0400 Subject: [PATCH 1/3] Added binary option to WebSockets --- .../computercraft/core/apis/HTTPAPI.java | 5 +++- .../core/apis/http/websocket/Websocket.java | 9 ++++++- .../apis/http/websocket/WebsocketClient.java | 9 +++++++ .../apis/http/websocket/WebsocketHandle.java | 15 ++++++++++- .../apis/http/websocket/WebsocketHandler.java | 12 ++++----- .../computercraft/lua/rom/apis/http/http.lua | 11 +++++--- .../core/apis/http/TestHttpApi.kt | 26 +++++++++++++++++++ 7 files changed, 74 insertions(+), 13 deletions(-) diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index a97c03a065..b0b25f841b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -150,16 +150,19 @@ public final Object[] websocket(IArguments args) throws LuaException { String address; Map headerTable; Optional timeoutArg; + boolean binary; if (args.get(0) instanceof Map) { var options = args.getTable(0); address = getStringField(options, "url"); headerTable = optTableField(options, "headers", Collections.emptyMap()); timeoutArg = optRealField(options, "timeout"); + binary = optBooleanField(options, "binary", false); } else { address = args.getString(0); headerTable = args.optTable(1, Collections.emptyMap()); timeoutArg = Optional.empty(); + binary = args.optBoolean(2, false); } var headers = getHeaders(headerTable); @@ -167,7 +170,7 @@ public final Object[] websocket(IArguments args) throws LuaException { try { var uri = WebsocketClient.parseUri(address); - if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) { + if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout, binary).queue(Websocket::connect)) { throw new LuaException("Too many websockets already open"); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index e2ff84c0cd..bc1682d74a 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -56,14 +56,16 @@ public class Websocket extends Resource implements WebsocketClient { private final String address; private final HttpHeaders headers; private final int timeout; + private final boolean binary; - public Websocket(ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { + public Websocket(ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout, boolean binary) { super(limiter); this.environment = environment; this.uri = uri; this.address = address; this.headers = headers; this.timeout = timeout; + this.binary = binary; } public void connect() { @@ -184,4 +186,9 @@ public void sendBinary(ByteBuffer message) { var channel = channel(); if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message))); } + + @Override + public boolean isBinary() { + return binary; + } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java index 02cf3f43cb..70bcac3b6d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java @@ -49,6 +49,15 @@ public interface WebsocketClient extends Closeable { */ void sendBinary(ByteBuffer message); + /** + * Determine whether the websocket sends binary messages by default. + * + * @return Whether the websocket sends binary messages by default. + */ + default boolean isBinary() { + return false; + } + /** * Parse an address, ensuring it is a valid websocket URI. * diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index ab4932d5c4..05ec143ae0 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -9,6 +9,8 @@ import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.options.Options; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; import java.util.Arrays; import java.util.Optional; @@ -79,7 +81,18 @@ public final void send(Coerced message, Optional binary) throws if (binary.orElse(false)) { websocket.sendBinary(LuaValues.encode(text)); } else { - websocket.sendText(text); + var data = text; + if (websocket.isBinary()) { + // Try to convert the string from UTF-8 bytes to UTF-16 codepoints. + // If this fails, fall back to normal ANSI. + try { + var buf = LuaValues.encode(text); + var bytes = new byte[buf.capacity()]; + buf.get(bytes); + data = new String(bytes, StandardCharsets.UTF_8); + } catch (UnsupportedCharsetException ignored) {} + } + websocket.sendText(data); } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 8fe6fa2442..68c5a9382b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -50,16 +50,16 @@ public void channelRead0(ChannelHandlerContext ctx, Object msg) { } var frame = (WebSocketFrame) msg; - if (frame instanceof TextWebSocketFrame textFrame) { + if (websocket.isBinary() || (frame instanceof BinaryWebSocketFrame)) { + var converted = NetworkUtils.toBytes(frame.content()); + + websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length); + websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, frame instanceof BinaryWebSocketFrame); + } else if (frame instanceof TextWebSocketFrame textFrame) { var data = textFrame.text(); websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length()); websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, false); - } else if (frame instanceof BinaryWebSocketFrame) { - var converted = NetworkUtils.toBytes(frame.content()); - - websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length); - websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true); } else if (frame instanceof CloseWebSocketFrame closeFrame) { websocket.close(closeFrame.statusCode(), closeFrame.reasonText()); } diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua index 9e074a4753..b499434a23 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua @@ -275,6 +275,7 @@ local function check_websocket_options(options, body) check_key(options, "url", "string") check_key(options, "headers", "table", true) check_key(options, "timeout", "number", true) + check_key(options, "binary", "boolean", true) end @@ -299,7 +300,7 @@ these options behave. @see websocket_success @see websocket_failure ]] -function websocketAsync(url, headers) +function websocketAsync(url, headers, binary) local actual_url if type(url) == "table" then check_websocket_options(url) @@ -307,10 +308,11 @@ function websocketAsync(url, headers) else expect(1, url, "string") expect(2, headers, "table", "nil") + expect(3, binary, "boolean", "nil") actual_url = url end - local ok, err = nativeWebsocket(url, headers) + local ok, err = nativeWebsocket(url, headers, binary) if not ok then os.queueEvent("websocket_failure", actual_url, err) end @@ -355,7 +357,7 @@ from above are passed in as fields instead (for instance, ws.close() ]] -function websocket(url, headers) +function websocket(url, headers, binary) local actual_url if type(url) == "table" then check_websocket_options(url) @@ -363,10 +365,11 @@ function websocket(url, headers) else expect(1, url, "string") expect(2, headers, "table", "nil") + expect(3, binary, "boolean", "nil") actual_url = url end - local ok, err = nativeWebsocket(url, headers) + local ok, err = nativeWebsocket(url, headers, binary) if not ok then return ok, err end while true do diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt index 13145d6db7..4a28d5fb97 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt @@ -88,6 +88,32 @@ class TestHttpApi { } } + @Test + fun `Supports binary websockets`() { + runServer { + LuaTaskRunner.runTest { + val httpApi = addApi(HTTPAPI(environment)) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL, null, true)), array(equalTo(true))) + + val connectEvent = pullEvent() + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java))) + + val websocket = connectEvent[2] as WebsocketHandle + websocket.send(Coerced("Hello \u00E2\u0098\u00BA"), Optional.of(false)) + + val message = websocket.receive(Optional.empty()).await() + // The string is converted to bytes because it's technically sent as a byte array. + // This difference doesn't matter in Lua, but it does here. + assertThat("Received a return message", message, array(equalTo("HELLO \u263A".toByteArray()), equalTo(false))) + + websocket.close() + + val closeEvent = pullEventOrTimeout(500.milliseconds, "websocket_closed") + assertThat("No event was queued", closeEvent, equalTo(null)) + } + } + } + @Test fun `Queues an event when the socket is externally closed`() { runServer { stop -> From a8f764844a0018440ccacb1f497406f6d731fe5b Mon Sep 17 00:00:00 2001 From: MCJack123 Date: Sun, 1 Oct 2023 20:10:56 -0400 Subject: [PATCH 2/3] Fixed CI --- .../core/apis/http/websocket/WebsocketClient.java | 2 +- .../core/apis/http/websocket/WebsocketHandle.java | 5 ++++- .../core/apis/http/websocket/WebsocketHandler.java | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java index 70bcac3b6d..4a7bf8378d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java @@ -51,7 +51,7 @@ public interface WebsocketClient extends Closeable { /** * Determine whether the websocket sends binary messages by default. - * + * * @return Whether the websocket sends binary messages by default. */ default boolean isBinary() { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index 05ec143ae0..70830de9a9 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -90,7 +90,10 @@ public final void send(Coerced message, Optional binary) throws var bytes = new byte[buf.capacity()]; buf.get(bytes); data = new String(bytes, StandardCharsets.UTF_8); - } catch (UnsupportedCharsetException ignored) {} + } catch (UnsupportedCharsetException ignored) { + // Suppress warnings. + data = text; + } } websocket.sendText(data); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 68c5a9382b..bc4e997931 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -50,7 +50,7 @@ public void channelRead0(ChannelHandlerContext ctx, Object msg) { } var frame = (WebSocketFrame) msg; - if (websocket.isBinary() || (frame instanceof BinaryWebSocketFrame)) { + if (websocket.isBinary() || frame instanceof BinaryWebSocketFrame) { var converted = NetworkUtils.toBytes(frame.content()); websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length); From b99a2e517a39d08c6eefbf206fb418b631099d9e Mon Sep 17 00:00:00 2001 From: MCJack123 Date: Thu, 5 Oct 2023 16:40:11 -0400 Subject: [PATCH 3/3] Fixed CI --- .../core/apis/http/websocket/TWebsocket.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java index 6e8c42123c..c8c357488b 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java @@ -27,14 +27,16 @@ public class TWebsocket extends Resource implements WebsocketClient private final IAPIEnvironment environment; private final URI uri; private final String address; + private final boolean binary; private @Nullable WebSocket websocket; - public TWebsocket(ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { + public TWebsocket(ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout, boolean binary) { super(limiter); this.environment = environment; this.uri = uri; this.address = address; + this.binary = binary; } public void connect() { @@ -76,6 +78,11 @@ public void sendBinary(ByteBuffer message) { websocket.send(array); } + @Override + public boolean isBinary() { + return binary; + } + @Override protected void dispose() { super.dispose();