mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-01 17:42:56 +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:
parent
55ed0dc3ef
commit
2ae14b4c08
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user