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_enable = true;
|
||||||
|
public static boolean http_websocket_enable = true;
|
||||||
public static AddressPredicate http_whitelist = new AddressPredicate( DEFAULT_HTTP_WHITELIST );
|
public static AddressPredicate http_whitelist = new AddressPredicate( DEFAULT_HTTP_WHITELIST );
|
||||||
public static AddressPredicate http_blacklist = new AddressPredicate( DEFAULT_HTTP_BLACKLIST );
|
public static AddressPredicate http_blacklist = new AddressPredicate( DEFAULT_HTTP_BLACKLIST );
|
||||||
public static boolean disable_lua51_features = false;
|
public static boolean disable_lua51_features = false;
|
||||||
@ -200,6 +201,7 @@ public class ComputerCraft
|
|||||||
public static Configuration config;
|
public static Configuration config;
|
||||||
|
|
||||||
public static Property http_enable;
|
public static Property http_enable;
|
||||||
|
public static Property http_websocket_enable;
|
||||||
public static Property http_whitelist;
|
public static Property http_whitelist;
|
||||||
public static Property http_blacklist;
|
public static Property http_blacklist;
|
||||||
public static Property disable_lua51_features;
|
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 = 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_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 );
|
ConfigCategory category = Config.config.getCategory( Configuration.CATEGORY_GENERAL );
|
||||||
Property currentProperty = category.get( "http_whitelist" );
|
Property currentProperty = category.get( "http_whitelist" );
|
||||||
@ -362,6 +367,7 @@ public class ComputerCraft
|
|||||||
public static void syncConfig() {
|
public static void syncConfig() {
|
||||||
|
|
||||||
http_enable = Config.http_enable.getBoolean();
|
http_enable = Config.http_enable.getBoolean();
|
||||||
|
http_websocket_enable = Config.http_websocket_enable.getBoolean();
|
||||||
http_whitelist = new AddressPredicate( Config.http_whitelist.getStringList() );
|
http_whitelist = new AddressPredicate( Config.http_whitelist.getStringList() );
|
||||||
http_blacklist = new AddressPredicate( Config.http_blacklist.getStringList() );
|
http_blacklist = new AddressPredicate( Config.http_blacklist.getStringList() );
|
||||||
disable_lua51_features = Config.disable_lua51_features.getBoolean();
|
disable_lua51_features = Config.disable_lua51_features.getBoolean();
|
||||||
|
@ -6,29 +6,37 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.apis;
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.api.lua.ILuaContext;
|
import dan200.computercraft.api.lua.ILuaContext;
|
||||||
import dan200.computercraft.api.lua.LuaException;
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
import dan200.computercraft.core.apis.http.HTTPCheck;
|
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.HTTPRequest;
|
||||||
import dan200.computercraft.core.apis.http.HTTPTask;
|
import dan200.computercraft.core.apis.http.WebsocketConnector;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import static dan200.computercraft.core.apis.ArgumentHelper.*;
|
import static dan200.computercraft.core.apis.ArgumentHelper.*;
|
||||||
|
|
||||||
public class HTTPAPI implements ILuaAPI
|
public class HTTPAPI implements ILuaAPI
|
||||||
{
|
{
|
||||||
private final IAPIEnvironment m_apiEnvironment;
|
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 )
|
public HTTPAPI( IAPIEnvironment environment )
|
||||||
{
|
{
|
||||||
m_apiEnvironment = environment;
|
m_apiEnvironment = environment;
|
||||||
m_httpTasks = new ArrayList<>();
|
m_httpTasks = new ArrayList<>();
|
||||||
|
m_closeables = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] getNames()
|
public String[] getNames()
|
||||||
{
|
{
|
||||||
@ -48,15 +56,11 @@ public class HTTPAPI implements ILuaAPI
|
|||||||
// Wait for all of our http requests
|
// Wait for all of our http requests
|
||||||
synchronized( m_httpTasks )
|
synchronized( m_httpTasks )
|
||||||
{
|
{
|
||||||
Iterator<HTTPTask> it = m_httpTasks.iterator();
|
Iterator<Future<?>> it = m_httpTasks.iterator();
|
||||||
while( it.hasNext() )
|
while( it.hasNext() )
|
||||||
{
|
{
|
||||||
final HTTPTask h = it.next();
|
final Future<?> h = it.next();
|
||||||
if( h.isFinished() )
|
if( h.isDone() ) it.remove();
|
||||||
{
|
|
||||||
h.whenFinished( m_apiEnvironment );
|
|
||||||
it.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,21 +70,36 @@ public class HTTPAPI implements ILuaAPI
|
|||||||
{
|
{
|
||||||
synchronized( m_httpTasks )
|
synchronized( m_httpTasks )
|
||||||
{
|
{
|
||||||
for( HTTPTask r : m_httpTasks )
|
for( Future<?> r : m_httpTasks )
|
||||||
{
|
{
|
||||||
r.cancel();
|
r.cancel( false );
|
||||||
}
|
}
|
||||||
m_httpTasks.clear();
|
m_httpTasks.clear();
|
||||||
}
|
}
|
||||||
|
synchronized( m_closeables )
|
||||||
|
{
|
||||||
|
for( Closeable x : m_closeables )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
x.close();
|
||||||
|
}
|
||||||
|
catch( IOException ignored )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_closeables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String[] getMethodNames()
|
public String[] getMethodNames()
|
||||||
{
|
{
|
||||||
return new String[] {
|
return new String[] {
|
||||||
"request",
|
"request",
|
||||||
"checkURL"
|
"checkURL",
|
||||||
|
"websocket",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +144,10 @@ public class HTTPAPI implements ILuaAPI
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
URL url = HTTPRequest.checkURL( urlString );
|
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 )
|
synchronized( m_httpTasks )
|
||||||
{
|
{
|
||||||
m_httpTasks.add( HTTPTask.submit( request ) );
|
m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( request ) );
|
||||||
}
|
}
|
||||||
return new Object[] { true };
|
return new Object[] { true };
|
||||||
}
|
}
|
||||||
@ -147,9 +166,47 @@ public class HTTPAPI implements ILuaAPI
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
URL url = HTTPRequest.checkURL( urlString );
|
URL url = HTTPRequest.checkURL( urlString );
|
||||||
HTTPCheck check = new HTTPCheck( urlString, url );
|
HTTPCheck check = new HTTPCheck( m_apiEnvironment, urlString, url );
|
||||||
synchronized( m_httpTasks ) {
|
synchronized( m_httpTasks )
|
||||||
m_httpTasks.add( HTTPTask.submit( check ) );
|
{
|
||||||
|
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 };
|
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;
|
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 String urlString;
|
||||||
private final URL url;
|
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.urlString = urlString;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
@ -22,24 +23,12 @@ public class HTTPCheck implements HTTPTask.IHTTPTask
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HTTPRequest.checkHost( url );
|
HTTPRequest.checkHost( url.getHost() );
|
||||||
|
environment.queueEvent( "http_check", new Object[] { urlString, true } );
|
||||||
}
|
}
|
||||||
catch( HTTPRequestException e )
|
catch( HTTPRequestException e )
|
||||||
{
|
{
|
||||||
error = e.getMessage();
|
environment.queueEvent( "http_check", new Object[] { urlString, false, 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 } );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class HTTPRequest implements HTTPTask.IHTTPTask
|
public class HTTPRequest implements Runnable
|
||||||
{
|
{
|
||||||
public static URL checkURL( String urlString ) throws HTTPRequestException
|
public static URL checkURL( String urlString ) throws HTTPRequestException
|
||||||
{
|
{
|
||||||
@ -55,11 +55,11 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InetAddress checkHost( URL url ) throws HTTPRequestException
|
public static InetAddress checkHost( String host ) throws HTTPRequestException
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InetAddress resolved = InetAddress.getByName( url.getHost() );
|
InetAddress resolved = InetAddress.getByName( host );
|
||||||
if( !ComputerCraft.http_whitelist.matches( resolved ) || ComputerCraft.http_blacklist.matches( resolved ) )
|
if( !ComputerCraft.http_whitelist.matches( resolved ) || ComputerCraft.http_blacklist.matches( resolved ) )
|
||||||
{
|
{
|
||||||
throw new HTTPRequestException( "Domain not permitted" );
|
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 URL m_url;
|
||||||
private final String m_urlString;
|
private final String m_urlString;
|
||||||
private final String m_postText;
|
private final String m_postText;
|
||||||
private final Map<String, String> m_headers;
|
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 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_urlString = urlString;
|
||||||
m_url = url;
|
m_url = url;
|
||||||
m_binary = binary;
|
m_binary = binary;
|
||||||
@ -96,28 +90,19 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
|||||||
m_headers = headers;
|
m_headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getContents()
|
|
||||||
{
|
|
||||||
byte[] result = m_result;
|
|
||||||
if( result != null )
|
|
||||||
{
|
|
||||||
return new ByteArrayInputStream( result );
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
// First verify the address is allowed.
|
// First verify the address is allowed.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
checkHost( m_url );
|
checkHost( m_url.getHost() );
|
||||||
}
|
}
|
||||||
catch( HTTPRequestException e )
|
catch( HTTPRequestException e )
|
||||||
{
|
{
|
||||||
m_success = false;
|
// Queue the failure event if not.
|
||||||
m_errorMessage = e.getMessage();
|
String error = e.getMessage();
|
||||||
|
m_environment.queueEvent( "http_failure", new Object[] { m_urlString, error == null ? "Could not connect" : error, null } );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,58 +171,36 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
|
|||||||
byte[] result = ByteStreams.toByteArray( is );
|
byte[] result = ByteStreams.toByteArray( is );
|
||||||
is.close();
|
is.close();
|
||||||
|
|
||||||
// We completed
|
// We've got some sort of response, so let's build a resulting object.
|
||||||
m_success = responseSuccess;
|
|
||||||
m_result = result;
|
|
||||||
m_responseCode = connection.getResponseCode();
|
|
||||||
m_encoding = connection.getContentEncoding();
|
|
||||||
|
|
||||||
Joiner joiner = Joiner.on( ',' );
|
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() )
|
for( Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet() )
|
||||||
{
|
{
|
||||||
headers.put( header.getKey(), joiner.join( header.getValue() ) );
|
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
|
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 )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
// There was an error
|
// There was an error
|
||||||
m_success = false;
|
m_environment.queueEvent( "http_failure", new Object[] { m_urlString, "Could not connect", null } );
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 } );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:wired_modem.peripheral_disconnected=Peripheral "%s" disconnected from network
|
||||||
|
|
||||||
gui.computercraft:config.http_enable=Enable HTTP API
|
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_whitelist=HTTP whitelist
|
||||||
gui.computercraft:config.http_blacklist=HTTP blacklist
|
gui.computercraft:config.http_blacklist=HTTP blacklist
|
||||||
gui.computercraft:config.disable_lua51_features=Disable Lua 5.1 features
|
gui.computercraft:config.disable_lua51_features=Disable Lua 5.1 features
|
||||||
|
Loading…
Reference in New Issue
Block a user