mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +00:00 
			
		
		
		
	Add support for HTTP timeouts (#1453)
- Add a `timeout` parameter to http request and websocket methods.
    - For requests, this sets the connection and read timeout.
    - For websockets, this sets the connection and handshake timeout.
 - Remove the timeout config option, as this is now specified by user
   code.
 - Use netty for handling websocket handshakes, meaning we no longer
   need to deal with pongs.
			
			
This commit is contained in:
		| @@ -32,9 +32,6 @@ class AddressRuleConfig { | ||||
|         config.add("action", action.name().toLowerCase(Locale.ROOT)); | ||||
| 
 | ||||
|         if (host.equals("*") && action == Action.ALLOW) { | ||||
|             config.setComment("timeout", "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited."); | ||||
|             config.add("timeout", AddressRule.TIMEOUT); | ||||
| 
 | ||||
|             config.setComment("max_download", """ | ||||
|                 The maximum size (in bytes) that a computer can download in a single request. | ||||
|                 Note that responses may receive more data than allowed, but this data will not | ||||
| @@ -58,7 +55,6 @@ class AddressRuleConfig { | ||||
|         var port = unboxOptInt(get(builder, "port", Number.class)); | ||||
|         return hostObj != null && checkEnum(builder, "action", Action.class) | ||||
|             && check(builder, "port", Number.class) | ||||
|             && check(builder, "timeout", Number.class) | ||||
|             && check(builder, "max_upload", Number.class) | ||||
|             && check(builder, "max_download", Number.class) | ||||
|             && check(builder, "websocket_message", Number.class) | ||||
| @@ -72,7 +68,6 @@ class AddressRuleConfig { | ||||
| 
 | ||||
|         var action = getEnum(builder, "action", Action.class).orElse(null); | ||||
|         var port = unboxOptInt(get(builder, "port", Number.class)); | ||||
|         var timeout = unboxOptInt(get(builder, "timeout", Number.class)); | ||||
|         var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue)); | ||||
|         var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue)); | ||||
|         var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue)); | ||||
| @@ -81,7 +76,6 @@ class AddressRuleConfig { | ||||
|             action, | ||||
|             maxUpload, | ||||
|             maxDownload, | ||||
|             timeout, | ||||
|             websocketMessage | ||||
|         ); | ||||
| 
 | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import java.util.Map; | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| import static dan200.computercraft.core.apis.TableHelper.*; | ||||
| import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; | ||||
| 
 | ||||
| /** | ||||
|  * Placeholder description, please ignore. | ||||
| @@ -31,6 +32,9 @@ import static dan200.computercraft.core.apis.TableHelper.*; | ||||
|  * @hidden | ||||
|  */ | ||||
| public class HTTPAPI implements ILuaAPI { | ||||
|     private static final double DEFAULT_TIMEOUT = 30; | ||||
|     private static final double MAX_TIMEOUT = 60; | ||||
| 
 | ||||
|     private final IAPIEnvironment apiEnvironment; | ||||
| 
 | ||||
|     private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>(() -> ResourceGroup.DEFAULT_LIMIT); | ||||
| @@ -72,6 +76,7 @@ public class HTTPAPI implements ILuaAPI { | ||||
|         String address, postString, requestMethod; | ||||
|         Map<?, ?> headerTable; | ||||
|         boolean binary, redirect; | ||||
|         Optional<Double> timeoutArg; | ||||
| 
 | ||||
|         if (args.get(0) instanceof Map) { | ||||
|             var options = args.getTable(0); | ||||
| @@ -81,7 +86,7 @@ public class HTTPAPI implements ILuaAPI { | ||||
|             binary = optBooleanField(options, "binary", false); | ||||
|             requestMethod = optStringField(options, "method", null); | ||||
|             redirect = optBooleanField(options, "redirect", true); | ||||
| 
 | ||||
|             timeoutArg = optRealField(options, "timeout"); | ||||
|         } else { | ||||
|             // Get URL and post information | ||||
|             address = args.getString(0); | ||||
| @@ -90,9 +95,11 @@ public class HTTPAPI implements ILuaAPI { | ||||
|             binary = args.optBoolean(3, false); | ||||
|             requestMethod = null; | ||||
|             redirect = true; | ||||
|             timeoutArg = Optional.empty(); | ||||
|         } | ||||
| 
 | ||||
|         var headers = getHeaders(headerTable); | ||||
|         var timeout = getTimeout(timeoutArg); | ||||
| 
 | ||||
|         HttpMethod httpMethod; | ||||
|         if (requestMethod == null) { | ||||
| @@ -106,7 +113,7 @@ public class HTTPAPI implements ILuaAPI { | ||||
| 
 | ||||
|         try { | ||||
|             var uri = HttpRequest.checkUri(address); | ||||
|             var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect); | ||||
|             var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect, timeout); | ||||
| 
 | ||||
|             // Make the request | ||||
|             if (!request.queue(r -> r.request(uri, httpMethod))) { | ||||
| @@ -134,16 +141,32 @@ public class HTTPAPI implements ILuaAPI { | ||||
|     } | ||||
| 
 | ||||
|     @LuaFunction | ||||
|     public final Object[] websocket(String address, Optional<Map<?, ?>> headerTbl) throws LuaException { | ||||
|     public final Object[] websocket(IArguments args) throws LuaException { | ||||
|         if (!CoreConfig.httpWebsocketEnabled) { | ||||
|             throw new LuaException("Websocket connections are disabled"); | ||||
|         } | ||||
| 
 | ||||
|         var headers = getHeaders(headerTbl.orElse(Collections.emptyMap())); | ||||
|         String address; | ||||
|         Map<?, ?> headerTable; | ||||
|         Optional<Double> timeoutArg; | ||||
| 
 | ||||
|         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"); | ||||
|         } else { | ||||
|             address = args.getString(0); | ||||
|             headerTable = args.optTable(1, Collections.emptyMap()); | ||||
|             timeoutArg = Optional.empty(); | ||||
|         } | ||||
| 
 | ||||
|         var headers = getHeaders(headerTable); | ||||
|         var timeout = getTimeout(timeoutArg); | ||||
| 
 | ||||
|         try { | ||||
|             var uri = Websocket.checkUri(address); | ||||
|             if (!new Websocket(websockets, apiEnvironment, uri, address, headers).queue(Websocket::connect)) { | ||||
|             if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) { | ||||
|                 throw new LuaException("Too many websockets already open"); | ||||
|             } | ||||
| 
 | ||||
| @@ -171,4 +194,17 @@ public class HTTPAPI implements ILuaAPI { | ||||
|         } | ||||
|         return headers; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse the timeout value, asserting it is in range. | ||||
|      * | ||||
|      * @param timeoutArg The (optional) timeout, in seconds. | ||||
|      * @return The parsed timeout value, in milliseconds. | ||||
|      * @throws LuaException If the timeout is in-range. | ||||
|      */ | ||||
|     private static int getTimeout(Optional<Double> timeoutArg) throws LuaException { | ||||
|         double timeout = timeoutArg.orElse(DEFAULT_TIMEOUT); | ||||
|         assertBetween(timeout, 0, MAX_TIMEOUT, "timeout out of range (%s)"); | ||||
|         return (int) (timeout * 1000); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import dan200.computercraft.api.lua.LuaValues; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.Map; | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| import static dan200.computercraft.api.lua.LuaValues.getNumericType; | ||||
| 
 | ||||
| @@ -100,6 +101,15 @@ public final class TableHelper { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static Optional<Double> optRealField(Map<?, ?> table, String key) throws LuaException { | ||||
|         var value = table.get(key); | ||||
|         if(value == null) { | ||||
|             return Optional.empty(); | ||||
|         } else { | ||||
|             return Optional.of(getRealField(table, key)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static double optRealField(Map<?, ?> table, String key, double def) throws LuaException { | ||||
|         return checkReal(key, optNumberField(table, key, def)); | ||||
|     } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import io.netty.buffer.ByteBuf; | ||||
| import io.netty.channel.ConnectTimeoutException; | ||||
| import io.netty.channel.EventLoopGroup; | ||||
| import io.netty.channel.nio.NioEventLoopGroup; | ||||
| import io.netty.channel.socket.SocketChannel; | ||||
| import io.netty.handler.codec.DecoderException; | ||||
| import io.netty.handler.codec.TooLongFrameException; | ||||
| import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; | ||||
| @@ -50,7 +51,7 @@ public final class NetworkUtils { | ||||
|         .build() | ||||
|     ); | ||||
| 
 | ||||
|     public static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler( | ||||
|     private static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler( | ||||
|         EXECUTOR, CoreConfig.httpUploadBandwidth, CoreConfig.httpDownloadBandwidth | ||||
|     ); | ||||
| 
 | ||||
| @@ -141,6 +142,30 @@ public final class NetworkUtils { | ||||
|         return options; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set up some basic properties of the channel. This adds a timeout, the traffic shaping handler, and the SSL | ||||
|      * handler. | ||||
|      * | ||||
|      * @param ch            The channel to initialise. | ||||
|      * @param uri           The URI to connect to. | ||||
|      * @param socketAddress The address of the socket to connect to. | ||||
|      * @param sslContext    The SSL context, if present. | ||||
|      * @param timeout       The timeout on this channel. | ||||
|      * @see io.netty.channel.ChannelInitializer | ||||
|      */ | ||||
|     public static void initChannel(SocketChannel ch, URI uri, InetSocketAddress socketAddress, @Nullable SslContext sslContext, int timeout) { | ||||
|         if (timeout > 0) ch.config().setConnectTimeoutMillis(timeout); | ||||
| 
 | ||||
|         var p = ch.pipeline(); | ||||
|         p.addLast(SHAPING_HANDLER); | ||||
| 
 | ||||
|         if (sslContext != null) { | ||||
|             var handler = sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort()); | ||||
|             if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout); | ||||
|             p.addLast(handler); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read a {@link ByteBuf} into a byte array. | ||||
|      * | ||||
|   | ||||
| @@ -12,7 +12,7 @@ public enum Action { | ||||
|     DENY; | ||||
| 
 | ||||
|     private final PartialOptions partial = new PartialOptions( | ||||
|         this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty() | ||||
|         this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty() | ||||
|     ); | ||||
| 
 | ||||
|     public PartialOptions toPartial() { | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import java.util.regex.Pattern; | ||||
| public final class AddressRule { | ||||
|     public static final long MAX_DOWNLOAD = 16 * 1024 * 1024; | ||||
|     public static final long MAX_UPLOAD = 4 * 1024 * 1024; | ||||
|     public static final int TIMEOUT = 30_000; | ||||
|     public static final int WEBSOCKET_MESSAGE = 128 * 1024; | ||||
| 
 | ||||
|     private final AddressPredicate predicate; | ||||
|   | ||||
| @@ -12,14 +12,12 @@ public final class Options { | ||||
|     public final Action action; | ||||
|     public final long maxUpload; | ||||
|     public final long maxDownload; | ||||
|     public final int timeout; | ||||
|     public final int websocketMessage; | ||||
| 
 | ||||
|     Options(Action action, long maxUpload, long maxDownload, int timeout, int websocketMessage) { | ||||
|     Options(Action action, long maxUpload, long maxDownload, int websocketMessage) { | ||||
|         this.action = action; | ||||
|         this.maxUpload = maxUpload; | ||||
|         this.maxDownload = maxDownload; | ||||
|         this.timeout = timeout; | ||||
|         this.websocketMessage = websocketMessage; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,23 +13,21 @@ import java.util.OptionalLong; | ||||
| @Immutable | ||||
| public final class PartialOptions { | ||||
|     public static final PartialOptions DEFAULT = new PartialOptions( | ||||
|         null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty() | ||||
|         null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty() | ||||
|     ); | ||||
| 
 | ||||
|     private final @Nullable Action action; | ||||
|     private final OptionalLong maxUpload; | ||||
|     private final OptionalLong maxDownload; | ||||
|     private final OptionalInt timeout; | ||||
|     private final OptionalInt websocketMessage; | ||||
| 
 | ||||
|     @SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API | ||||
|     private @Nullable Options options; | ||||
| 
 | ||||
|     public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt timeout, OptionalInt websocketMessage) { | ||||
|     public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage) { | ||||
|         this.action = action; | ||||
|         this.maxUpload = maxUpload; | ||||
|         this.maxDownload = maxDownload; | ||||
|         this.timeout = timeout; | ||||
|         this.websocketMessage = websocketMessage; | ||||
|     } | ||||
| 
 | ||||
| @@ -40,7 +38,6 @@ public final class PartialOptions { | ||||
|             action == null ? Action.DENY : action, | ||||
|             maxUpload.orElse(AddressRule.MAX_UPLOAD), | ||||
|             maxDownload.orElse(AddressRule.MAX_DOWNLOAD), | ||||
|             timeout.orElse(AddressRule.TIMEOUT), | ||||
|             websocketMessage.orElse(AddressRule.WEBSOCKET_MESSAGE) | ||||
|         ); | ||||
|     } | ||||
| @@ -59,7 +56,6 @@ public final class PartialOptions { | ||||
|             action == null && other.action != null ? other.action : action, | ||||
|             maxUpload.isPresent() ? maxUpload : other.maxUpload, | ||||
|             maxDownload.isPresent() ? maxDownload : other.maxDownload, | ||||
|             timeout.isPresent() ? timeout : other.timeout, | ||||
|             websocketMessage.isPresent() ? websocketMessage : other.websocketMessage | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -52,12 +52,13 @@ public class HttpRequest extends Resource<HttpRequest> { | ||||
|     private final ByteBuf postBuffer; | ||||
|     private final HttpHeaders headers; | ||||
|     private final boolean binary; | ||||
|     private final int timeout; | ||||
| 
 | ||||
|     final AtomicInteger redirects; | ||||
| 
 | ||||
|     public HttpRequest( | ||||
|         ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText, | ||||
|         HttpHeaders headers, boolean binary, boolean followRedirects | ||||
|         HttpHeaders headers, boolean binary, boolean followRedirects, int timeout | ||||
|     ) { | ||||
|         super(limiter); | ||||
|         this.environment = environment; | ||||
| @@ -68,6 +69,7 @@ public class HttpRequest extends Resource<HttpRequest> { | ||||
|         this.headers = headers; | ||||
|         this.binary = binary; | ||||
|         redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0); | ||||
|         this.timeout = timeout; | ||||
| 
 | ||||
|         if (postText != null) { | ||||
|             if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { | ||||
| @@ -143,20 +145,10 @@ public class HttpRequest extends Resource<HttpRequest> { | ||||
|                 .handler(new ChannelInitializer<SocketChannel>() { | ||||
|                     @Override | ||||
|                     protected void initChannel(SocketChannel ch) { | ||||
| 
 | ||||
|                         if (options.timeout > 0) { | ||||
|                             ch.config().setConnectTimeoutMillis(options.timeout); | ||||
|                         } | ||||
|                         NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout); | ||||
| 
 | ||||
|                         var p = ch.pipeline(); | ||||
|                         p.addLast(NetworkUtils.SHAPING_HANDLER); | ||||
|                         if (sslContext != null) { | ||||
|                             p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort())); | ||||
|                         } | ||||
| 
 | ||||
|                         if (options.timeout > 0) { | ||||
|                             p.addLast(new ReadTimeoutHandler(options.timeout, TimeUnit.MILLISECONDS)); | ||||
|                         } | ||||
|                         if (timeout > 0) p.addLast(new ReadTimeoutHandler(timeout, TimeUnit.MILLISECONDS)); | ||||
| 
 | ||||
|                         p.addLast( | ||||
|                             new HttpClientCodec(), | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import io.netty.handler.codec.http.HttpClientCodec; | ||||
| import io.netty.handler.codec.http.HttpHeaderNames; | ||||
| import io.netty.handler.codec.http.HttpHeaders; | ||||
| import io.netty.handler.codec.http.HttpObjectAggregator; | ||||
| import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; | ||||
| import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler; | ||||
| import io.netty.handler.codec.http.websocketx.WebSocketVersion; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| @@ -59,13 +59,15 @@ public class Websocket extends Resource<Websocket> { | ||||
|     private final URI uri; | ||||
|     private final String address; | ||||
|     private final HttpHeaders headers; | ||||
|     private final int timeout; | ||||
| 
 | ||||
|     public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers) { | ||||
|     public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { | ||||
|         super(limiter); | ||||
|         this.environment = environment; | ||||
|         this.uri = uri; | ||||
|         this.address = address; | ||||
|         this.headers = headers; | ||||
|         this.timeout = timeout; | ||||
|     } | ||||
| 
 | ||||
|     public static URI checkUri(String address) throws HTTPRequestException { | ||||
| @@ -125,23 +127,21 @@ public class Websocket extends Resource<Websocket> { | ||||
|                 .handler(new ChannelInitializer<SocketChannel>() { | ||||
|                     @Override | ||||
|                     protected void initChannel(SocketChannel ch) { | ||||
|                         var p = ch.pipeline(); | ||||
|                         p.addLast(NetworkUtils.SHAPING_HANDLER); | ||||
|                         if (sslContext != null) { | ||||
|                             p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort())); | ||||
|                         } | ||||
|                         NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout); | ||||
| 
 | ||||
|                         var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL); | ||||
|                         WebSocketClientHandshaker handshaker = new NoOriginWebSocketHandshaker( | ||||
|                         var handshaker = new NoOriginWebSocketHandshaker( | ||||
|                             uri, WebSocketVersion.V13, subprotocol, true, headers, | ||||
|                             options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage | ||||
|                         ); | ||||
| 
 | ||||
|                         var p = ch.pipeline(); | ||||
|                         p.addLast( | ||||
|                             new HttpClientCodec(), | ||||
|                             new HttpObjectAggregator(8192), | ||||
|                             WebsocketCompressionHandler.INSTANCE, | ||||
|                             new WebsocketHandler(Websocket.this, handshaker, options) | ||||
|                             new WebSocketClientProtocolHandler(handshaker, false, timeout), | ||||
|                             new WebsocketHandler(Websocket.this, options) | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|   | ||||
| @@ -17,37 +17,34 @@ import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EV | ||||
| 
 | ||||
| public class WebsocketHandler extends SimpleChannelInboundHandler<Object> { | ||||
|     private final Websocket websocket; | ||||
|     private final WebSocketClientHandshaker handshaker; | ||||
|     private final Options options; | ||||
|     private boolean handshakeComplete = false; | ||||
| 
 | ||||
|     public WebsocketHandler(Websocket websocket, WebSocketClientHandshaker handshaker, Options options) { | ||||
|         this.handshaker = handshaker; | ||||
|     public WebsocketHandler(Websocket websocket, Options options) { | ||||
|         this.websocket = websocket; | ||||
|         this.options = options; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void channelActive(ChannelHandlerContext ctx) throws Exception { | ||||
|         handshaker.handshake(ctx.channel()); | ||||
|         super.channelActive(ctx); | ||||
|     public void channelInactive(ChannelHandlerContext ctx) throws Exception { | ||||
|         fail("Connection closed"); | ||||
|         super.channelInactive(ctx); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void channelInactive(ChannelHandlerContext ctx) throws Exception { | ||||
|         websocket.close(-1, "Websocket is inactive"); | ||||
|         super.channelInactive(ctx); | ||||
|     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { | ||||
|         if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) { | ||||
|             websocket.success(ctx.channel(), options); | ||||
|             handshakeComplete = true; | ||||
|         } else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) { | ||||
|             websocket.failure("Timed out"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void channelRead0(ChannelHandlerContext ctx, Object msg) { | ||||
|         if (websocket.isClosed()) return; | ||||
| 
 | ||||
|         if (!handshaker.isHandshakeComplete()) { | ||||
|             handshaker.finishHandshake(ctx.channel(), (FullHttpResponse) msg); | ||||
|             websocket.success(ctx.channel(), options); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (msg instanceof FullHttpResponse response) { | ||||
|             throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')'); | ||||
|         } | ||||
| @@ -65,9 +62,6 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object> { | ||||
|             websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true); | ||||
|         } else if (frame instanceof CloseWebSocketFrame closeFrame) { | ||||
|             websocket.close(closeFrame.statusCode(), closeFrame.reasonText()); | ||||
|         } else if (frame instanceof PingWebSocketFrame) { | ||||
|             frame.content().retain(); | ||||
|             ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -75,8 +69,11 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object> { | ||||
|     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { | ||||
|         ctx.close(); | ||||
| 
 | ||||
|         var message = NetworkUtils.toFriendlyError(cause); | ||||
|         if (handshaker.isHandshakeComplete()) { | ||||
|         fail(NetworkUtils.toFriendlyError(cause)); | ||||
|     } | ||||
| 
 | ||||
|     private void fail(String message) { | ||||
|         if (handshakeComplete) { | ||||
|             websocket.close(-1, message); | ||||
|         } else { | ||||
|             websocket.failure(message); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ local methods = { | ||||
|     PATCH = true, TRACE = true, | ||||
| } | ||||
|  | ||||
| local function checkKey(options, key, ty, opt) | ||||
| local function check_key(options, key, ty, opt) | ||||
|     local value = options[key] | ||||
|     local valueTy = type(value) | ||||
|  | ||||
| @@ -29,23 +29,24 @@ local function checkKey(options, key, ty, opt) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function checkOptions(options, body) | ||||
|     checkKey(options, "url", "string") | ||||
| local function check_request_options(options, body) | ||||
|     check_key(options, "url", "string") | ||||
|     if body == false then | ||||
|         checkKey(options, "body", "nil") | ||||
|         check_key(options, "body", "nil") | ||||
|     else | ||||
|         checkKey(options, "body", "string", not body) | ||||
|         check_key(options, "body", "string", not body) | ||||
|     end | ||||
|     checkKey(options, "headers", "table", true) | ||||
|     checkKey(options, "method", "string", true) | ||||
|     checkKey(options, "redirect", "boolean", true) | ||||
|     check_key(options, "headers", "table", true) | ||||
|     check_key(options, "method", "string", true) | ||||
|     check_key(options, "redirect", "boolean", true) | ||||
|     check_key(options, "timeout", "number", true) | ||||
|  | ||||
|     if options.method and not methods[options.method] then | ||||
|         error("Unsupported HTTP method", 3) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function wrapRequest(_url, ...) | ||||
| local function wrap_request(_url, ...) | ||||
|     local ok, err = nativeHTTPRequest(...) | ||||
|     if ok then | ||||
|         while true do | ||||
| @@ -72,6 +73,7 @@ decoded. | ||||
| @tparam[2] { | ||||
|   url = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
|   timeout? = number, | ||||
| } request Options for the request. See @{http.request} for details on how | ||||
| these options behave. | ||||
|  | ||||
| @@ -86,6 +88,7 @@ error or connection timeout. | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| @changed 1.105.0 Added support for custom timeouts. | ||||
|  | ||||
| @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), | ||||
| and print the returned page. | ||||
| @@ -99,14 +102,14 @@ request.close() | ||||
| ]] | ||||
| function get(_url, _headers, _binary) | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url, false) | ||||
|         return wrapRequest(_url.url, _url) | ||||
|         check_request_options(_url, false) | ||||
|         return wrap_request(_url.url, _url) | ||||
|     end | ||||
|  | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _headers, "table", "nil") | ||||
|     expect(3, _binary, "boolean", "nil") | ||||
|     return wrapRequest(_url, _url, nil, _headers, _binary) | ||||
|     return wrap_request(_url, _url, nil, _headers, _binary) | ||||
| end | ||||
|  | ||||
| --[[- Make a HTTP POST request to the given url. | ||||
| @@ -122,6 +125,7 @@ decoded. | ||||
| @tparam[2] { | ||||
|   url = string, body? = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
|   timeout? = number, | ||||
| } request Options for the request. See @{http.request} for details on how | ||||
| these options behave. | ||||
|  | ||||
| @@ -137,18 +141,19 @@ error or connection timeout. | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| @changed 1.105.0 Added support for custom timeouts. | ||||
| ]] | ||||
| function post(_url, _post, _headers, _binary) | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url, true) | ||||
|         return wrapRequest(_url.url, _url) | ||||
|         check_request_options(_url, true) | ||||
|         return wrap_request(_url.url, _url) | ||||
|     end | ||||
|  | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _post, "string") | ||||
|     expect(3, _headers, "table", "nil") | ||||
|     expect(4, _binary, "boolean", "nil") | ||||
|     return wrapRequest(_url, _url, _post, _headers, _binary) | ||||
|     return wrap_request(_url, _url, _post, _headers, _binary) | ||||
| end | ||||
|  | ||||
| --[[- Asynchronously make a HTTP request to the given url. | ||||
| @@ -168,6 +173,7 @@ decoded. | ||||
| @tparam[2] { | ||||
|   url = string, body? = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
|   timeout? = number, | ||||
| } request Options for the request. | ||||
|  | ||||
| This table form is an expanded version of the previous syntax. All arguments | ||||
| @@ -178,6 +184,7 @@ from above are passed in as fields instead (for instance, | ||||
|  | ||||
|  - `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`. | ||||
|  - `redirect`: Whether to follow HTTP redirects. Defaults to true. | ||||
|  - `timeout`: The connection timeout, in seconds. | ||||
|  | ||||
| @see http.get  For a synchronous way to make GET requests. | ||||
| @see http.post For a synchronous way to make POST requests. | ||||
| @@ -186,11 +193,12 @@ from above are passed in as fields instead (for instance, | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| @changed 1.105.0 Added support for custom timeouts. | ||||
| ]] | ||||
| function request(_url, _post, _headers, _binary) | ||||
|     local url | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url) | ||||
|         check_request_options(_url) | ||||
|         url = _url.url | ||||
|     else | ||||
|         expect(1, _url, "string") | ||||
| @@ -263,26 +271,48 @@ end | ||||
|  | ||||
| local nativeWebsocket = native.websocket | ||||
|  | ||||
| local function check_websocket_options(options, body) | ||||
|     check_key(options, "url", "string") | ||||
|     check_key(options, "headers", "table", true) | ||||
|     check_key(options, "timeout", "number", true) | ||||
| end | ||||
|  | ||||
|  | ||||
| --[[- Asynchronously open a websocket. | ||||
|  | ||||
| This returns immediately, a @{websocket_success} or @{websocket_failure} | ||||
| will be queued once the request has completed. | ||||
|  | ||||
| @tparam string url The websocket url to connect to. This should have the | ||||
| @tparam[1] string url The websocket url to connect to. This should have the | ||||
| `ws://` or `wss://` protocol. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| @tparam[1, opt] { [string] = string } headers Additional headers to send as part | ||||
| of the initial websocket connection. | ||||
|  | ||||
| @tparam[2] { | ||||
|   url = string, headers? = { [string] = string }, timeout ?= number, | ||||
| } request Options for the websocket.  See @{http.websocket} for details on how | ||||
| these options behave. | ||||
|  | ||||
| @since 1.80pr1.3 | ||||
| @changed 1.95.3 Added User-Agent to default headers. | ||||
| @changed 1.105.0 Added support for table argument and custom timeout. | ||||
| @see websocket_success | ||||
| @see websocket_failure | ||||
| ]] | ||||
| function websocketAsync(url, headers) | ||||
|     local actual_url | ||||
|     if type(url) == "table" then | ||||
|         check_websocket_options(url) | ||||
|         actual_url = url.url | ||||
|     else | ||||
|         expect(1, url, "string") | ||||
|         expect(2, headers, "table", "nil") | ||||
|         actual_url = url | ||||
|     end | ||||
|  | ||||
|     local ok, err = nativeWebsocket(url, headers) | ||||
|     if not ok then | ||||
|         os.queueEvent("websocket_failure", url, err) | ||||
|         os.queueEvent("websocket_failure", actual_url, err) | ||||
|     end | ||||
|  | ||||
|     -- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on. | ||||
| @@ -291,30 +321,59 @@ end | ||||
|  | ||||
| --[[- Open a websocket. | ||||
|  | ||||
| @tparam string url The websocket url to connect to. This should have the | ||||
| @tparam[1] string url The websocket url to connect to. This should have the | ||||
| `ws://` or `wss://` protocol. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| @tparam[1,opt] { [string] = string } headers Additional headers to send as part | ||||
| of the initial websocket connection. | ||||
|  | ||||
| @tparam[2] { | ||||
|   url = string, headers? = { [string] = string }, timeout ?= number, | ||||
| } request Options for the websocket. | ||||
|  | ||||
| This table form is an expanded version of the previous syntax. All arguments | ||||
| from above are passed in as fields instead (for instance, | ||||
| `http.websocket("https://example.com")` becomes `http.websocket { url = | ||||
| "https://example.com" }`). | ||||
|  This table also accepts the following additional options: | ||||
|  | ||||
|   - `timeout`: The connection timeout, in seconds. | ||||
|  | ||||
| @treturn Websocket The websocket connection. | ||||
| @treturn[2] false If the websocket connection failed. | ||||
| @treturn string An error message describing why the connection failed. | ||||
|  | ||||
| @since 1.80pr1.1 | ||||
| @changed 1.80pr1.3 No longer asynchronous. | ||||
| @changed 1.95.3 Added User-Agent to default headers. | ||||
| ]] | ||||
| function websocket(_url, _headers) | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _headers, "table", "nil") | ||||
| @changed 1.105.0 Added support for table argument and custom timeout. | ||||
|  | ||||
|     local ok, err = nativeWebsocket(_url, _headers) | ||||
| @usage Connect to an echo websocket and send a message. | ||||
|  | ||||
|     local ws = assert(http.websocket("wss://example.tweaked.cc/echo")) | ||||
|     ws.send("Hello!") -- Send a message | ||||
|     print(ws.receive()) -- And receive the reply | ||||
|     ws.close() | ||||
|  | ||||
| ]] | ||||
| function websocket(url, headers) | ||||
|     local actual_url | ||||
|     if type(url) == "table" then | ||||
|         check_websocket_options(url) | ||||
|         actual_url = url.url | ||||
|     else | ||||
|         expect(1, url, "string") | ||||
|         expect(2, headers, "table", "nil") | ||||
|         actual_url = url | ||||
|     end | ||||
|  | ||||
|     local ok, err = nativeWebsocket(url, headers) | ||||
|     if not ok then return ok, err end | ||||
|  | ||||
|     while true do | ||||
|         local event, url, param = os.pullEvent( ) | ||||
|         if event == "websocket_success" and url == _url then | ||||
|         if event == "websocket_success" and url == actual_url then | ||||
|             return param | ||||
|         elseif event == "websocket_failure" and url == _url then | ||||
|         elseif event == "websocket_failure" and url == actual_url then | ||||
|             return false, param | ||||
|         end | ||||
|     end | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| 
 | ||||
| package http | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.ObjectArguments | ||||
| import dan200.computercraft.core.CoreConfig | ||||
| import dan200.computercraft.core.apis.HTTPAPI | ||||
| import dan200.computercraft.core.apis.http.options.Action | ||||
| @@ -45,7 +46,7 @@ class TestHttpApi { | ||||
|         LuaTaskRunner.runTest { | ||||
|             val httpApi = addApi(HTTPAPI(environment)) | ||||
| 
 | ||||
|             val result = httpApi.websocket(WS_ADDRESS, Optional.empty()) | ||||
|             val result = httpApi.websocket(ObjectArguments(WS_ADDRESS)) | ||||
|             assertArrayEquals(arrayOf(true), result, "Should have created websocket") | ||||
| 
 | ||||
|             val event = pullEvent() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates