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