mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-15 19:55:42 +00:00
Merge pull request #395 from SquidDev-CC/ComputerCraft/feature/websocket
Websocket support
This commit is contained in:
commit
c9181a121f
@ -121,6 +121,7 @@ public class ComputerCraft
|
||||
};
|
||||
|
||||
public static boolean http_enable = true;
|
||||
public static boolean http_websocket_enable = true;
|
||||
public static AddressPredicate http_whitelist = new AddressPredicate( DEFAULT_HTTP_WHITELIST );
|
||||
public static AddressPredicate http_blacklist = new AddressPredicate( DEFAULT_HTTP_BLACKLIST );
|
||||
public static boolean disable_lua51_features = false;
|
||||
@ -200,6 +201,7 @@ public class ComputerCraft
|
||||
public static Configuration config;
|
||||
|
||||
public static Property http_enable;
|
||||
public static Property http_websocket_enable;
|
||||
public static Property http_whitelist;
|
||||
public static Property http_blacklist;
|
||||
public static Property disable_lua51_features;
|
||||
@ -271,6 +273,9 @@ public class ComputerCraft
|
||||
Config.http_enable = Config.config.get( Configuration.CATEGORY_GENERAL, "http_enable", http_enable );
|
||||
Config.http_enable.setComment( "Enable the \"http\" API on Computers (see \"http_whitelist\" and \"http_blacklist\" for more fine grained control than this)" );
|
||||
|
||||
Config.http_websocket_enable = Config.config.get( Configuration.CATEGORY_GENERAL, "http_websocket_enable", http_websocket_enable );
|
||||
Config.http_websocket_enable.setComment( "Enable use of http websockets. This requires the \"http_enable\" option to also be true." );
|
||||
|
||||
{
|
||||
ConfigCategory category = Config.config.getCategory( Configuration.CATEGORY_GENERAL );
|
||||
Property currentProperty = category.get( "http_whitelist" );
|
||||
@ -362,6 +367,7 @@ public class ComputerCraft
|
||||
public static void syncConfig() {
|
||||
|
||||
http_enable = Config.http_enable.getBoolean();
|
||||
http_websocket_enable = Config.http_websocket_enable.getBoolean();
|
||||
http_whitelist = new AddressPredicate( Config.http_whitelist.getStringList() );
|
||||
http_blacklist = new AddressPredicate( Config.http_blacklist.getStringList() );
|
||||
disable_lua51_features = Config.disable_lua51_features.getBoolean();
|
||||
|
@ -6,27 +6,35 @@
|
||||
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.apis.http.HTTPCheck;
|
||||
import dan200.computercraft.core.apis.http.HTTPExecutor;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequest;
|
||||
import dan200.computercraft.core.apis.http.HTTPTask;
|
||||
import dan200.computercraft.core.apis.http.WebsocketConnector;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static dan200.computercraft.core.apis.ArgumentHelper.*;
|
||||
|
||||
public class HTTPAPI implements ILuaAPI
|
||||
{
|
||||
private final IAPIEnvironment m_apiEnvironment;
|
||||
private final List<HTTPTask> m_httpTasks;
|
||||
private final List<Future<?>> m_httpTasks;
|
||||
private final Set<Closeable> m_closeables;
|
||||
|
||||
public HTTPAPI( IAPIEnvironment environment )
|
||||
{
|
||||
m_apiEnvironment = environment;
|
||||
m_httpTasks = new ArrayList<>();
|
||||
m_closeables = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -48,15 +56,11 @@ public class HTTPAPI implements ILuaAPI
|
||||
// Wait for all of our http requests
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
Iterator<HTTPTask> it = m_httpTasks.iterator();
|
||||
Iterator<Future<?>> it = m_httpTasks.iterator();
|
||||
while( it.hasNext() )
|
||||
{
|
||||
final HTTPTask h = it.next();
|
||||
if( h.isFinished() )
|
||||
{
|
||||
h.whenFinished( m_apiEnvironment );
|
||||
it.remove();
|
||||
}
|
||||
final Future<?> h = it.next();
|
||||
if( h.isDone() ) it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,21 +70,36 @@ public class HTTPAPI implements ILuaAPI
|
||||
{
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
for( HTTPTask r : m_httpTasks )
|
||||
for( Future<?> r : m_httpTasks )
|
||||
{
|
||||
r.cancel();
|
||||
r.cancel( false );
|
||||
}
|
||||
m_httpTasks.clear();
|
||||
}
|
||||
synchronized( m_closeables )
|
||||
{
|
||||
for( Closeable x : m_closeables )
|
||||
{
|
||||
try
|
||||
{
|
||||
x.close();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
m_closeables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String[] getMethodNames()
|
||||
{
|
||||
return new String[] {
|
||||
return new String[] {
|
||||
"request",
|
||||
"checkURL"
|
||||
"checkURL",
|
||||
"websocket",
|
||||
};
|
||||
}
|
||||
|
||||
@ -125,10 +144,10 @@ public class HTTPAPI implements ILuaAPI
|
||||
try
|
||||
{
|
||||
URL url = HTTPRequest.checkURL( urlString );
|
||||
HTTPRequest request = new HTTPRequest( urlString, url, postString, headers, binary );
|
||||
HTTPRequest request = new HTTPRequest( m_apiEnvironment, urlString, url, postString, headers, binary );
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
m_httpTasks.add( HTTPTask.submit( request ) );
|
||||
m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( request ) );
|
||||
}
|
||||
return new Object[] { true };
|
||||
}
|
||||
@ -147,9 +166,47 @@ public class HTTPAPI implements ILuaAPI
|
||||
try
|
||||
{
|
||||
URL url = HTTPRequest.checkURL( urlString );
|
||||
HTTPCheck check = new HTTPCheck( urlString, url );
|
||||
synchronized( m_httpTasks ) {
|
||||
m_httpTasks.add( HTTPTask.submit( check ) );
|
||||
HTTPCheck check = new HTTPCheck( m_apiEnvironment, urlString, url );
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( check ) );
|
||||
}
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
return new Object[] { false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
case 2: // websocket
|
||||
{
|
||||
String address = getString( args, 0 );
|
||||
Map<Object, Object> headerTbl = optTable( args, 1, Collections.emptyMap() );
|
||||
|
||||
HashMap<String, String> headers = new HashMap<String, String>( headerTbl.size() );
|
||||
for( Object key : headerTbl.keySet() )
|
||||
{
|
||||
Object value = headerTbl.get( key );
|
||||
if( key instanceof String && value instanceof String )
|
||||
{
|
||||
headers.put( (String) key, (String) value );
|
||||
}
|
||||
}
|
||||
|
||||
if( !ComputerCraft.http_websocket_enable )
|
||||
{
|
||||
throw new LuaException( "Websocket connections are disabled" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
URI uri = WebsocketConnector.checkURI( address );
|
||||
int port = WebsocketConnector.getPort( uri );
|
||||
|
||||
Future<?> connector = WebsocketConnector.createConnector( m_apiEnvironment, this, uri, address, port, headers );
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
m_httpTasks.add( connector );
|
||||
}
|
||||
return new Object[] { true };
|
||||
}
|
||||
@ -164,4 +221,20 @@ public class HTTPAPI implements ILuaAPI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addCloseable( Closeable closeable )
|
||||
{
|
||||
synchronized( m_closeables )
|
||||
{
|
||||
m_closeables.add( closeable );
|
||||
}
|
||||
}
|
||||
|
||||
public void removeCloseable( Closeable closeable )
|
||||
{
|
||||
synchronized( m_closeables )
|
||||
{
|
||||
m_closeables.remove( closeable );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,15 @@ import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class HTTPCheck implements HTTPTask.IHTTPTask
|
||||
public class HTTPCheck implements Runnable
|
||||
{
|
||||
private final IAPIEnvironment environment;
|
||||
private final String urlString;
|
||||
private final URL url;
|
||||
private String error;
|
||||
|
||||
public HTTPCheck( String urlString, URL url )
|
||||
public HTTPCheck( IAPIEnvironment environment, String urlString, URL url )
|
||||
{
|
||||
this.environment = environment;
|
||||
this.urlString = urlString;
|
||||
this.url = url;
|
||||
}
|
||||
@ -22,24 +23,12 @@ public class HTTPCheck implements HTTPTask.IHTTPTask
|
||||
{
|
||||
try
|
||||
{
|
||||
HTTPRequest.checkHost( url );
|
||||
HTTPRequest.checkHost( url.getHost() );
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, true } );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
error = e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whenFinished( IAPIEnvironment environment )
|
||||
{
|
||||
if( error == null )
|
||||
{
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, true } );
|
||||
}
|
||||
else
|
||||
{
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, false, error } );
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, false, e.getMessage() } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Just a shared object for executing simple HTTP related tasks.
|
||||
*/
|
||||
public final class HTTPExecutor
|
||||
{
|
||||
public static final ListeningExecutorService EXECUTOR = MoreExecutors.listeningDecorator( new ThreadPoolExecutor(
|
||||
4, Integer.MAX_VALUE,
|
||||
60L, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>(),
|
||||
new ThreadFactoryBuilder()
|
||||
.setDaemon( true )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.setNameFormat( "ComputerCraft-HTTP-%d" )
|
||||
.build()
|
||||
) );
|
||||
|
||||
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, new ThreadFactoryBuilder()
|
||||
.setDaemon( true )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.setNameFormat( "ComputerCraft-Netty-%d" )
|
||||
.build()
|
||||
);
|
||||
|
||||
private HTTPExecutor()
|
||||
{
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HTTPRequest implements HTTPTask.IHTTPTask
|
||||
public class HTTPRequest implements Runnable
|
||||
{
|
||||
public static URL checkURL( String urlString ) throws HTTPRequestException
|
||||
{
|
||||
@ -55,11 +55,11 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
||||
return url;
|
||||
}
|
||||
|
||||
public static InetAddress checkHost( URL url ) throws HTTPRequestException
|
||||
public static InetAddress checkHost( String host ) throws HTTPRequestException
|
||||
{
|
||||
try
|
||||
{
|
||||
InetAddress resolved = InetAddress.getByName( url.getHost() );
|
||||
InetAddress resolved = InetAddress.getByName( host );
|
||||
if( !ComputerCraft.http_whitelist.matches( resolved ) || ComputerCraft.http_blacklist.matches( resolved ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
@ -73,22 +73,16 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
||||
}
|
||||
}
|
||||
|
||||
private final IAPIEnvironment m_environment;
|
||||
private final URL m_url;
|
||||
private final String m_urlString;
|
||||
private final String m_postText;
|
||||
private final Map<String, String> m_headers;
|
||||
|
||||
private boolean m_success = false;
|
||||
private String m_encoding;
|
||||
private byte[] m_result;
|
||||
private boolean m_binary;
|
||||
private int m_responseCode = -1;
|
||||
private Map<String, String> m_responseHeaders;
|
||||
private String m_errorMessage;
|
||||
|
||||
public HTTPRequest( String urlString, URL url, final String postText, final Map<String, String> headers, boolean binary ) throws HTTPRequestException
|
||||
public HTTPRequest( IAPIEnvironment environment, String urlString, URL url, final String postText, final Map<String, String> headers, boolean binary ) throws HTTPRequestException
|
||||
{
|
||||
// Parse the URL
|
||||
m_environment = environment;
|
||||
m_urlString = urlString;
|
||||
m_url = url;
|
||||
m_binary = binary;
|
||||
@ -96,28 +90,19 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
||||
m_headers = headers;
|
||||
}
|
||||
|
||||
public InputStream getContents()
|
||||
{
|
||||
byte[] result = m_result;
|
||||
if( result != null )
|
||||
{
|
||||
return new ByteArrayInputStream( result );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
// First verify the address is allowed.
|
||||
try
|
||||
{
|
||||
checkHost( m_url );
|
||||
checkHost( m_url.getHost() );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
m_success = false;
|
||||
m_errorMessage = e.getMessage();
|
||||
// Queue the failure event if not.
|
||||
String error = e.getMessage();
|
||||
m_environment.queueEvent( "http_failure", new Object[] { m_urlString, error == null ? "Could not connect" : error, null } );
|
||||
return;
|
||||
}
|
||||
|
||||
@ -186,58 +171,36 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
||||
byte[] result = ByteStreams.toByteArray( is );
|
||||
is.close();
|
||||
|
||||
// We completed
|
||||
m_success = responseSuccess;
|
||||
m_result = result;
|
||||
m_responseCode = connection.getResponseCode();
|
||||
m_encoding = connection.getContentEncoding();
|
||||
|
||||
// We've got some sort of response, so let's build a resulting object.
|
||||
Joiner joiner = Joiner.on( ',' );
|
||||
Map<String, String> headers = m_responseHeaders = new HashMap<>();
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
for( Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet() )
|
||||
{
|
||||
headers.put( header.getKey(), joiner.join( header.getValue() ) );
|
||||
}
|
||||
|
||||
InputStream contents = new ByteArrayInputStream( result );
|
||||
ILuaObject stream = wrapStream(
|
||||
m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, connection.getContentEncoding() ),
|
||||
connection.getResponseCode(), headers
|
||||
);
|
||||
|
||||
connection.disconnect(); // disconnect
|
||||
|
||||
// Queue the appropriate event.
|
||||
if( responseSuccess )
|
||||
{
|
||||
m_environment.queueEvent( "http_success", new Object[] { m_urlString, stream } );
|
||||
}
|
||||
else
|
||||
{
|
||||
m_environment.queueEvent( "http_failure", new Object[] { m_urlString, "Could not connect", stream } );
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
// There was an error
|
||||
m_success = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whenFinished( IAPIEnvironment environment )
|
||||
{
|
||||
final String url = m_urlString;
|
||||
if( m_success )
|
||||
{
|
||||
// Queue the "http_success" event
|
||||
InputStream contents = getContents();
|
||||
Object result = wrapStream(
|
||||
m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, m_encoding ),
|
||||
m_responseCode, m_responseHeaders
|
||||
);
|
||||
environment.queueEvent( "http_success", new Object[] { url, result } );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Queue the "http_failure" event
|
||||
String error = "Could not connect";
|
||||
if( m_errorMessage != null ) error = m_errorMessage;
|
||||
|
||||
InputStream contents = getContents();
|
||||
Object result = null;
|
||||
if( contents != null )
|
||||
{
|
||||
result = wrapStream(
|
||||
m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, m_encoding ),
|
||||
m_responseCode, m_responseHeaders
|
||||
);
|
||||
}
|
||||
environment.queueEvent( "http_failure", new Object[] { url, error, result } );
|
||||
m_environment.queueEvent( "http_failure", new Object[] { m_urlString, "Could not connect", null } );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* A task which executes asynchronously on a new thread.
|
||||
*
|
||||
* This functions very similarly to a {@link Future}, but with an additional
|
||||
* method which is called on the main thread when the task is completed.
|
||||
*/
|
||||
public class HTTPTask
|
||||
{
|
||||
public interface IHTTPTask extends Runnable
|
||||
{
|
||||
void whenFinished( IAPIEnvironment environment );
|
||||
}
|
||||
|
||||
private static final ExecutorService httpThreads = new ThreadPoolExecutor(
|
||||
4, Integer.MAX_VALUE,
|
||||
60L, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>(),
|
||||
new ThreadFactoryBuilder()
|
||||
.setDaemon( true )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.setNameFormat( "ComputerCraft-HTTP-%d" )
|
||||
.build()
|
||||
);
|
||||
|
||||
private final Future<?> future;
|
||||
private final IHTTPTask task;
|
||||
|
||||
private HTTPTask( Future<?> future, IHTTPTask task )
|
||||
{
|
||||
this.future = future;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
public static HTTPTask submit( IHTTPTask task )
|
||||
{
|
||||
Future<?> future = httpThreads.submit( task );
|
||||
return new HTTPTask( future, task );
|
||||
}
|
||||
|
||||
public void cancel()
|
||||
{
|
||||
future.cancel( false );
|
||||
}
|
||||
|
||||
public boolean isFinished()
|
||||
{
|
||||
return future.isDone();
|
||||
}
|
||||
|
||||
public void whenFinished( IAPIEnvironment environment )
|
||||
{
|
||||
task.whenFinished( environment );
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.apis.HTTPAPI;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.websocketx.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public class WebsocketConnection extends SimpleChannelInboundHandler<Object> implements ILuaObject, Closeable
|
||||
{
|
||||
public static final String SUCCESS_EVENT = "websocket_success";
|
||||
public static final String FAILURE_EVENT = "websocket_failure";
|
||||
public static final String CLOSE_EVENT = "websocket_closed";
|
||||
public static final String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
private final String url;
|
||||
private final IAPIEnvironment computer;
|
||||
private final HTTPAPI api;
|
||||
private boolean open = true;
|
||||
|
||||
private Channel channel;
|
||||
private final WebSocketClientHandshaker handshaker;
|
||||
|
||||
public WebsocketConnection( IAPIEnvironment computer, HTTPAPI api, WebSocketClientHandshaker handshaker, String url )
|
||||
{
|
||||
this.computer = computer;
|
||||
this.api = api;
|
||||
this.handshaker = handshaker;
|
||||
this.url = url;
|
||||
|
||||
api.addCloseable( this );
|
||||
}
|
||||
|
||||
private void close( boolean remove )
|
||||
{
|
||||
open = false;
|
||||
if( remove ) api.removeCloseable( this );
|
||||
|
||||
if( channel != null )
|
||||
{
|
||||
channel.close();
|
||||
channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
close( false );
|
||||
}
|
||||
|
||||
private void onClosed()
|
||||
{
|
||||
close( true );
|
||||
computer.queueEvent( CLOSE_EVENT, new Object[] { url } );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
channel = ctx.channel();
|
||||
super.handlerAdded( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
handshaker.handshake( ctx.channel() );
|
||||
super.channelActive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
onClosed();
|
||||
super.channelInactive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0( ChannelHandlerContext ctx, Object msg ) throws Exception
|
||||
{
|
||||
Channel ch = ctx.channel();
|
||||
if( !handshaker.isHandshakeComplete() )
|
||||
{
|
||||
handshaker.finishHandshake( ch, (FullHttpResponse) msg );
|
||||
computer.queueEvent( SUCCESS_EVENT, new Object[] { url, this } );
|
||||
return;
|
||||
}
|
||||
|
||||
if( msg instanceof FullHttpResponse )
|
||||
{
|
||||
FullHttpResponse response = (FullHttpResponse) msg;
|
||||
throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString( CharsetUtil.UTF_8 ) + ')' );
|
||||
}
|
||||
|
||||
WebSocketFrame frame = (WebSocketFrame) msg;
|
||||
if( frame instanceof TextWebSocketFrame )
|
||||
{
|
||||
computer.queueEvent( MESSAGE_EVENT, new Object[] { url, ((TextWebSocketFrame) frame).text() } );
|
||||
}
|
||||
else if( frame instanceof BinaryWebSocketFrame )
|
||||
{
|
||||
ByteBuf data = frame.content();
|
||||
byte[] converted = new byte[ data.readableBytes() ];
|
||||
data.readBytes( converted );
|
||||
computer.queueEvent( MESSAGE_EVENT, new Object[] { url, data } );
|
||||
}
|
||||
else if( frame instanceof CloseWebSocketFrame )
|
||||
{
|
||||
ch.close();
|
||||
onClosed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
|
||||
{
|
||||
ctx.close();
|
||||
computer.queueEvent( FAILURE_EVENT, new Object[] {
|
||||
url,
|
||||
cause instanceof WebSocketHandshakeException ? cause.getMessage() : "Could not connect"
|
||||
} );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String[] getMethodNames()
|
||||
{
|
||||
return new String[] { "receive", "send", "close" };
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
|
||||
{
|
||||
switch( method )
|
||||
{
|
||||
case 0:
|
||||
while( true )
|
||||
{
|
||||
checkOpen();
|
||||
Object[] event = context.pullEvent( MESSAGE_EVENT );
|
||||
if( event.length >= 3 && Objects.equal( event[ 1 ], url ) )
|
||||
{
|
||||
return new Object[] { event[ 2 ] };
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
checkOpen();
|
||||
String text = arguments.length > 0 && arguments[ 0 ] != null ? arguments[ 0 ].toString() : "";
|
||||
channel.writeAndFlush( new TextWebSocketFrame( text ) );
|
||||
return null;
|
||||
}
|
||||
case 2:
|
||||
close( true );
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOpen() throws LuaException
|
||||
{
|
||||
if( !open ) throw new LuaException( "attempt to use a closed file" );
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.core.apis.HTTPAPI;
|
||||
import dan200.computercraft.core.apis.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
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.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
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 io.netty.handler.ssl.SslContextBuilder;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/*
|
||||
* Provides functionality to verify and connect to a remote websocket.
|
||||
*/
|
||||
public final class WebsocketConnector
|
||||
{
|
||||
private static final Object lock = new Object();
|
||||
private static TrustManagerFactory trustManager;
|
||||
|
||||
private WebsocketConnector()
|
||||
{
|
||||
}
|
||||
|
||||
private static TrustManagerFactory getTrustManager()
|
||||
{
|
||||
if( trustManager != null ) return trustManager;
|
||||
synchronized( lock )
|
||||
{
|
||||
if( trustManager != null ) return trustManager;
|
||||
|
||||
TrustManagerFactory tmf = null;
|
||||
try
|
||||
{
|
||||
tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() );
|
||||
tmf.init( (KeyStore) null );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
ComputerCraft.log.error( "Cannot setup trust manager", e );
|
||||
}
|
||||
|
||||
return trustManager = tmf;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
uri = new URI( "ws://" + uri.toString() );
|
||||
}
|
||||
catch( URISyntaxException e )
|
||||
{
|
||||
throw new HTTPRequestException( "URL malformed" );
|
||||
}
|
||||
}
|
||||
else if( !scheme.equalsIgnoreCase( "wss" ) && !scheme.equalsIgnoreCase( "ws" ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Invalid scheme '" + scheme + "'" );
|
||||
}
|
||||
|
||||
if( !ComputerCraft.http_whitelist.matches( uri.getHost() ) || ComputerCraft.http_blacklist.matches( uri.getHost() ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
public static int getPort( URI uri ) throws HTTPRequestException
|
||||
{
|
||||
int port = uri.getPort();
|
||||
if( port >= 0 ) return port;
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if( scheme.equalsIgnoreCase( "ws" ) )
|
||||
{
|
||||
return 80;
|
||||
}
|
||||
else if( scheme.equalsIgnoreCase( "wss" ) )
|
||||
{
|
||||
return 443;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HTTPRequestException( "Invalid scheme '" + scheme + "'" );
|
||||
}
|
||||
}
|
||||
|
||||
public static Future<?> createConnector( final IAPIEnvironment environment, final HTTPAPI api, final URI uri, final String address, final int port, final Map<String, String> headers )
|
||||
{
|
||||
return HTTPExecutor.EXECUTOR.submit( () -> {
|
||||
InetAddress resolved;
|
||||
try
|
||||
{
|
||||
resolved = HTTPRequest.checkHost( uri.getHost() );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
environment.queueEvent( WebsocketConnection.FAILURE_EVENT, new Object[] { address, e.getMessage() } );
|
||||
return;
|
||||
}
|
||||
|
||||
InetSocketAddress socketAddress = new InetSocketAddress( resolved, uri.getPort() == -1 ? port : uri.getPort() );
|
||||
|
||||
final SslContext ssl;
|
||||
if( uri.getScheme().equalsIgnoreCase( "wss" ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
ssl = SslContextBuilder.forClient().trustManager( getTrustManager() ).build();
|
||||
}
|
||||
catch( SSLException e )
|
||||
{
|
||||
environment.queueEvent( WebsocketConnection.FAILURE_EVENT, new Object[] { address, "Cannot create secure socket" } );
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ssl = null;
|
||||
}
|
||||
|
||||
HttpHeaders httpHeaders = new DefaultHttpHeaders();
|
||||
for( Map.Entry<String, String> header : headers.entrySet() )
|
||||
{
|
||||
httpHeaders.add( header.getKey(), header.getValue() );
|
||||
}
|
||||
|
||||
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( uri, WebSocketVersion.V13, null, false, httpHeaders );
|
||||
final WebsocketConnection connection = new WebsocketConnection( environment, api, handshaker, address );
|
||||
|
||||
new Bootstrap()
|
||||
.group( HTTPExecutor.LOOP_GROUP )
|
||||
.channel( NioSocketChannel.class )
|
||||
.handler( new ChannelInitializer<SocketChannel>()
|
||||
{
|
||||
@Override
|
||||
protected void initChannel( SocketChannel ch ) throws Exception
|
||||
{
|
||||
ChannelPipeline p = ch.pipeline();
|
||||
if( ssl != null ) p.addLast( ssl.newHandler( ch.alloc(), uri.getHost(), port ) );
|
||||
p.addLast( new HttpClientCodec(), new HttpObjectAggregator( 8192 ), connection );
|
||||
}
|
||||
} )
|
||||
.remoteAddress( socketAddress )
|
||||
.connect();
|
||||
} );
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ gui.computercraft:wired_modem.peripheral_connected=Peripheral "%s" connected to
|
||||
gui.computercraft:wired_modem.peripheral_disconnected=Peripheral "%s" disconnected from network
|
||||
|
||||
gui.computercraft:config.http_enable=Enable HTTP API
|
||||
gui.computercraft:config.http_websocket_enable=Enable HTTP websockets
|
||||
gui.computercraft:config.http_whitelist=HTTP whitelist
|
||||
gui.computercraft:config.http_blacklist=HTTP blacklist
|
||||
gui.computercraft:config.disable_lua51_features=Disable Lua 5.1 features
|
||||
|
Loading…
Reference in New Issue
Block a user