mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-30 21:23:00 +00:00 
			
		
		
		
	Prevent sending too many websocket messages at once
This commit is contained in:
		| @@ -5,14 +5,13 @@ | |||||||
| package dan200.computercraft.core.apis.http.websocket; | package dan200.computercraft.core.apis.http.websocket; | ||||||
| 
 | 
 | ||||||
| import com.google.common.base.Strings; | import com.google.common.base.Strings; | ||||||
|  | import dan200.computercraft.api.lua.LuaException; | ||||||
| import dan200.computercraft.core.Logging; | import dan200.computercraft.core.Logging; | ||||||
| import dan200.computercraft.core.apis.IAPIEnvironment; | import dan200.computercraft.core.apis.IAPIEnvironment; | ||||||
| import dan200.computercraft.core.apis.http.HTTPRequestException; | import dan200.computercraft.core.apis.http.*; | ||||||
| import dan200.computercraft.core.apis.http.NetworkUtils; |  | ||||||
| import dan200.computercraft.core.apis.http.Resource; |  | ||||||
| import dan200.computercraft.core.apis.http.ResourceGroup; |  | ||||||
| import dan200.computercraft.core.apis.http.options.Options; | import dan200.computercraft.core.apis.http.options.Options; | ||||||
| import dan200.computercraft.core.metrics.Metrics; | import dan200.computercraft.core.metrics.Metrics; | ||||||
|  | import dan200.computercraft.core.util.AtomicHelpers; | ||||||
| import io.netty.bootstrap.Bootstrap; | import io.netty.bootstrap.Bootstrap; | ||||||
| import io.netty.buffer.Unpooled; | import io.netty.buffer.Unpooled; | ||||||
| import io.netty.channel.Channel; | import io.netty.channel.Channel; | ||||||
| @@ -24,10 +23,8 @@ import io.netty.handler.codec.http.HttpClientCodec; | |||||||
| import io.netty.handler.codec.http.HttpHeaderNames; | import io.netty.handler.codec.http.HttpHeaderNames; | ||||||
| import io.netty.handler.codec.http.HttpHeaders; | import io.netty.handler.codec.http.HttpHeaders; | ||||||
| import io.netty.handler.codec.http.HttpObjectAggregator; | import io.netty.handler.codec.http.HttpObjectAggregator; | ||||||
| import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; | import io.netty.handler.codec.http.websocketx.*; | ||||||
| import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; | import io.netty.util.concurrent.GenericFutureListener; | ||||||
| import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler; |  | ||||||
| import io.netty.handler.codec.http.websocketx.WebSocketVersion; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| 
 | 
 | ||||||
