diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 88b413c39..71833ce7f 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -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(); diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 67ccc4413..8fd82d4ee 100644 --- a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -6,29 +6,37 @@ 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 m_httpTasks; - + private final List> m_httpTasks; + private final Set m_closeables; + public HTTPAPI( IAPIEnvironment environment ) { m_apiEnvironment = environment; m_httpTasks = new ArrayList<>(); + m_closeables = new HashSet<>(); } - + @Override public String[] getNames() { @@ -48,15 +56,11 @@ public class HTTPAPI implements ILuaAPI // Wait for all of our http requests synchronized( m_httpTasks ) { - Iterator it = m_httpTasks.iterator(); + Iterator> 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 headerTbl = optTable( args, 1, Collections.emptyMap() ); + + HashMap headers = new HashMap( 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 ); + } + } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPCheck.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPCheck.java index ee3da0072..401d11e95 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/HTTPCheck.java +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPCheck.java @@ -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() } ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPExecutor.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPExecutor.java new file mode 100644 index 000000000..a4f75a316 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPExecutor.java @@ -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(), + 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() + { + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java index d50df523c..28f41c362 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java @@ -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 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 m_responseHeaders; - private String m_errorMessage; - public HTTPRequest( String urlString, URL url, final String postText, final Map headers, boolean binary ) throws HTTPRequestException + public HTTPRequest( IAPIEnvironment environment, String urlString, URL url, final String postText, final Map 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 headers = m_responseHeaders = new HashMap<>(); + Map headers = new HashMap<>(); for( Map.Entry> 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 } ); } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPTask.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPTask.java deleted file mode 100644 index c510bf5d5..000000000 --- a/src/main/java/dan200/computercraft/core/apis/http/HTTPTask.java +++ /dev/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(), - 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 ); - } -} diff --git a/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnection.java b/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnection.java new file mode 100644 index 000000000..ddfc12110 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnection.java @@ -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 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" ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnector.java b/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnector.java new file mode 100644 index 000000000..16f8a50ea --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/WebsocketConnector.java @@ -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 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 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() + { + @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(); + } ); + } +} diff --git a/src/main/resources/assets/computercraft/lang/en_us.lang b/src/main/resources/assets/computercraft/lang/en_us.lang index edc0b8a0d..709bbd714 100644 --- a/src/main/resources/assets/computercraft/lang/en_us.lang +++ b/src/main/resources/assets/computercraft/lang/en_us.lang @@ -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