2019-01-11 11:33:05 +00:00
|
|
|
/*
|
|
|
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
2021-01-06 17:13:40 +00:00
|
|
|
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
2019-01-11 11:33:05 +00:00
|
|
|
* Send enquiries to dratcliffe@gmail.com
|
|
|
|
*/
|
|
|
|
package dan200.computercraft.core.apis.http.websocket;
|
|
|
|
|
|
|
|
import com.google.common.base.Strings;
|
|
|
|
import dan200.computercraft.ComputerCraft;
|
|
|
|
import dan200.computercraft.core.apis.IAPIEnvironment;
|
|
|
|
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
|
|
|
import dan200.computercraft.core.apis.http.NetworkUtils;
|
|
|
|
import dan200.computercraft.core.apis.http.Resource;
|
2019-01-11 12:07:56 +00:00
|
|
|
import dan200.computercraft.core.apis.http.ResourceGroup;
|
2020-05-15 21:44:46 +00:00
|
|
|
import dan200.computercraft.core.apis.http.options.Options;
|
2019-01-11 11:33:05 +00:00
|
|
|
import dan200.computercraft.shared.util.IoUtil;
|
|
|
|
import io.netty.bootstrap.Bootstrap;
|
|
|
|
import io.netty.channel.Channel;
|
|
|
|
import io.netty.channel.ChannelFuture;
|
|
|
|
import io.netty.channel.ChannelInitializer;
|
|
|
|
import io.netty.channel.ChannelPipeline;
|
|
|
|
import io.netty.channel.socket.SocketChannel;
|
|
|
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
|
|
|
import io.netty.handler.codec.http.HttpClientCodec;
|
2021-06-24 19:48:28 +00:00
|
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
2019-01-11 11:33:05 +00:00
|
|
|
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.WebSocketClientHandshakerFactory;
|
|
|
|
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
|
|
|
import io.netty.handler.ssl.SslContext;
|
|
|
|
|
|
|
|
import java.lang.ref.WeakReference;
|
|
|
|
import java.net.InetSocketAddress;
|
|
|
|
import java.net.URI;
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
import java.util.concurrent.Future;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides functionality to verify and connect to a remote websocket.
|
|
|
|
*/
|
|
|
|
public class Websocket extends Resource<Websocket>
|
|
|
|
{
|
2020-04-19 19:11:49 +00:00
|
|
|
/**
|
|
|
|
* We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as
|
|
|
|
* working with larger messages (especially within a Lua VM) is absurd.
|
|
|
|
*/
|
|
|
|
public static final int MAX_MESSAGE_SIZE = 1 << 30;
|
2019-01-11 11:33:05 +00:00
|
|
|
|
|
|
|
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 Future<?> executorFuture;
|
|
|
|
private ChannelFuture connectFuture;
|
|
|
|
private WeakReference<WebsocketHandle> websocketHandle;
|
|
|
|
|
|
|
|
private final IAPIEnvironment environment;
|
|
|
|
private final URI uri;
|
|
|
|
private final String address;
|
|
|
|
private final HttpHeaders headers;
|
|
|
|
|
2019-01-11 12:07:56 +00:00
|
|
|
public Websocket( ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers )
|
2019-01-11 11:33:05 +00:00
|
|
|
{
|
|
|
|
super( limiter );
|
|
|
|
this.environment = environment;
|
|
|
|
this.uri = uri;
|
|
|
|
this.address = address;
|
|
|
|
this.headers = headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static URI checkUri( String address ) throws HTTPRequestException
|
|
|
|
{
|
|
|
|
URI uri = null;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
uri = new URI( address );
|
|
|
|
}
|
|
|
|
catch( URISyntaxException ignored )
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
if( uri == null || uri.getHost() == null )
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
uri = new URI( "ws://" + address );
|
|
|
|
}
|
|
|
|
catch( URISyntaxException ignored )
|
|
|
|
{
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( uri == null || uri.getHost() == null ) throw new HTTPRequestException( "URL malformed" );
|
|
|
|
|
|
|
|
String scheme = uri.getScheme();
|
|
|
|
if( scheme == null )
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2019-03-29 21:21:39 +00:00
|
|
|
uri = new URI( "ws://" + uri );
|
2019-01-11 11:33:05 +00:00
|
|
|
}
|
|
|
|
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 );
|
|
|
|
checkClosed();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void doConnect()
|
|
|
|
{
|
|
|
|
// If we're cancelled, abort.
|
|
|
|
if( isClosed() ) return;
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
boolean ssl = uri.getScheme().equalsIgnoreCase( "wss" );
|
2020-09-15 21:05:27 +00:00
|
|
|
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri, ssl );
|
2020-05-15 21:44:46 +00:00
|
|
|
Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress );
|
2019-01-11 11:33:05 +00:00
|
|
|
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
|
|
|
|
|
|
|
// getAddress may have a slight delay, so let's perform another cancellation check.
|
|
|
|
if( isClosed() ) return;
|
|
|
|
|
|
|
|
connectFuture = new Bootstrap()
|
|
|
|
.group( NetworkUtils.LOOP_GROUP )
|
|
|
|
.channel( NioSocketChannel.class )
|
|
|
|
.handler( new ChannelInitializer<SocketChannel>()
|
|
|
|
{
|
|
|
|
@Override
|
|
|
|
protected void initChannel( SocketChannel ch )
|
|
|
|
{
|
|
|
|
ChannelPipeline p = ch.pipeline();
|
Add config options for a global bandwidth limit
This uses Netty's global traffic shaping handlers to limit the rate at
which packets can be sent and received. If the bandwidth limit is hit,
we'll start dropping packets, which will mean remote servers send
traffic to us at a much slower pace.
This isn't perfect, as there is only a global limit, and not a
per-computer one. As a result, its possible for one computer to use
all/most bandwidth, and thus slow down other computers.
This would be something to improve on in the future. However, I've spent
a lot of time reading the netty source code and docs, and the
implementation for that is significantly more complex, and one I'm not
comfortable working on right now.
For the time being, this satisfies the issues in #33 and hopefully
alleviates server owner's concerns about the http API. Remaining
problems can either be solved by moderation (with help of the
//computercraft track` command) or future updates.
Closes #33
2021-07-28 14:53:22 +00:00
|
|
|
p.addLast( NetworkUtils.SHAPING_HANDLER );
|
2019-01-11 11:33:05 +00:00
|
|
|
if( sslContext != null )
|
|
|
|
{
|
|
|
|
p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) );
|
|
|
|
}
|
|
|
|
|
2021-06-24 19:48:28 +00:00
|
|
|
String subprotocol = headers.get( HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL );
|
2019-01-11 11:33:05 +00:00
|
|
|
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
|
2021-06-24 19:48:28 +00:00
|
|
|
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
2020-05-15 21:44:46 +00:00
|
|
|
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
2019-01-11 11:33:05 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
p.addLast(
|
|
|
|
new HttpClientCodec(),
|
|
|
|
new HttpObjectAggregator( 8192 ),
|
2021-07-25 15:40:27 +00:00
|
|
|
WebsocketCompressionHandler.INSTANCE,
|
2020-05-15 21:44:46 +00:00
|
|
|
new WebsocketHandler( Websocket.this, handshaker, options )
|
2019-01-11 11:33:05 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
} )
|
|
|
|
.remoteAddress( socketAddress )
|
2019-01-22 11:11:25 +00:00
|
|
|
.connect()
|
|
|
|
.addListener( c -> {
|
2021-06-01 21:08:52 +00:00
|
|
|
if( !c.isSuccess() ) failure( NetworkUtils.toFriendlyError( c.cause() ) );
|
2019-01-22 11:11:25 +00:00
|
|
|
} );
|
2019-01-11 11:33:05 +00:00
|
|
|
|
|
|
|
// Do an additional check for cancellation
|
|
|
|
checkClosed();
|
|
|
|
}
|
|
|
|
catch( HTTPRequestException e )
|
|
|
|
{
|
|
|
|
failure( e.getMessage() );
|
|
|
|
}
|
|
|
|
catch( Exception e )
|
|
|
|
{
|
2021-06-01 21:08:52 +00:00
|
|
|
failure( NetworkUtils.toFriendlyError( e ) );
|
2020-05-15 21:44:46 +00:00
|
|
|
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in websocket", e );
|
2019-01-11 11:33:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-15 21:44:46 +00:00
|
|
|
void success( Channel channel, Options options )
|
2019-01-11 11:33:05 +00:00
|
|
|
{
|
|
|
|
if( isClosed() ) return;
|
|
|
|
|
2020-05-15 21:44:46 +00:00
|
|
|
WebsocketHandle handle = new WebsocketHandle( this, options, channel );
|
Replace getMethodNames/callMethod with annotations (#447)
When creating a peripheral or custom Lua object, one must implement two
methods:
- getMethodNames(): String[] - Returns the name of the methods
- callMethod(int, ...): Object[] - Invokes the method using an index in
the above array.
This has a couple of problems:
- It's somewhat unwieldy to use - you need to keep track of array
indices, which leads to ugly code.
- Functions which yield (for instance, those which run on the main
thread) are blocking. This means we need to spawn new threads for
each CC-side yield.
We replace this system with a few changes:
- @LuaFunction annotation: One may annotate a public instance method
with this annotation. This then exposes a peripheral/lua object
method.
Furthermore, this method can accept and return a variety of types,
which often makes functions cleaner (e.g. can return an int rather
than an Object[], and specify and int argument rather than
Object[]).
- MethodResult: Instead of returning an Object[] and having blocking
yields, functions return a MethodResult. This either contains an
immediate return, or an instruction to yield with some continuation
to resume with.
MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
rather than our weird bodgey hacks before. This means we no longer
spawn new threads when yielding within CC.
- Methods accept IArguments instead of a raw Object array. This has a
few benefits:
- Consistent argument handling - people no longer need to use
ArgumentHelper (as it doesn't exist!), or even be aware of its
existence - you're rather forced into using it.
- More efficient code in some cases. We provide a Cobalt-specific
implementation of IArguments, which avoids the boxing/unboxing when
handling numbers and binary strings.
2020-05-15 12:21:16 +00:00
|
|
|
environment().queueEvent( SUCCESS_EVENT, address, handle );
|
2019-03-29 21:21:39 +00:00
|
|
|
websocketHandle = createOwnerReference( handle );
|
2019-01-11 11:33:05 +00:00
|
|
|
|
|
|
|
checkClosed();
|
|
|
|
}
|
|
|
|
|
|
|
|
void failure( String message )
|
|
|
|
{
|
Replace getMethodNames/callMethod with annotations (#447)
When creating a peripheral or custom Lua object, one must implement two
methods:
- getMethodNames(): String[] - Returns the name of the methods
- callMethod(int, ...): Object[] - Invokes the method using an index in
the above array.
This has a couple of problems:
- It's somewhat unwieldy to use - you need to keep track of array
indices, which leads to ugly code.
- Functions which yield (for instance, those which run on the main
thread) are blocking. This means we need to spawn new threads for
each CC-side yield.
We replace this system with a few changes:
- @LuaFunction annotation: One may annotate a public instance method
with this annotation. This then exposes a peripheral/lua object
method.
Furthermore, this method can accept and return a variety of types,
which often makes functions cleaner (e.g. can return an int rather
than an Object[], and specify and int argument rather than
Object[]).
- MethodResult: Instead of returning an Object[] and having blocking
yields, functions return a MethodResult. This either contains an
immediate return, or an instruction to yield with some continuation
to resume with.
MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
rather than our weird bodgey hacks before. This means we no longer
spawn new threads when yielding within CC.
- Methods accept IArguments instead of a raw Object array. This has a
few benefits:
- Consistent argument handling - people no longer need to use
ArgumentHelper (as it doesn't exist!), or even be aware of its
existence - you're rather forced into using it.
- More efficient code in some cases. We provide a Cobalt-specific
implementation of IArguments, which avoids the boxing/unboxing when
handling numbers and binary strings.
2020-05-15 12:21:16 +00:00
|
|
|
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message );
|
2019-01-11 11:33:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void close( int status, String reason )
|
|
|
|
{
|
|
|
|
if( tryClose() )
|
|
|
|
{
|
Replace getMethodNames/callMethod with annotations (#447)
When creating a peripheral or custom Lua object, one must implement two
methods:
- getMethodNames(): String[] - Returns the name of the methods
- callMethod(int, ...): Object[] - Invokes the method using an index in
the above array.
This has a couple of problems:
- It's somewhat unwieldy to use - you need to keep track of array
indices, which leads to ugly code.
- Functions which yield (for instance, those which run on the main
thread) are blocking. This means we need to spawn new threads for
each CC-side yield.
We replace this system with a few changes:
- @LuaFunction annotation: One may annotate a public instance method
with this annotation. This then exposes a peripheral/lua object
method.
Furthermore, this method can accept and return a variety of types,
which often makes functions cleaner (e.g. can return an int rather
than an Object[], and specify and int argument rather than
Object[]).
- MethodResult: Instead of returning an Object[] and having blocking
yields, functions return a MethodResult. This either contains an
immediate return, or an instruction to yield with some continuation
to resume with.
MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
rather than our weird bodgey hacks before. This means we no longer
spawn new threads when yielding within CC.
- Methods accept IArguments instead of a raw Object array. This has a
few benefits:
- Consistent argument handling - people no longer need to use
ArgumentHelper (as it doesn't exist!), or even be aware of its
existence - you're rather forced into using it.
- More efficient code in some cases. We provide a Cobalt-specific
implementation of IArguments, which avoids the boxing/unboxing when
handling numbers and binary strings.
2020-05-15 12:21:16 +00:00
|
|
|
environment.queueEvent( CLOSE_EVENT, address,
|
2019-01-11 11:33:05 +00:00
|
|
|
Strings.isNullOrEmpty( reason ) ? null : reason,
|
Replace getMethodNames/callMethod with annotations (#447)
When creating a peripheral or custom Lua object, one must implement two
methods:
- getMethodNames(): String[] - Returns the name of the methods
- callMethod(int, ...): Object[] - Invokes the method using an index in
the above array.
This has a couple of problems:
- It's somewhat unwieldy to use - you need to keep track of array
indices, which leads to ugly code.
- Functions which yield (for instance, those which run on the main
thread) are blocking. This means we need to spawn new threads for
each CC-side yield.
We replace this system with a few changes:
- @LuaFunction annotation: One may annotate a public instance method
with this annotation. This then exposes a peripheral/lua object
method.
Furthermore, this method can accept and return a variety of types,
which often makes functions cleaner (e.g. can return an int rather
than an Object[], and specify and int argument rather than
Object[]).
- MethodResult: Instead of returning an Object[] and having blocking
yields, functions return a MethodResult. This either contains an
immediate return, or an instruction to yield with some continuation
to resume with.
MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
rather than our weird bodgey hacks before. This means we no longer
spawn new threads when yielding within CC.
- Methods accept IArguments instead of a raw Object array. This has a
few benefits:
- Consistent argument handling - people no longer need to use
ArgumentHelper (as it doesn't exist!), or even be aware of its
existence - you're rather forced into using it.
- More efficient code in some cases. We provide a Cobalt-specific
implementation of IArguments, which avoids the boxing/unboxing when
handling numbers and binary strings.
2020-05-15 12:21:16 +00:00
|
|
|
status < 0 ? null : status );
|
2019-01-11 11:33:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void dispose()
|
|
|
|
{
|
|
|
|
super.dispose();
|
|
|
|
|
|
|
|
executorFuture = closeFuture( executorFuture );
|
|
|
|
connectFuture = closeChannel( connectFuture );
|
|
|
|
|
2019-03-29 21:21:39 +00:00
|
|
|
WeakReference<WebsocketHandle> websocketHandleRef = websocketHandle;
|
2019-01-11 11:33:05 +00:00
|
|
|
WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
2020-05-20 07:44:44 +00:00
|
|
|
IoUtil.closeQuietly( websocketHandle );
|
2019-01-11 11:33:05 +00:00
|
|
|
this.websocketHandle = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public IAPIEnvironment environment()
|
|
|
|
{
|
|
|
|
return environment;
|
|
|
|
}
|
|
|
|
|
|
|
|
public String address()
|
|
|
|
{
|
|
|
|
return address;
|
|
|
|
}
|
|
|
|
}
|