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