| @@ -35,6 +32,7 @@ import javax.annotation.Nullable; | |||||||
| import java.net.URI; | import java.net.URI; | ||||||
| import java.nio.ByteBuffer; | import java.nio.ByteBuffer; | ||||||
| import java.util.concurrent.Future; | import java.util.concurrent.Future; | ||||||
|  | import java.util.concurrent.atomic.AtomicInteger; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Provides functionality to verify and connect to a remote websocket. |  * Provides functionality to verify and connect to a remote websocket. | ||||||
| @@ -57,6 +55,9 @@ public class Websocket extends Resource<Websocket> implements WebsocketClient { | |||||||
|     private final HttpHeaders headers; |     private final HttpHeaders headers; | ||||||
|     private final int timeout; |     private final int timeout; | ||||||
| 
 | 
 | ||||||
|  |     private final AtomicInteger inFlight = new AtomicInteger(0); | ||||||
|  |     private final GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Void>> onSend = f -> inFlight.decrementAndGet(); | ||||||
|  | 
 | ||||||
|     public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { |     public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { | ||||||
|         super(limiter); |         super(limiter); | ||||||
|         this.environment = environment; |         this.environment = environment; | ||||||
| @@ -170,18 +171,27 @@ public class Websocket extends Resource<Websocket> implements WebsocketClient { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void sendText(String message) { |     public void sendText(String message) throws LuaException { | ||||||
|         environment.observe(Metrics.WEBSOCKET_OUTGOING, message.length()); |         sendMessage(new TextWebSocketFrame(message), message.length()); | ||||||
| 
 |  | ||||||
|         var channel = channel(); |  | ||||||
|         if (channel != null) channel.writeAndFlush(new TextWebSocketFrame(message)); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void sendBinary(ByteBuffer message) { |     public void sendBinary(ByteBuffer message) throws LuaException { | ||||||
|         environment.observe(Metrics.WEBSOCKET_OUTGOING, message.remaining()); |         long size = message.remaining(); | ||||||
|  |         sendMessage(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)), size); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  |     private void sendMessage(WebSocketFrame frame, long size) throws LuaException { | ||||||
|         var channel = channel(); |         var channel = channel(); | ||||||
|         if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message))); |         if (channel == null) return; | ||||||
|  | 
 | ||||||
|  |         // Grow the number of in-flight requests, aborting if we've hit the limit. This is then decremented when the | ||||||
|  |         // promise finishes. | ||||||
|  |         if (!AtomicHelpers.incrementToLimit(inFlight, ResourceQueue.DEFAULT_LIMIT)) { | ||||||
|  |             throw new LuaException("Too many ongoing websocket messages"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         environment.observe(Metrics.WEBSOCKET_OUTGOING, size); | ||||||
|  |         channel.writeAndFlush(frame).addListener(onSend); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| 
 | 
 | ||||||
| package dan200.computercraft.core.apis.http.websocket; | package dan200.computercraft.core.apis.http.websocket; | ||||||
| 
 | 
 | ||||||
|  | import dan200.computercraft.api.lua.LuaException; | ||||||
| import dan200.computercraft.core.apis.http.HTTPRequestException; | import dan200.computercraft.core.apis.http.HTTPRequestException; | ||||||
| 
 | 
 | ||||||
| import java.io.Closeable; | import java.io.Closeable; | ||||||
| @@ -39,15 +40,17 @@ public interface WebsocketClient extends Closeable { | |||||||
|      * Send a text websocket frame. |      * Send a text websocket frame. | ||||||
|      * |      * | ||||||
|      * @param message The message to send. |      * @param message The message to send. | ||||||
|  |      * @throws LuaException If the message could not be sent. | ||||||
|      */ |      */ | ||||||
|     void sendText(String message); |     void sendText(String message) throws LuaException; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Send a binary websocket frame. |      * Send a binary websocket frame. | ||||||
|      * |      * | ||||||
|      * @param message The message to send. |      * @param message The message to send. | ||||||
|  |      * @throws LuaException If the message could not be sent. | ||||||
|      */ |      */ | ||||||
|     void sendBinary(ByteBuffer message); |     void sendBinary(ByteBuffer message) throws LuaException; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Parse an address, ensuring it is a valid websocket URI. |      * Parse an address, ensuring it is a valid websocket URI. | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||||
|  | // | ||||||
|  | // SPDX-License-Identifier: MPL-2.0 | ||||||
|  | 
 | ||||||
|  | package dan200.computercraft.core.util; | ||||||
|  | 
 | ||||||
|  | import java.util.concurrent.atomic.AtomicInteger; | ||||||
|  | 
 | ||||||
|  | public final class AtomicHelpers { | ||||||
|  |     private AtomicHelpers() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A version of {@link AtomicInteger#getAndIncrement()}, which increments until a limit is reached. | ||||||
|  |      * | ||||||
|  |      * @param atomic The atomic to increment. | ||||||
|  |      * @param limit  The maximum value of {@code value}. | ||||||
|  |      * @return Whether the value was sucessfully incremented. | ||||||
|  |      */ | ||||||
|  |     public static boolean incrementToLimit(AtomicInteger atomic, int limit) { | ||||||
|  |         int value; | ||||||
|  |         do { | ||||||
|  |             value = atomic.get(); | ||||||
|  |             if (value >= limit) return false; | ||||||
|  |         } while (!atomic.compareAndSet(value, value + 1)); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -89,6 +89,30 @@ class TestHttpApi { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Test | ||||||
|  |     fun `Errors if too many websocket messages are sent`() { | ||||||
|  |         runServer { | ||||||
|  |             LuaTaskRunner.runTest { | ||||||
|  |                 val httpApi = addApi(HTTPAPI(environment)) | ||||||
|  |                 assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), 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 | ||||||
|  |                 val error = assertThrows<LuaException> { | ||||||
|  |                     for (i in 0 until 10_000) { | ||||||
|  |                         websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 websocket.close() | ||||||
|  | 
 | ||||||
|  |                 assertThat(error.message, equalTo("Too many ongoing websocket messages")) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun `Queues an event when the socket is externally closed`() { |     fun `Queues an event when the socket is externally closed`() { | ||||||
|         runServer { stop -> |         runServer { stop -> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates