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:
Jonathan Coates 2023-05-23 23:32:16 +01:00 committed by GitHub
parent 55ed0dc3ef
commit 2ae14b4c08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 203 additions and 96 deletions

View File

@ -32,9 +32,6 @@ public static UnmodifiableConfig makeRule(String host, Action action) {
config.add("action", action.name().toLowerCase(Locale.ROOT));
if (host.equals("*") && action == Action.ALLOW) {
config.setComment("timeout", "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited.");
config.add("timeout", AddressRule.TIMEOUT);
config.setComment("max_download", """
The maximum size (in bytes) that a computer can download in a single request.
Note that responses may receive more data than allowed, but this data will not
@ -58,7 +55,6 @@ public static boolean checkRule(UnmodifiableConfig builder) {
var port = unboxOptInt(get(builder, "port", Number.class));
return hostObj != null && checkEnum(builder, "action", Action.class)
&& check(builder, "port", Number.class)
&& check(builder, "timeout", Number.class)
&& check(builder, "max_upload", Number.class)
&& check(builder, "max_download", Number.class)
&& check(builder, "websocket_message", Number.class)
@ -72,7 +68,6 @@ public static AddressRule parseRule(UnmodifiableConfig builder) {
var action = getEnum(builder, "action", Action.class).orElse(null);
var port = unboxOptInt(get(builder, "port", Number.class));
var timeout = unboxOptInt(get(builder, "timeout", Number.class));
var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue));
var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue));
var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue));
@ -81,7 +76,6 @@ public static AddressRule parseRule(UnmodifiableConfig builder) {
action,
maxUpload,
maxDownload,
timeout,
websocketMessage
);

View File

@ -23,6 +23,7 @@
import java.util.Optional;
import static dan200.computercraft.core.apis.TableHelper.*;
import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween;
/**
* Placeholder description, please ignore.
@ -31,6 +32,9 @@
* @hidden
*/
public class HTTPAPI implements ILuaAPI {
private static final double DEFAULT_TIMEOUT = 30;
private static final double MAX_TIMEOUT = 60;
private final IAPIEnvironment apiEnvironment;
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>(() -> ResourceGroup.DEFAULT_LIMIT);
@ -72,6 +76,7 @@ public final Object[] request(IArguments args) throws LuaException {
String address, postString, requestMethod;
Map<?, ?> headerTable;
boolean binary, redirect;
Optional<Double> timeoutArg;
if (args.get(0) instanceof Map) {
var options = args.getTable(0);
@ -81,7 +86,7 @@ public final Object[] request(IArguments args) throws LuaException {
binary = optBooleanField(options, "binary", false);
requestMethod = optStringField(options, "method", null);
redirect = optBooleanField(options, "redirect", true);
timeoutArg = optRealField(options, "timeout");
} else {
// Get URL and post information
address = args.getString(0);
@ -90,9 +95,11 @@ public final Object[] request(IArguments args) throws LuaException {
binary = args.optBoolean(3, false);
requestMethod = null;
redirect = true;
timeoutArg = Optional.empty();
}
var headers = getHeaders(headerTable);
var timeout = getTimeout(timeoutArg);
HttpMethod httpMethod;
if (requestMethod == null) {
@ -106,7 +113,7 @@ public final Object[] request(IArguments args) throws LuaException {
try {
var uri = HttpRequest.checkUri(address);
var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect);
var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect, timeout);
// Make the request
if (!request.queue(r -> r.request(uri, httpMethod))) {
@ -134,16 +141,32 @@ public final Object[] checkURL(String address) throws LuaException {
}
@LuaFunction
public final Object[] websocket(String address, Optional<Map<?, ?>> headerTbl) throws LuaException {
public final Object[] websocket(IArguments args) throws LuaException {
if (!CoreConfig.httpWebsocketEnabled) {
throw new LuaException("Websocket connections are disabled");
}
var headers = getHeaders(headerTbl.orElse(Collections.emptyMap()));
String address;
Map<?, ?> headerTable;
Optional<Double> timeoutArg;
if (args.get(0) instanceof Map) {
var options = args.getTable(0);
address = getStringField(options, "url");
headerTable = optTableField(options, "headers", Collections.emptyMap());
timeoutArg = optRealField(options, "timeout");
} else {
address = args.getString(0);
headerTable = args.optTable(1, Collections.emptyMap());
timeoutArg = Optional.empty();
}
var headers = getHeaders(headerTable);
var timeout = getTimeout(timeoutArg);
try {
var uri = Websocket.checkUri(address);
if (!new Websocket(websockets, apiEnvironment, uri, address, headers).queue(Websocket::connect)) {
if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
throw new LuaException("Too many websockets already open");
}
@ -171,4 +194,17 @@ private HttpHeaders getHeaders(Map<?, ?> headerTable) throws LuaException {
}
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);
}
}

View File

@ -9,6 +9,7 @@
import javax.annotation.Nullable;
import java.util.Map;
import java.util.Optional;
import static dan200.computercraft.api.lua.LuaValues.getNumericType;
@ -100,6 +101,15 @@ public static int optIntField(Map<?, ?> table, String key, int def) throws LuaEx
}
}
public static Optional<Double> optRealField(Map<?, ?> table, String key) throws LuaException {
var value = table.get(key);
if(value == null) {
return Optional.empty();
} else {
return Optional.of(getRealField(table, key));
}
}
public static double optRealField(Map<?, ?> table, String key, double def) throws LuaException {
return checkReal(key, optNumberField(table, key, def));
}

View File

@ -13,6 +13,7 @@
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
@ -50,7 +51,7 @@ public final class NetworkUtils {
.build()
);
public static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler(
private static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler(
EXECUTOR, CoreConfig.httpUploadBandwidth, CoreConfig.httpDownloadBandwidth
);
@ -141,6 +142,30 @@ public static Options getOptions(String host, InetSocketAddress address) throws
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.
*

View File

@ -12,7 +12,7 @@ public enum Action {
DENY;
private final PartialOptions partial = new PartialOptions(
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty()
);
public PartialOptions toPartial() {

View File

@ -23,7 +23,6 @@
public final class AddressRule {
public static final long MAX_DOWNLOAD = 16 * 1024 * 1024;
public static final long MAX_UPLOAD = 4 * 1024 * 1024;
public static final int TIMEOUT = 30_000;
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
private final AddressPredicate predicate;

View File

@ -12,14 +12,12 @@ public final class Options {
public final Action action;
public final long maxUpload;
public final long maxDownload;
public final int timeout;
public final int websocketMessage;
Options(Action action, long maxUpload, long maxDownload, int timeout, int websocketMessage) {
Options(Action action, long maxUpload, long maxDownload, int websocketMessage) {
this.action = action;
this.maxUpload = maxUpload;
this.maxDownload = maxDownload;
this.timeout = timeout;
this.websocketMessage = websocketMessage;
}
}

View File

@ -13,23 +13,21 @@
@Immutable
public final class PartialOptions {
public static final PartialOptions DEFAULT = new PartialOptions(
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty()
);
private final @Nullable Action action;
private final OptionalLong maxUpload;
private final OptionalLong maxDownload;
private final OptionalInt timeout;
private final OptionalInt websocketMessage;
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
private @Nullable Options options;
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt timeout, OptionalInt websocketMessage) {
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage) {
this.action = action;
this.maxUpload = maxUpload;
this.maxDownload = maxDownload;
this.timeout = timeout;
this.websocketMessage = websocketMessage;
}
@ -40,7 +38,6 @@ Options toOptions() {
action == null ? Action.DENY : action,
maxUpload.orElse(AddressRule.MAX_UPLOAD),
maxDownload.orElse(AddressRule.MAX_DOWNLOAD),
timeout.orElse(AddressRule.TIMEOUT),
websocketMessage.orElse(AddressRule.WEBSOCKET_MESSAGE)
);
}
@ -59,7 +56,6 @@ PartialOptions merge(PartialOptions other) {
action == null && other.action != null ? other.action : action,
maxUpload.isPresent() ? maxUpload : other.maxUpload,
maxDownload.isPresent() ? maxDownload : other.maxDownload,
timeout.isPresent() ? timeout : other.timeout,
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage
);
}

View File

@ -52,12 +52,13 @@ public class HttpRequest extends Resource<HttpRequest> {
private final ByteBuf postBuffer;
private final HttpHeaders headers;
private final boolean binary;
private final int timeout;
final AtomicInteger redirects;
public HttpRequest(
ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
HttpHeaders headers, boolean binary, boolean followRedirects
HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
) {
super(limiter);
this.environment = environment;
@ -68,6 +69,7 @@ public HttpRequest(
this.headers = headers;
this.binary = binary;
redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0);
this.timeout = timeout;
if (postText != null) {
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
@ -143,20 +145,10 @@ private void doRequest(URI uri, HttpMethod method) {
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
if (options.timeout > 0) {
ch.config().setConnectTimeoutMillis(options.timeout);
}
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout);
var p = ch.pipeline();
p.addLast(NetworkUtils.SHAPING_HANDLER);
if (sslContext != null) {
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort()));
}
if (options.timeout > 0) {
p.addLast(new ReadTimeoutHandler(options.timeout, TimeUnit.MILLISECONDS));
}
if (timeout > 0) p.addLast(new ReadTimeoutHandler(timeout, TimeUnit.MILLISECONDS));
p.addLast(
new HttpClientCodec(),

View File

@ -23,7 +23,7 @@
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -59,13 +59,15 @@ public class Websocket extends Resource<Websocket> {
private final URI uri;
private final String address;
private final HttpHeaders headers;
private final int timeout;
public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers) {
public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) {
super(limiter);
this.environment = environment;
this.uri = uri;
this.address = address;
this.headers = headers;
this.timeout = timeout;
}
public static URI checkUri(String address) throws HTTPRequestException {
@ -125,23 +127,21 @@ private void doConnect() {
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
var p = ch.pipeline();
p.addLast(NetworkUtils.SHAPING_HANDLER);
if (sslContext != null) {
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort()));
}
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout);
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
WebSocketClientHandshaker handshaker = new NoOriginWebSocketHandshaker(
var handshaker = new NoOriginWebSocketHandshaker(
uri, WebSocketVersion.V13, subprotocol, true, headers,
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
);
var p = ch.pipeline();
p.addLast(
new HttpClientCodec(),
new HttpObjectAggregator(8192),
WebsocketCompressionHandler.INSTANCE,
new WebsocketHandler(Websocket.this, handshaker, options)
new WebSocketClientProtocolHandler(handshaker, false, timeout),
new WebsocketHandler(Websocket.this, options)
);
}
})

View File

@ -17,37 +17,34 @@
public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
private final Websocket websocket;
private final WebSocketClientHandshaker handshaker;
private final Options options;
private boolean handshakeComplete = false;
public WebsocketHandler(Websocket websocket, WebSocketClientHandshaker handshaker, Options options) {
this.handshaker = handshaker;
public WebsocketHandler(Websocket websocket, Options options) {
this.websocket = websocket;
this.options = options;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
handshaker.handshake(ctx.channel());
super.channelActive(ctx);
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
fail("Connection closed");
super.channelInactive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
websocket.close(-1, "Websocket is inactive");
super.channelInactive(ctx);
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
websocket.success(ctx.channel(), options);
handshakeComplete = true;
} else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
websocket.failure("Timed out");
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (websocket.isClosed()) return;
if (!handshaker.isHandshakeComplete()) {
handshaker.finishHandshake(ctx.channel(), (FullHttpResponse) msg);
websocket.success(ctx.channel(), options);
return;
}
if (msg instanceof FullHttpResponse response) {
throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
@ -65,9 +62,6 @@ public void channelRead0(ChannelHandlerContext ctx, Object msg) {
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true);
} else if (frame instanceof CloseWebSocketFrame closeFrame) {
websocket.close(closeFrame.statusCode(), closeFrame.reasonText());
} else if (frame instanceof PingWebSocketFrame) {
frame.content().retain();
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content()));
}
}
@ -75,8 +69,11 @@ public void channelRead0(ChannelHandlerContext ctx, Object msg) {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
var message = NetworkUtils.toFriendlyError(cause);
if (handshaker.isHandshakeComplete()) {
fail(NetworkUtils.toFriendlyError(cause));
}
private void fail(String message) {
if (handshakeComplete) {
websocket.close(-1, message);
} else {
websocket.failure(message);

View File

@ -20,7 +20,7 @@ local methods = {
PATCH = true, TRACE = true,
}
local function checkKey(options, key, ty, opt)
local function check_key(options, key, ty, opt)
local value = options[key]
local valueTy = type(value)
@ -29,23 +29,24 @@ local function checkKey(options, key, ty, opt)
end
end
local function checkOptions(options, body)
checkKey(options, "url", "string")
local function check_request_options(options, body)
check_key(options, "url", "string")
if body == false then
checkKey(options, "body", "nil")
check_key(options, "body", "nil")
else
checkKey(options, "body", "string", not body)
check_key(options, "body", "string", not body)
end
checkKey(options, "headers", "table", true)
checkKey(options, "method", "string", true)
checkKey(options, "redirect", "boolean", true)
check_key(options, "headers", "table", true)
check_key(options, "method", "string", true)
check_key(options, "redirect", "boolean", true)
check_key(options, "timeout", "number", true)
if options.method and not methods[options.method] then
error("Unsupported HTTP method", 3)
end
end
local function wrapRequest(_url, ...)
local function wrap_request(_url, ...)
local ok, err = nativeHTTPRequest(...)
if ok then
while true do
@ -72,6 +73,7 @@ decoded.
@tparam[2] {
url = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
timeout? = number,
} request Options for the request. See @{http.request} for details on how
these options behave.
@ -86,6 +88,7 @@ error or connection timeout.
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
@usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
and print the returned page.
@ -99,14 +102,14 @@ request.close()
]]
function get(_url, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, false)
return wrapRequest(_url.url, _url)
check_request_options(_url, false)
return wrap_request(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
expect(3, _binary, "boolean", "nil")
return wrapRequest(_url, _url, nil, _headers, _binary)
return wrap_request(_url, _url, nil, _headers, _binary)
end
--[[- Make a HTTP POST request to the given url.
@ -122,6 +125,7 @@ decoded.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
timeout? = number,
} request Options for the request. See @{http.request} for details on how
these options behave.
@ -137,18 +141,19 @@ error or connection timeout.
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
]]
function post(_url, _post, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, true)
return wrapRequest(_url.url, _url)
check_request_options(_url, true)
return wrap_request(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _post, "string")
expect(3, _headers, "table", "nil")
expect(4, _binary, "boolean", "nil")
return wrapRequest(_url, _url, _post, _headers, _binary)
return wrap_request(_url, _url, _post, _headers, _binary)
end
--[[- Asynchronously make a HTTP request to the given url.
@ -168,6 +173,7 @@ decoded.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
timeout? = number,
} request Options for the request.
This table form is an expanded version of the previous syntax. All arguments
@ -178,6 +184,7 @@ from above are passed in as fields instead (for instance,
- `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`.
- `redirect`: Whether to follow HTTP redirects. Defaults to true.
- `timeout`: The connection timeout, in seconds.
@see http.get For a synchronous way to make GET requests.
@see http.post For a synchronous way to make POST requests.
@ -186,11 +193,12 @@ from above are passed in as fields instead (for instance,
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
]]
function request(_url, _post, _headers, _binary)
local url
if type(_url) == "table" then
checkOptions(_url)
check_request_options(_url)
url = _url.url
else
expect(1, _url, "string")
@ -263,26 +271,48 @@ end
local nativeWebsocket = native.websocket
local function check_websocket_options(options, body)
check_key(options, "url", "string")
check_key(options, "headers", "table", true)
check_key(options, "timeout", "number", true)
end
--[[- Asynchronously open a websocket.
This returns immediately, a @{websocket_success} or @{websocket_failure}
will be queued once the request has completed.
@tparam string url The websocket url to connect to. This should have the
@tparam[1] string url The websocket url to connect to. This should have the
`ws://` or `wss://` protocol.
@tparam[opt] { [string] = string } headers Additional headers to send as part
@tparam[1, opt] { [string] = string } headers Additional headers to send as part
of the initial websocket connection.
@tparam[2] {
url = string, headers? = { [string] = string }, timeout ?= number,
} request Options for the websocket. See @{http.websocket} for details on how
these options behave.
@since 1.80pr1.3
@changed 1.95.3 Added User-Agent to default headers.
@changed 1.105.0 Added support for table argument and custom timeout.
@see websocket_success
@see websocket_failure
]]
function websocketAsync(url, headers)
expect(1, url, "string")
expect(2, headers, "table", "nil")
local actual_url
if type(url) == "table" then
check_websocket_options(url)
actual_url = url.url
else
expect(1, url, "string")
expect(2, headers, "table", "nil")
actual_url = url
end
local ok, err = nativeWebsocket(url, headers)
if not ok then
os.queueEvent("websocket_failure", url, err)
os.queueEvent("websocket_failure", actual_url, err)
end
-- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on.
@ -291,30 +321,59 @@ end
--[[- Open a websocket.
@tparam string url The websocket url to connect to. This should have the
@tparam[1] string url The websocket url to connect to. This should have the
`ws://` or `wss://` protocol.
@tparam[opt] { [string] = string } headers Additional headers to send as part
@tparam[1,opt] { [string] = string } headers Additional headers to send as part
of the initial websocket connection.
@tparam[2] {
url = string, headers? = { [string] = string }, timeout ?= number,
} request Options for the websocket.
This table form is an expanded version of the previous syntax. All arguments
from above are passed in as fields instead (for instance,
`http.websocket("https://example.com")` becomes `http.websocket { url =
"https://example.com" }`).
This table also accepts the following additional options:
- `timeout`: The connection timeout, in seconds.
@treturn Websocket The websocket connection.
@treturn[2] false If the websocket connection failed.
@treturn string An error message describing why the connection failed.
@since 1.80pr1.1
@changed 1.80pr1.3 No longer asynchronous.
@changed 1.95.3 Added User-Agent to default headers.
]]
function websocket(_url, _headers)
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
@changed 1.105.0 Added support for table argument and custom timeout.
local ok, err = nativeWebsocket(_url, _headers)
@usage Connect to an echo websocket and send a message.
local ws = assert(http.websocket("wss://example.tweaked.cc/echo"))
ws.send("Hello!") -- Send a message
print(ws.receive()) -- And receive the reply
ws.close()
]]
function websocket(url, headers)
local actual_url
if type(url) == "table" then
check_websocket_options(url)
actual_url = url.url
else
expect(1, url, "string")
expect(2, headers, "table", "nil")
actual_url = url
end
local ok, err = nativeWebsocket(url, headers)
if not ok then return ok, err end
while true do
local event, url, param = os.pullEvent( )
if event == "websocket_success" and url == _url then
if event == "websocket_success" and url == actual_url then
return param
elseif event == "websocket_failure" and url == _url then
elseif event == "websocket_failure" and url == actual_url then
return false, param
end
end

View File

@ -4,6 +4,7 @@
package http
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.CoreConfig
import dan200.computercraft.core.apis.HTTPAPI
import dan200.computercraft.core.apis.http.options.Action
@ -45,7 +46,7 @@ fun after() {
LuaTaskRunner.runTest {
val httpApi = addApi(HTTPAPI(environment))
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
val result = httpApi.websocket(ObjectArguments(WS_ADDRESS))
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
val event = pullEvent()