mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-02-03 12:49:11 +00:00
Reduce coupling in websocket code
- Add a new WebsocketClient interface, which WebsocketHandle uses for sending messages and closing. This reduces coupling between Websocket and WebsocketHandle, which is nice, though admitedly only use for copy-cat :). - WebsocketHandle now uses Websocket(Client).isClosed(), rather than tracking the closed state itself - this makes the class mostly a thin Lua wrapper over the client, which is nice. - Convert Options into a record. - Clarify the behaviour of ws.close() and the websocket_closed event. Our previous test was incorrect as it called WebsocketHandle.close (rather than WebsocketHandle.doClose), which had slightly different semantics in whether the event is queued.
This commit is contained in:
parent
3188197447
commit
ae71eb3cae
@ -12,6 +12,7 @@ import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import dan200.computercraft.core.apis.http.websocket.WebsocketClient;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
@ -165,7 +166,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
var timeout = getTimeout(timeoutArg);
|
||||
|
||||
try {
|
||||
var uri = Websocket.checkUri(address);
|
||||
var uri = WebsocketClient.parseUri(address);
|
||||
if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
|
||||
throw new LuaException("Too many websockets already open");
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ public final class NetworkUtils {
|
||||
*/
|
||||
public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
|
||||
var options = AddressRule.apply(CoreConfig.httpRules, host, address);
|
||||
if (options.action == Action.DENY) throw new HTTPRequestException("Domain not permitted");
|
||||
if (options.action() == Action.DENY) throw new HTTPRequestException("Domain not permitted");
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ public final class NetworkUtils {
|
||||
* @throws HTTPRequestException If a proxy is required but not configured correctly.
|
||||
*/
|
||||
public static @Nullable Consumer<SocketChannel> getProxyHandler(Options options, int timeout) throws HTTPRequestException {
|
||||
if (!options.useProxy) return null;
|
||||
if (!options.useProxy()) return null;
|
||||
|
||||
var type = CoreConfig.httpProxyType;
|
||||
var host = CoreConfig.httpProxyHost;
|
||||
|
@ -6,20 +6,13 @@ package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
|
||||
/**
|
||||
* Options about a specific domain.
|
||||
* Options for a given HTTP request or websocket, which control its resource constraints.
|
||||
*
|
||||
* @param action Whether to {@link Action#ALLOW} or {@link Action#DENY} this request.
|
||||
* @param maxUpload The maximum size of the HTTP request.
|
||||
* @param maxDownload The maximum size of the HTTP response.
|
||||
* @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
|
||||
* @param useProxy Whether to use the configured proxy.
|
||||
*/
|
||||
public final class Options {
|
||||
public final Action action;
|
||||
public final long maxUpload;
|
||||
public final long maxDownload;
|
||||
public final int websocketMessage;
|
||||
public final boolean useProxy;
|
||||
|
||||
Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.websocketMessage = websocketMessage;
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ public final class PartialOptions {
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
|
||||
Options toOptions() {
|
||||
public Options toOptions() {
|
||||
if (options != null) return options;
|
||||
|
||||
return options = new Options(
|
||||
|
@ -130,7 +130,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
||||
if (isClosed()) return;
|
||||
|
||||
var requestBody = getHeaderSize(headers) + postBuffer.capacity();
|
||||
if (options.maxUpload != 0 && requestBody > options.maxUpload) {
|
||||
if (options.maxUpload() != 0 && requestBody > options.maxUpload()) {
|
||||
failure("Request body is too large");
|
||||
return;
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
var partial = content.content();
|
||||
if (partial.isReadable()) {
|
||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
||||
if (options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload) {
|
||||
if (options.maxDownload() != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload()) {
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
|
@ -16,8 +16,8 @@ import java.net.URI;
|
||||
* A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
|
||||
* original HTTP request.
|
||||
*/
|
||||
public class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
||||
public NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||
class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
||||
NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||
super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,9 @@ import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@ -23,21 +24,22 @@ 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.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Provides functionality to verify and connect to a remote websocket.
|
||||
*/
|
||||
public class Websocket extends Resource<Websocket> {
|
||||
public class Websocket extends Resource<Websocket> implements WebsocketClient {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Websocket.class);
|
||||
|
||||
/**
|
||||
@ -46,14 +48,8 @@ public class Websocket extends Resource<Websocket> {
|
||||
*/
|
||||
public static final int MAX_MESSAGE_SIZE = 1 << 30;
|
||||
|
||||
static final String SUCCESS_EVENT = "websocket_success";
|
||||
static final String FAILURE_EVENT = "websocket_failure";
|
||||
static final String CLOSE_EVENT = "websocket_closed";
|
||||
static final String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
private @Nullable Future<?> executorFuture;
|
||||
private @Nullable ChannelFuture connectFuture;
|
||||
private @Nullable WeakReference<WebsocketHandle> websocketHandle;
|
||||
private @Nullable ChannelFuture channelFuture;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final URI uri;
|
||||
@ -70,38 +66,6 @@ public class Websocket extends Resource<Websocket> {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public static URI checkUri(String address) throws HTTPRequestException {
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = new URI(address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
} else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
|
||||
throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
if (isClosed()) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
|
||||
@ -122,7 +86,7 @@ public class Websocket extends Resource<Websocket> {
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if (isClosed()) return;
|
||||
|
||||
connectFuture = new Bootstrap()
|
||||
channelFuture = new Bootstrap()
|
||||
.group(NetworkUtils.LOOP_GROUP)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@ -133,7 +97,7 @@ public class Websocket extends Resource<Websocket> {
|
||||
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
||||
var handshaker = new NoOriginWebSocketHandshaker(
|
||||
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();
|
||||
@ -162,12 +126,12 @@ public class Websocket extends Resource<Websocket> {
|
||||
}
|
||||
}
|
||||
|
||||
void success(Channel channel, Options options) {
|
||||
void success(Options options) {
|
||||
if (isClosed()) return;
|
||||
|
||||
var handle = new WebsocketHandle(this, options, channel);
|
||||
var handle = new WebsocketHandle(environment, address, this, options);
|
||||
environment().queueEvent(SUCCESS_EVENT, address, handle);
|
||||
websocketHandle = createOwnerReference(handle);
|
||||
createOwnerReference(handle);
|
||||
|
||||
checkClosed();
|
||||
}
|
||||
@ -189,19 +153,35 @@ public class Websocket extends Resource<Websocket> {
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture(executorFuture);
|
||||
connectFuture = closeChannel(connectFuture);
|
||||
|
||||
var websocketHandleRef = websocketHandle;
|
||||
var websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
||||
IoUtil.closeQuietly(websocketHandle);
|
||||
this.websocketHandle = null;
|
||||
channelFuture = closeChannel(channelFuture);
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment() {
|
||||
IAPIEnvironment environment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public String address() {
|
||||
String address() {
|
||||
return address;
|
||||
}
|
||||
|
||||
private @Nullable Channel channel() {
|
||||
var channel = channelFuture;
|
||||
return channel == null ? null : channel.channel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendText(String message) {
|
||||
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.length());
|
||||
|
||||
var channel = channel();
|
||||
if (channel != null) channel.writeAndFlush(new TextWebSocketFrame(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBinary(ByteBuffer message) {
|
||||
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.remaining());
|
||||
|
||||
var channel = channel();
|
||||
if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A client-side websocket, which can be used to send messages to a remote server.
|
||||
* <p>
|
||||
* {@link WebsocketHandle} wraps this into a Lua-compatible interface.
|
||||
*/
|
||||
public interface WebsocketClient extends Closeable {
|
||||
String SUCCESS_EVENT = "websocket_success";
|
||||
String FAILURE_EVENT = "websocket_failure";
|
||||
String CLOSE_EVENT = "websocket_closed";
|
||||
String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
/**
|
||||
* Determine whether this websocket is closed.
|
||||
*
|
||||
* @return Whether this websocket is closed.
|
||||
*/
|
||||
boolean isClosed();
|
||||
|
||||
/**
|
||||
* Close this websocket.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Send a text websocket frame.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendText(String message);
|
||||
|
||||
/**
|
||||
* Send a binary websocket frame.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendBinary(ByteBuffer message);
|
||||
|
||||
/**
|
||||
* Parse an address, ensuring it is a valid websocket URI.
|
||||
*
|
||||
* @param address The address to parse.
|
||||
* @return The parsed URI.
|
||||
* @throws HTTPRequestException If the address is not valid.
|
||||
*/
|
||||
static URI parseUri(String address) throws HTTPRequestException {
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = new URI(address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
} else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
|
||||
throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
}
|
@ -6,40 +6,34 @@ package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.CLOSE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||
|
||||
/**
|
||||
* A websocket, which can be used to send an receive messages with a web server.
|
||||
* A websocket, which can be used to send and receive messages with a web server.
|
||||
*
|
||||
* @cc.module http.Websocket
|
||||
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
||||
*/
|
||||
public class WebsocketHandle implements Closeable {
|
||||
private final Websocket websocket;
|
||||
public class WebsocketHandle {
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
private final WebsocketClient websocket;
|
||||
private final Options options;
|
||||
private boolean closed = false;
|
||||
|
||||
private @Nullable Channel channel;
|
||||
|
||||
public WebsocketHandle(Websocket websocket, Options options, Channel channel) {
|
||||
public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) {
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,7 +52,7 @@ public class WebsocketHandle implements Closeable {
|
||||
public final MethodResult receive(Optional<Double> timeout) throws LuaException {
|
||||
checkOpen();
|
||||
var timeoutId = timeout.isPresent()
|
||||
? websocket.environment().startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||
? environment.startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||
: -1;
|
||||
|
||||
return new ReceiveCallback(timeoutId).pull;
|
||||
@ -78,17 +72,14 @@ public class WebsocketHandle implements Closeable {
|
||||
checkOpen();
|
||||
|
||||
var text = message.value();
|
||||
if (options.websocketMessage != 0 && text.length() > options.websocketMessage) {
|
||||
if (options.websocketMessage() != 0 && text.length() > options.websocketMessage()) {
|
||||
throw new LuaException("Message is too large");
|
||||
}
|
||||
|
||||
websocket.environment().observe(Metrics.WEBSOCKET_OUTGOING, text.length());
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.writeAndFlush(binary.orElse(false)
|
||||
? new BinaryWebSocketFrame(Unpooled.wrappedBuffer(LuaValues.encode(text)))
|
||||
: new TextWebSocketFrame(text));
|
||||
if (binary.orElse(false)) {
|
||||
websocket.sendBinary(LuaValues.encode(text));
|
||||
} else {
|
||||
websocket.sendText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,25 +87,13 @@ public class WebsocketHandle implements Closeable {
|
||||
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
|
||||
* along it.
|
||||
*/
|
||||
@LuaFunction("close")
|
||||
public final void doClose() {
|
||||
close();
|
||||
@LuaFunction
|
||||
public final void close() {
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
private void checkOpen() throws LuaException {
|
||||
if (closed) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
if (websocket.isClosed()) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
private final class ReceiveCallback implements ILuaCallback {
|
||||
@ -127,9 +106,9 @@ public class WebsocketHandle implements Closeable {
|
||||
|
||||
@Override
|
||||
public MethodResult resume(Object[] event) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], websocket.address())) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], address)) {
|
||||
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], websocket.address()) && closed) {
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], address) && websocket.isClosed()) {
|
||||
// If the socket is closed abort.
|
||||
return MethodResult.of();
|
||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
||||
|
@ -13,14 +13,14 @@ import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.websocketx.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||
|
||||
public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
private final Websocket websocket;
|
||||
private final Options options;
|
||||
private boolean handshakeComplete = false;
|
||||
|
||||
public WebsocketHandler(Websocket websocket, Options options) {
|
||||
WebsocketHandler(Websocket websocket, Options options) {
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
}
|
||||
@ -32,9 +32,9 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
||||
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
||||
websocket.success(ctx.channel(), options);
|
||||
websocket.success(options);
|
||||
handshakeComplete = true;
|
||||
} else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
|
||||
websocket.failure("Timed out");
|
||||
|
@ -23,8 +23,8 @@ public class AddressRuleTest {
|
||||
Action.ALLOW.toPartial()
|
||||
));
|
||||
|
||||
assertEquals(apply(rules, "localhost", 8080).action, Action.ALLOW);
|
||||
assertEquals(apply(rules, "localhost", 8081).action, Action.DENY);
|
||||
assertEquals(apply(rules, "localhost", 8080).action(), Action.ALLOW);
|
||||
assertEquals(apply(rules, "localhost", 8081).action(), Action.DENY);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ -43,7 +43,7 @@ public class AddressRuleTest {
|
||||
"169.254.169.254", // AWS, Digital Ocean, GCP, etc..
|
||||
})
|
||||
public void blocksLocalDomains(String domain) {
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.DENY);
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.DENY);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ -52,7 +52,7 @@ public class AddressRuleTest {
|
||||
"100.63.255.255", "100.128.0.0"
|
||||
})
|
||||
public void allowsNonLocalDomains(String domain) {
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.ALLOW);
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.ALLOW);
|
||||
}
|
||||
|
||||
private Options apply(Iterable<AddressRule> rules, String host, int port) {
|
||||
|
@ -27,7 +27,7 @@ object HttpServer {
|
||||
const val URL: String = "http://127.0.0.1:$PORT"
|
||||
const val WS_URL: String = "ws://127.0.0.1:$PORT/ws"
|
||||
|
||||
fun runServer(run: () -> Unit) {
|
||||
fun runServer(run: (stop: () -> Unit) -> Unit) {
|
||||
val workerGroup: EventLoopGroup = NioEventLoopGroup(2)
|
||||
try {
|
||||
val ch = ServerBootstrap()
|
||||
@ -48,7 +48,7 @@ object HttpServer {
|
||||
},
|
||||
).bind(PORT).sync().channel()
|
||||
try {
|
||||
run()
|
||||
run { workerGroup.shutdownGracefully() }
|
||||
} finally {
|
||||
ch.close().sync()
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core.apis.http
|
||||
|
||||
import dan200.computercraft.api.lua.Coerced
|
||||
import dan200.computercraft.api.lua.LuaException
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
@ -22,7 +23,9 @@ import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.util.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TestHttpApi {
|
||||
companion object {
|
||||
@ -79,12 +82,36 @@ class TestHttpApi {
|
||||
|
||||
websocket.close()
|
||||
|
||||
val closeEvent = pullEventOrTimeout(500.milliseconds, "websocket_closed")
|
||||
assertThat("No event was queued", closeEvent, equalTo(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Queues an event when the socket is externally closed`() {
|
||||
runServer { stop ->
|
||||
LuaTaskRunner.runTest {
|
||||
val httpApi = addApi(HTTPAPI(environment))
|
||||
assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true)))
|
||||
|
||||
val connectEvent = pullEvent()
|
||||
assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
|
||||
|
||||
val websocket = connectEvent[2] as WebsocketHandle
|
||||
|
||||
stop()
|
||||
|
||||
val closeEvent = pullEvent("websocket_closed")
|
||||
assertThat(
|
||||
"Websocket was closed",
|
||||
closeEvent,
|
||||
array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)),
|
||||
)
|
||||
|
||||
assertThrows<LuaException>("Throws an exception when sending") {
|
||||
websocket.send(Coerced("hello"), Optional.of(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import dan200.computercraft.core.apis.OSAPI
|
||||
import dan200.computercraft.core.apis.PeripheralAPI
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
@ -29,6 +30,9 @@ interface LuaTaskContext {
|
||||
/** Pull a Lua event */
|
||||
suspend fun pullEvent(event: String? = null): Array<out Any?>
|
||||
|
||||
suspend fun pullEventOrTimeout(timeout: Duration, event: String? = null): Array<out Any?>? =
|
||||
withTimeoutOrNull(timeout) { pullEvent(event) }
|
||||
|
||||
/** Resolve a [MethodResult] until completion, returning the resulting values. */
|
||||
suspend fun MethodResult.await(): Array<out Any?>? {
|
||||
var result = this
|
||||
|
Loading…
Reference in New Issue
Block a user