mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-05 15:00:29 +00:00
WIP: Http rework (#98)
- Move all HTTP tasks to a unified "MonitoredResource" model. This provides a uniform way of tracking object's lifetimes and disposing of them when complete. - Rewrite HTTP requests to use Netty instead of standard Java. This offers several advantages: - We have access to more HTTP verbs (mostly PATCH). - We can now do http -> https redirects. - We no longer need to spawn in a new thread for each HTTP request. While we do need to run some tasks off-thread in order to resolve IPs, it's generally a much shorter task, and so is less likely to inflate the thread pool. - Introduce several limits for the http API: - There's a limit on how many HTTP requests and websockets may exist at the same time. If the limit is reached, additional ones will be queued up until pending requests have finished. - HTTP requests may upload a maximum of 4Mib and download a maximum of 16Mib (configurable). - .getResponseCode now returns the status text, as well as the status code.
This commit is contained in:
parent
101b3500cc
commit
932f8a44fc
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,13 +1,17 @@
|
||||
# Build directories
|
||||
/classes
|
||||
/logs
|
||||
/build
|
||||
/out
|
||||
|
||||
# Runtime directories
|
||||
/run
|
||||
/run-*
|
||||
/test-files
|
||||
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.idea
|
||||
.gradle
|
||||
/luaj-2.0.3/lib
|
||||
/luaj-2.0.3/*.jar
|
||||
*.DS_Store
|
||||
/test-files
|
||||
|
@ -23,6 +23,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.event.TurtleAction;
|
||||
import dan200.computercraft.core.apis.AddressPredicate;
|
||||
import dan200.computercraft.core.apis.ApiFactories;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import dan200.computercraft.core.filesystem.ComboMount;
|
||||
import dan200.computercraft.core.filesystem.FileMount;
|
||||
import dan200.computercraft.core.filesystem.FileSystemMount;
|
||||
@ -57,6 +58,7 @@ import dan200.computercraft.shared.turtle.blocks.TileTurtle;
|
||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
||||
import dan200.computercraft.shared.util.CreativeTabMain;
|
||||
import dan200.computercraft.shared.util.IDAssigner;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
import dan200.computercraft.shared.wired.CapabilityWiredElement;
|
||||
import dan200.computercraft.shared.wired.WiredNode;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@ -77,7 +79,6 @@ import net.minecraftforge.fml.common.network.NetworkRegistry;
|
||||
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
|
||||
import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper;
|
||||
import net.minecraftforge.fml.relauncher.Side;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.*;
|
||||
@ -137,6 +138,12 @@ public class ComputerCraft
|
||||
public static AddressPredicate http_whitelist = new AddressPredicate( DEFAULT_HTTP_WHITELIST );
|
||||
public static AddressPredicate http_blacklist = new AddressPredicate( DEFAULT_HTTP_BLACKLIST );
|
||||
|
||||
public static int httpMaxRequests = 16;
|
||||
public static long httpMaxDownload = 16 * 1024 * 1024;
|
||||
public static long httpMaxUpload = 4 * 1024 * 1024;
|
||||
public static int httpMaxWebsockets = 4;
|
||||
public static int httpMaxWebsocketMessage = Websocket.MAX_MESSAGE_SIZE;
|
||||
|
||||
public static boolean enableCommandBlock = false;
|
||||
public static int modem_range = 64;
|
||||
public static int modem_highAltitudeRange = 384;
|
||||
@ -599,12 +606,12 @@ public class ComputerCraft
|
||||
}
|
||||
else
|
||||
{
|
||||
IOUtils.closeQuietly( zipFile );
|
||||
IoUtil.closeQuietly( zipFile );
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
if( zipFile != null ) IOUtils.closeQuietly( zipFile );
|
||||
if( zipFile != null ) IoUtil.closeQuietly( zipFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,39 +6,40 @@
|
||||
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.CheckUrl;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceQueue;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
|
||||
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 java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.apis.ArgumentHelper.*;
|
||||
import static dan200.computercraft.core.apis.TableHelper.*;
|
||||
|
||||
public class HTTPAPI implements ILuaAPI
|
||||
{
|
||||
private static final Set<String> HTTP_METHODS = ImmutableSet.of(
|
||||
"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE"
|
||||
);
|
||||
|
||||
private final IAPIEnvironment m_apiEnvironment;
|
||||
private final List<Future<?>> m_httpTasks;
|
||||
private final Set<Closeable> m_closeables;
|
||||
|
||||
private final ResourceQueue<CheckUrl> checkUrls = new ResourceQueue<>();
|
||||
private final ResourceQueue<HttpRequest> requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests );
|
||||
private final ResourceQueue<Websocket> websockets = new ResourceQueue<>( () -> ComputerCraft.httpMaxWebsockets );
|
||||
|
||||
public HTTPAPI( IAPIEnvironment environment )
|
||||
{
|
||||
m_apiEnvironment = environment;
|
||||
m_httpTasks = new ArrayList<>();
|
||||
m_closeables = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -50,45 +51,27 @@ public class HTTPAPI implements ILuaAPI
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update()
|
||||
public void startup()
|
||||
{
|
||||
// Wait for all of our http requests
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
Iterator<Future<?>> it = m_httpTasks.iterator();
|
||||
while( it.hasNext() )
|
||||
{
|
||||
final Future<?> h = it.next();
|
||||
if( h.isDone() ) it.remove();
|
||||
}
|
||||
}
|
||||
checkUrls.startup();
|
||||
requests.startup();
|
||||
websockets.startup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown()
|
||||
{
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
for( Future<?> r : m_httpTasks )
|
||||
{
|
||||
r.cancel( false );
|
||||
}
|
||||
m_httpTasks.clear();
|
||||
}
|
||||
synchronized( m_closeables )
|
||||
{
|
||||
for( Closeable x : m_closeables )
|
||||
{
|
||||
try
|
||||
{
|
||||
x.close();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
m_closeables.clear();
|
||||
}
|
||||
checkUrls.shutdown();
|
||||
requests.shutdown();
|
||||
websockets.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update()
|
||||
{
|
||||
// It's rather ugly to run this here, but we need to clean up
|
||||
// resources as often as possible to reduce blocking.
|
||||
Resource.cleanup();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@ -109,16 +92,16 @@ public class HTTPAPI implements ILuaAPI
|
||||
{
|
||||
case 0: // request
|
||||
{
|
||||
String urlString, postString, requestMethod;
|
||||
String address, postString, requestMethod;
|
||||
Map<Object, Object> headerTable;
|
||||
boolean binary, redirect;
|
||||
|
||||
if( args.length >= 1 && args[0] instanceof Map )
|
||||
{
|
||||
Map<?, ?> options = (Map) args[0];
|
||||
urlString = getStringField( options, "url" );
|
||||
address = getStringField( options, "url" );
|
||||
postString = optStringField( options, "body", null );
|
||||
headerTable = optTableField( options, "headers", null );
|
||||
headerTable = optTableField( options, "headers", Collections.emptyMap() );
|
||||
binary = optBooleanField( options, "binary", false );
|
||||
requestMethod = optStringField( options, "method", null );
|
||||
redirect = optBooleanField( options, "redirect", true );
|
||||
@ -127,43 +110,46 @@ public class HTTPAPI implements ILuaAPI
|
||||
else
|
||||
{
|
||||
// Get URL and post information
|
||||
urlString = getString( args, 0 );
|
||||
address = getString( args, 0 );
|
||||
postString = optString( args, 1, null );
|
||||
headerTable = optTable( args, 2, null );
|
||||
headerTable = optTable( args, 2, Collections.emptyMap() );
|
||||
binary = optBoolean( args, 3, false );
|
||||
requestMethod = null;
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
Map<String, String> headers = null;
|
||||
if( headerTable != null )
|
||||
HttpHeaders headers = getHeaders( headerTable );
|
||||
|
||||
|
||||
HttpMethod httpMethod;
|
||||
if( requestMethod == null )
|
||||
{
|
||||
headers = new HashMap<>( headerTable.size() );
|
||||
for( Object key : headerTable.keySet() )
|
||||
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) );
|
||||
if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) )
|
||||
{
|
||||
Object value = headerTable.get( key );
|
||||
if( key instanceof String && value instanceof String )
|
||||
{
|
||||
headers.put( (String) key, (String) value );
|
||||
}
|
||||
throw new LuaException( "Unsupported HTTP method" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if( requestMethod != null && !HTTP_METHODS.contains( requestMethod ) )
|
||||
{
|
||||
throw new LuaException( "Unsupported HTTP method" );
|
||||
}
|
||||
|
||||
// Make the request
|
||||
try
|
||||
{
|
||||
URL url = HTTPRequest.checkURL( urlString );
|
||||
HTTPRequest request = new HTTPRequest( m_apiEnvironment, urlString, url, postString, headers, binary, requestMethod, redirect );
|
||||
synchronized( m_httpTasks )
|
||||
URI uri = HttpRequest.checkUri( address );
|
||||
|
||||
HttpRequest request = new HttpRequest( requests, m_apiEnvironment, address, postString, headers, binary, redirect );
|
||||
|
||||
long requestBody = request.body().readableBytes() + HttpRequest.getHeaderSize( headers );
|
||||
if( ComputerCraft.httpMaxUpload != 0 && requestBody > ComputerCraft.httpMaxUpload )
|
||||
{
|
||||
m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( request ) );
|
||||
throw new HTTPRequestException( "Request body is too large" );
|
||||
}
|
||||
|
||||
// Make the request
|
||||
request.queue( r -> r.request( uri, httpMethod ) );
|
||||
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
@ -171,21 +157,16 @@ public class HTTPAPI implements ILuaAPI
|
||||
return new Object[] { false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
case 1: // checkURL
|
||||
{
|
||||
// checkURL
|
||||
// Get URL
|
||||
String urlString = getString( args, 0 );
|
||||
String address = getString( args, 0 );
|
||||
|
||||
// Check URL
|
||||
try
|
||||
{
|
||||
URL url = HTTPRequest.checkURL( urlString );
|
||||
HTTPCheck check = new HTTPCheck( m_apiEnvironment, urlString, url );
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( check ) );
|
||||
}
|
||||
URI uri = HttpRequest.checkUri( address );
|
||||
new CheckUrl( checkUrls, m_apiEnvironment, address, uri ).queue( CheckUrl::run );
|
||||
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
@ -198,31 +179,18 @@ public class HTTPAPI implements ILuaAPI
|
||||
String address = getString( args, 0 );
|
||||
Map<Object, Object> headerTbl = optTable( args, 1, Collections.emptyMap() );
|
||||
|
||||
HashMap<String, String> 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" );
|
||||
}
|
||||
|
||||
HttpHeaders headers = getHeaders( headerTbl );
|
||||
|
||||
try
|
||||
{
|
||||
URI uri = WebsocketConnector.checkURI( address );
|
||||
int port = WebsocketConnector.getPort( uri );
|
||||
URI uri = Websocket.checkUri( address );
|
||||
new Websocket( websockets, m_apiEnvironment, uri, address, headers ).queue( Websocket::connect );
|
||||
|
||||
Future<?> connector = WebsocketConnector.createConnector( m_apiEnvironment, this, uri, address, port, headers );
|
||||
synchronized( m_httpTasks )
|
||||
{
|
||||
m_httpTasks.add( connector );
|
||||
}
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
@ -237,19 +205,25 @@ public class HTTPAPI implements ILuaAPI
|
||||
}
|
||||
}
|
||||
|
||||
public void addCloseable( Closeable closeable )
|
||||
@Nonnull
|
||||
private static HttpHeaders getHeaders( @Nonnull Map<?, ?> headerTable ) throws LuaException
|
||||
{
|
||||
synchronized( m_closeables )
|
||||
HttpHeaders headers = new DefaultHttpHeaders();
|
||||
for( Object key : headerTable.keySet() )
|
||||
{
|
||||
m_closeables.add( closeable );
|
||||
}
|
||||
}
|
||||
|
||||
public void removeCloseable( Closeable closeable )
|
||||
{
|
||||
synchronized( m_closeables )
|
||||
{
|
||||
m_closeables.remove( closeable );
|
||||
Object value = headerTable.get( key );
|
||||
if( key instanceof String && value instanceof String )
|
||||
{
|
||||
try
|
||||
{
|
||||
headers.add( (String) key, value );
|
||||
}
|
||||
catch( IllegalArgumentException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
@ -35,14 +36,8 @@ public abstract class HandleGeneric implements ILuaObject
|
||||
|
||||
protected final void close()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_closable.close();
|
||||
m_open = false;
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
m_open = false;
|
||||
IoUtil.closeQuietly( m_closable );
|
||||
m_closable = null;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Checks a URL using {@link NetworkUtils#getAddress(String, int, boolean)}}
|
||||
*
|
||||
* This requires a DNS lookup, and so needs to occur off-thread.
|
||||
*/
|
||||
public class CheckUrl extends Resource<CheckUrl>
|
||||
{
|
||||
private static final String EVENT = "http_check";
|
||||
|
||||
private Future<?> future;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
private final String host;
|
||||
|
||||
public CheckUrl( ResourceQueue<CheckUrl> limiter, IAPIEnvironment environment, String address, URI uri )
|
||||
{
|
||||
super( limiter );
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
this.host = uri.getHost();
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
future = NetworkUtils.EXECUTOR.submit( this::doRun );
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doRun()
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
|
||||
try
|
||||
{
|
||||
NetworkUtils.getAddress( host, 80, false );
|
||||
if( tryClose() ) environment.queueEvent( EVENT, new Object[] { address, true } );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
if( tryClose() ) environment.queueEvent( EVENT, new Object[] { address, false, e.getMessage() } );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
future = closeFuture( future );
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class HTTPCheck implements Runnable
|
||||
{
|
||||
private final IAPIEnvironment environment;
|
||||
private final String urlString;
|
||||
private final URL url;
|
||||
|
||||
public HTTPCheck( IAPIEnvironment environment, String urlString, URL url )
|
||||
{
|
||||
this.environment = environment;
|
||||
this.urlString = urlString;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
HTTPRequest.checkHost( url.getHost() );
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, true } );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
environment.queueEvent( "http_check", new Object[] { urlString, false, e.getMessage() } );
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. 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 dan200.computercraft.shared.util.ThreadUtils;
|
||||
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<>(),
|
||||
ThreadUtils.builder( "HTTP" )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.build()
|
||||
) );
|
||||
|
||||
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, ThreadUtils.builder( "Netty" )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.build()
|
||||
);
|
||||
|
||||
private HTTPExecutor()
|
||||
{
|
||||
}
|
||||
}
|
@ -1,287 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HTTPRequest implements Runnable
|
||||
{
|
||||
public static URL checkURL( String urlString ) throws HTTPRequestException
|
||||
{
|
||||
URL url;
|
||||
try
|
||||
{
|
||||
url = new URL( urlString );
|
||||
}
|
||||
catch( MalformedURLException e )
|
||||
{
|
||||
throw new HTTPRequestException( "URL malformed" );
|
||||
}
|
||||
|
||||
// Validate the URL
|
||||
String protocol = url.getProtocol().toLowerCase();
|
||||
if( !protocol.equals( "http" ) && !protocol.equals( "https" ) )
|
||||
{
|
||||
throw new HTTPRequestException( "URL not http" );
|
||||
}
|
||||
|
||||
// Compare the URL to the whitelist
|
||||
if( !ComputerCraft.http_whitelist.matches( url.getHost() ) || ComputerCraft.http_blacklist.matches( url.getHost() ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
public static InetAddress checkHost( String host ) throws HTTPRequestException
|
||||
{
|
||||
try
|
||||
{
|
||||
InetAddress resolved = InetAddress.getByName( host );
|
||||
if( !ComputerCraft.http_whitelist.matches( resolved ) || ComputerCraft.http_blacklist.matches( resolved ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
catch( UnknownHostException e )
|
||||
{
|
||||
throw new HTTPRequestException( "Unknown host" );
|
||||
}
|
||||
}
|
||||
|
||||
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_binary;
|
||||
private final String m_method;
|
||||
private final boolean m_followRedirects;
|
||||
|
||||
public HTTPRequest( IAPIEnvironment environment, String urlString, URL url, final String postText, final Map<String, String> headers, boolean binary, final String method, final boolean followRedirects ) throws HTTPRequestException
|
||||
{
|
||||
m_environment = environment;
|
||||
m_urlString = urlString;
|
||||
m_url = url;
|
||||
m_binary = binary;
|
||||
m_postText = postText;
|
||||
m_headers = headers;
|
||||
m_method = method;
|
||||
m_followRedirects = followRedirects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
// First verify the address is allowed.
|
||||
try
|
||||
{
|
||||
checkHost( m_url.getHost() );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Connect to the URL
|
||||
HttpURLConnection connection = (HttpURLConnection) m_url.openConnection();
|
||||
|
||||
if( m_postText != null )
|
||||
{
|
||||
connection.setRequestMethod( "POST" );
|
||||
connection.setDoOutput( true );
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.setRequestMethod( "GET" );
|
||||
}
|
||||
if( m_method != null ) connection.setRequestMethod( m_method );
|
||||
connection.setInstanceFollowRedirects( m_followRedirects );
|
||||
|
||||
// Set headers
|
||||
connection.setRequestProperty( "accept-charset", "UTF-8" );
|
||||
if( m_postText != null )
|
||||
{
|
||||
connection.setRequestProperty( "content-type", "application/x-www-form-urlencoded; charset=utf-8" );
|
||||
}
|
||||
if( m_headers != null )
|
||||
{
|
||||
for( Map.Entry<String, String> header : m_headers.entrySet() )
|
||||
{
|
||||
connection.setRequestProperty( header.getKey(), header.getValue() );
|
||||
}
|
||||
}
|
||||
|
||||
// Add request size and count to the tracker before opening the connection
|
||||
m_environment.addTrackingChange( TrackingField.HTTP_REQUESTS );
|
||||
m_environment.addTrackingChange( TrackingField.HTTP_UPLOAD,
|
||||
getHeaderSize( connection.getRequestProperties() ) + (m_postText == null ? 0 : m_postText.length()) );
|
||||
|
||||
// Send POST text
|
||||
if( m_postText != null )
|
||||
{
|
||||
OutputStream os = connection.getOutputStream();
|
||||
OutputStreamWriter osw;
|
||||
try
|
||||
{
|
||||
osw = new OutputStreamWriter( os, "UTF-8" );
|
||||
}
|
||||
catch( UnsupportedEncodingException e )
|
||||
{
|
||||
osw = new OutputStreamWriter( os );
|
||||
}
|
||||
BufferedWriter writer = new BufferedWriter( osw );
|
||||
writer.write( m_postText, 0, m_postText.length() );
|
||||
writer.close();
|
||||
}
|
||||
|
||||
// Read response
|
||||
InputStream is;
|
||||
int code = connection.getResponseCode();
|
||||
boolean responseSuccess;
|
||||
if( code >= 200 && code < 400 )
|
||||
{
|
||||
is = connection.getInputStream();
|
||||
responseSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
is = connection.getErrorStream();
|
||||
responseSuccess = false;
|
||||
}
|
||||
|
||||
byte[] result = ByteStreams.toByteArray( is );
|
||||
is.close();
|
||||
|
||||
String encoding = connection.getContentEncoding();
|
||||
Charset charset = encoding != null && Charset.isSupported( encoding )
|
||||
? Charset.forName( encoding ) : StandardCharsets.UTF_8;
|
||||
|
||||
// We've got some sort of response, so let's build a resulting object.
|
||||
Joiner joiner = Joiner.on( ',' );
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
for( Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet() )
|
||||
{
|
||||
headers.put( header.getKey(), joiner.join( header.getValue() ) );
|
||||
}
|
||||
|
||||
m_environment.addTrackingChange( TrackingField.HTTP_DOWNLOAD,
|
||||
getHeaderSize( connection.getHeaderFields() ) + result.length );
|
||||
|
||||
SeekableByteChannel contents = new ArrayByteChannel( result );
|
||||
ILuaObject stream = wrapStream(
|
||||
m_binary
|
||||
? new BinaryReadableHandle( contents )
|
||||
: new EncodedReadableHandle( EncodedReadableHandle.open( contents, charset ) ),
|
||||
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_environment.queueEvent( "http_failure", new Object[] { m_urlString, "Could not connect", null } );
|
||||
}
|
||||
}
|
||||
|
||||
private static ILuaObject wrapStream( final ILuaObject reader, final int responseCode, final Map<String, String> responseHeaders )
|
||||
{
|
||||
String[] oldMethods = reader.getMethodNames();
|
||||
final int methodOffset = oldMethods.length;
|
||||
|
||||
final String[] newMethods = Arrays.copyOf( oldMethods, oldMethods.length + 2 );
|
||||
newMethods[methodOffset + 0] = "getResponseCode";
|
||||
newMethods[methodOffset + 1] = "getResponseHeaders";
|
||||
|
||||
return new ILuaObject()
|
||||
{
|
||||
@Nonnull
|
||||
@Override
|
||||
public String[] getMethodNames()
|
||||
{
|
||||
return newMethods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException
|
||||
{
|
||||
if( method < methodOffset )
|
||||
{
|
||||
return reader.callMethod( context, method, args );
|
||||
}
|
||||
switch( method - methodOffset )
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
// getResponseCode
|
||||
return new Object[] { responseCode };
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
// getResponseHeaders
|
||||
return new Object[] { responseHeaders };
|
||||
}
|
||||
default:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static long getHeaderSize( Map<String, List<String>> headers )
|
||||
{
|
||||
long size = 0;
|
||||
for( Map.Entry<String, List<String>> header : headers.entrySet() )
|
||||
{
|
||||
size += header.getKey() == null ? 0 : header.getKey().length();
|
||||
for( String value : header.getValue() ) size += value == null ? 0 : value.length() + 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
@ -14,4 +14,10 @@ public class HTTPRequestException extends Exception
|
||||
{
|
||||
super( s );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable fillInStackTrace()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.shared.util.ThreadUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
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.security.KeyStore;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
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 NetworkUtils
|
||||
{
|
||||
public static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
|
||||
4, Integer.MAX_VALUE,
|
||||
60L, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
ThreadUtils.builder( "Network" )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.build()
|
||||
);
|
||||
|
||||
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, ThreadUtils.builder( "Netty" )
|
||||
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
|
||||
.build()
|
||||
);
|
||||
|
||||
private NetworkUtils()
|
||||
{
|
||||
}
|
||||
|
||||
private static final Object sslLock = new Object();
|
||||
private static TrustManagerFactory trustManager;
|
||||
private static SslContext sslContext;
|
||||
private static boolean triedSslContext = false;
|
||||
|
||||
private static TrustManagerFactory getTrustManager()
|
||||
{
|
||||
if( trustManager != null ) return trustManager;
|
||||
synchronized( sslLock )
|
||||
{
|
||||
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 SslContext getSslContext() throws HTTPRequestException
|
||||
{
|
||||
if( sslContext != null || triedSslContext ) return sslContext;
|
||||
synchronized( sslLock )
|
||||
{
|
||||
if( sslContext != null || triedSslContext ) return sslContext;
|
||||
try
|
||||
{
|
||||
return sslContext = SslContextBuilder
|
||||
.forClient()
|
||||
.trustManager( getTrustManager() )
|
||||
.build();
|
||||
}
|
||||
catch( SSLException e )
|
||||
{
|
||||
ComputerCraft.log.error( "Cannot construct SSL context", e );
|
||||
triedSslContext = true;
|
||||
sslContext = null;
|
||||
|
||||
throw new HTTPRequestException( "Cannot create a secure connection" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a host is allowed
|
||||
*
|
||||
* @param host The domain to check against
|
||||
* @throws HTTPRequestException If the host is not permitted.
|
||||
*/
|
||||
public static void checkHost( String host ) throws HTTPRequestException
|
||||
{
|
||||
if( !ComputerCraft.http_whitelist.matches( host ) || ComputerCraft.http_blacklist.matches( host ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link InetSocketAddress} from the resolved {@code host} and port.
|
||||
*
|
||||
* Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
|
||||
*
|
||||
* @param host The host to resolve.
|
||||
* @param port The port, or -1 if not defined.
|
||||
* @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified.
|
||||
* @return The resolved address.
|
||||
* @throws HTTPRequestException If the host is not permitted.
|
||||
*/
|
||||
public static InetSocketAddress getAddress( String host, int port, boolean ssl ) throws HTTPRequestException
|
||||
{
|
||||
if( port < 0 ) port = ssl ? 443 : 80;
|
||||
|
||||
InetSocketAddress socketAddress = new InetSocketAddress( host, port );
|
||||
if( socketAddress.isUnresolved() ) throw new HTTPRequestException( "Unknown host" );
|
||||
|
||||
InetAddress address = socketAddress.getAddress();
|
||||
if( !ComputerCraft.http_whitelist.matches( address ) || ComputerCraft.http_blacklist.matches( address ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
}
|
||||
|
||||
return socketAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a {@link ByteBuf} into a byte array.
|
||||
*
|
||||
* @param buffer The buffer to read.
|
||||
* @return The resulting bytes.
|
||||
*/
|
||||
public static byte[] toBytes( ByteBuf buffer )
|
||||
{
|
||||
byte[] bytes = new byte[buffer.readableBytes()];
|
||||
buffer.readBytes( bytes );
|
||||
return bytes;
|
||||
}
|
||||
}
|
145
src/main/java/dan200/computercraft/core/apis/http/Resource.java
Normal file
145
src/main/java/dan200/computercraft/core/apis/http/Resource.java
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A holder for one or more resources, with a lifetime.
|
||||
*/
|
||||
public abstract class Resource<T extends Resource<T>> implements Closeable
|
||||
{
|
||||
private final AtomicBoolean closed = new AtomicBoolean( false );
|
||||
private final ResourceQueue<T> limiter;
|
||||
|
||||
protected Resource( ResourceQueue<T> limiter )
|
||||
{
|
||||
this.limiter = limiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this resource is closed.
|
||||
*
|
||||
* @return Whether this resource is closed.
|
||||
*/
|
||||
public final boolean isClosed()
|
||||
{
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this has been cancelled. If so, it'll clean up any
|
||||
* existing resources and cancel any pending futures.
|
||||
*/
|
||||
public final boolean checkClosed()
|
||||
{
|
||||
if( !closed.get() ) return false;
|
||||
dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to close the current resource.
|
||||
*
|
||||
* @return Whether this was successfully closed, or {@code false} if it has already been closed.
|
||||
*/
|
||||
protected final boolean tryClose()
|
||||
{
|
||||
if( closed.getAndSet( true ) ) return false;
|
||||
dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any pending resources
|
||||
*
|
||||
* Note, this may be called multiple times, and so should be thread-safe and
|
||||
* avoid any major side effects.
|
||||
*/
|
||||
protected void dispose()
|
||||
{
|
||||
@SuppressWarnings( "unchecked" ) T thisT = (T) this;
|
||||
limiter.release( thisT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link WeakReference} which will close {@code this} when collected.
|
||||
*
|
||||
* @param object The object to reference to
|
||||
* @return The weak reference.
|
||||
*/
|
||||
protected <R> WeakReference<R> createOwnerReference( R object )
|
||||
{
|
||||
return new CloseReference<>( this, object );
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close()
|
||||
{
|
||||
tryClose();
|
||||
}
|
||||
|
||||
public void queue( Consumer<T> task )
|
||||
{
|
||||
@SuppressWarnings( "unchecked" ) T thisT = (T) this;
|
||||
limiter.queue( thisT, () -> task.accept( thisT ) );
|
||||
}
|
||||
|
||||
protected static <T extends Closeable> T closeCloseable( T closeable )
|
||||
{
|
||||
if( closeable != null ) IoUtil.closeQuietly( closeable );
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static ChannelFuture closeChannel( ChannelFuture future )
|
||||
{
|
||||
if( future != null )
|
||||
{
|
||||
future.cancel( false );
|
||||
Channel channel = future.channel();
|
||||
if( channel != null && channel.isOpen() ) channel.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static <T extends Future<?>> T closeFuture( T future )
|
||||
{
|
||||
if( future != null ) future.cancel( true );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
|
||||
|
||||
private static class CloseReference<T> extends WeakReference<T>
|
||||
{
|
||||
final Resource<?> resource;
|
||||
|
||||
CloseReference( Resource<?> resource, T referent )
|
||||
{
|
||||
super( referent, QUEUE );
|
||||
this.resource = resource;
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanup()
|
||||
{
|
||||
Reference<?> reference;
|
||||
while( (reference = QUEUE.poll()) != null ) ((CloseReference) reference).resource.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A queue for {@link Resource}s, with built-in rate-limiting.
|
||||
*/
|
||||
public class ResourceQueue<T extends Resource<T>>
|
||||
{
|
||||
private static final IntSupplier ZERO = () -> 0;
|
||||
|
||||
private final IntSupplier limit;
|
||||
|
||||
private boolean active = false;
|
||||
|
||||
private final Set<T> resources = new HashSet<>();
|
||||
private final ArrayDeque<Supplier<T>> pending = new ArrayDeque<>();
|
||||
|
||||
public ResourceQueue( IntSupplier limit )
|
||||
{
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public ResourceQueue()
|
||||
{
|
||||
this.limit = ZERO;
|
||||
}
|
||||
|
||||
public void startup()
|
||||
{
|
||||
active = true;
|
||||
}
|
||||
|
||||
public synchronized void shutdown()
|
||||
{
|
||||
active = false;
|
||||
|
||||
pending.clear();
|
||||
for( T resource : resources ) resource.close();
|
||||
resources.clear();
|
||||
|
||||
Resource.cleanup();
|
||||
}
|
||||
|
||||
public void queue( T resource, Runnable setup )
|
||||
{
|
||||
queue( () -> {
|
||||
setup.run();
|
||||
return resource;
|
||||
} );
|
||||
}
|
||||
|
||||
public synchronized void queue( Supplier<T> resource )
|
||||
{
|
||||
Resource.cleanup();
|
||||
if( !active ) return;
|
||||
|
||||
int limit = this.limit.getAsInt();
|
||||
if( limit <= 0 || resources.size() < limit )
|
||||
{
|
||||
resources.add( resource.get() );
|
||||
}
|
||||
else
|
||||
{
|
||||
pending.add( resource );
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void release( T resource )
|
||||
{
|
||||
if( !active ) return;
|
||||
|
||||
resources.remove( resource );
|
||||
|
||||
int limit = this.limit.getAsInt();
|
||||
if( limit <= 0 || resources.size() < limit )
|
||||
{
|
||||
Supplier<T> next = pending.poll();
|
||||
if( next != null ) resources.add( next.get() );
|
||||
}
|
||||
}
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
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 dan200.computercraft.core.tracking.TrackingField;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
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.util.Arrays;
|
||||
|
||||
import static dan200.computercraft.core.apis.ArgumentHelper.optBoolean;
|
||||
|
||||
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()
|
||||
{
|
||||
close( false );
|
||||
}
|
||||
|
||||
private void onClosed( int status, String reason )
|
||||
{
|
||||
close( true );
|
||||
|
||||
computer.queueEvent( CLOSE_EVENT, new Object[] {
|
||||
url,
|
||||
Strings.isNullOrEmpty( reason ) ? null : reason,
|
||||
status < 0 ? null : status,
|
||||
} );
|
||||
}
|
||||
|
||||
@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( -1, "Websocket is inactive" );
|
||||
super.channelInactive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0( ChannelHandlerContext ctx, Object msg )
|
||||
{
|
||||
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 )
|
||||
{
|
||||
String data = ((TextWebSocketFrame) frame).text();
|
||||
|
||||
computer.addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() );
|
||||
computer.queueEvent( MESSAGE_EVENT, new Object[] { url, data, false } );
|
||||
}
|
||||
else if( frame instanceof BinaryWebSocketFrame )
|
||||
{
|
||||
ByteBuf data = frame.content();
|
||||
byte[] converted = new byte[data.readableBytes()];
|
||||
data.readBytes( converted );
|
||||
|
||||
computer.addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length );
|
||||
computer.queueEvent( MESSAGE_EVENT, new Object[] { url, converted, true } );
|
||||
}
|
||||
else if( frame instanceof CloseWebSocketFrame )
|
||||
{
|
||||
CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) frame;
|
||||
ch.close();
|
||||
onClosed( closeFrame.statusCode(), closeFrame.reasonText() );
|
||||
}
|
||||
}
|
||||
|
||||
@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: // receive
|
||||
while( true )
|
||||
{
|
||||
checkOpen();
|
||||
Object[] event = context.pullEvent( MESSAGE_EVENT );
|
||||
if( event.length >= 3 && Objects.equal( event[1], url ) )
|
||||
{
|
||||
return Arrays.copyOfRange( event, 2, event.length );
|
||||
}
|
||||
}
|
||||
case 1: // send
|
||||
{
|
||||
checkOpen();
|
||||
String text = arguments.length > 0 && arguments[0] != null ? arguments[0].toString() : "";
|
||||
boolean binary = optBoolean( arguments, 1, false );
|
||||
computer.addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() );
|
||||
channel.writeAndFlush( binary
|
||||
? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( StringUtil.encodeString( text ) ) )
|
||||
: 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" );
|
||||
}
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. 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.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.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
|
||||
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, true, 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 ),
|
||||
WebSocketClientCompressionHandler.INSTANCE,
|
||||
connection
|
||||
);
|
||||
}
|
||||
} )
|
||||
.remoteAddress( socketAddress )
|
||||
.connect();
|
||||
} );
|
||||
}
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceQueue;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.ConnectTimeoutException;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.TooLongFrameException;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Represents one or more
|
||||
*/
|
||||
public class HttpRequest extends Resource<HttpRequest>
|
||||
{
|
||||
private static final String SUCCESS_EVENT = "http_success";
|
||||
private static final String FAILURE_EVENT = "http_failure";
|
||||
|
||||
private static final int MAX_REDIRECTS = 16;
|
||||
|
||||
private static final int TIMEOUT = 30000;
|
||||
|
||||
private Future<?> executorFuture;
|
||||
private ChannelFuture connectFuture;
|
||||
private HttpRequestHandler currentRequest;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
|
||||
private final String address;
|
||||
private final ByteBuf postBuffer;
|
||||
private final HttpHeaders headers;
|
||||
private final boolean binary;
|
||||
|
||||
final AtomicInteger redirects;
|
||||
|
||||
public HttpRequest( ResourceQueue<HttpRequest> limiter, IAPIEnvironment environment, String address, String postText, HttpHeaders headers, boolean binary, boolean followRedirects )
|
||||
{
|
||||
super( limiter );
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
this.postBuffer = postText != null
|
||||
? Unpooled.wrappedBuffer( postText.getBytes( StandardCharsets.UTF_8 ) )
|
||||
: Unpooled.buffer( 0 );
|
||||
this.headers = headers;
|
||||
this.binary = binary;
|
||||
this.redirects = new AtomicInteger( followRedirects ? MAX_REDIRECTS : 0 );
|
||||
|
||||
if( postText != null )
|
||||
{
|
||||
if( !headers.contains( HttpHeaderNames.CONTENT_TYPE ) )
|
||||
{
|
||||
headers.set( HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8" );
|
||||
}
|
||||
|
||||
if( !headers.contains( HttpHeaderNames.CONTENT_LENGTH ) )
|
||||
{
|
||||
headers.set( HttpHeaderNames.CONTENT_LENGTH, postBuffer.readableBytes() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment()
|
||||
{
|
||||
return environment;
|
||||
}
|
||||
|
||||
public static URI checkUri( String address ) throws HTTPRequestException
|
||||
{
|
||||
URI url;
|
||||
try
|
||||
{
|
||||
url = new URI( address );
|
||||
}
|
||||
catch( URISyntaxException e )
|
||||
{
|
||||
throw new HTTPRequestException( "URL malformed" );
|
||||
}
|
||||
|
||||
checkUri( url );
|
||||
return url;
|
||||
}
|
||||
|
||||
public static void checkUri( URI url ) throws HTTPRequestException
|
||||
{
|
||||
// Validate the URL
|
||||
if( url.getScheme() == null ) throw new HTTPRequestException( "Must specify http or https" );
|
||||
|
||||
String scheme = url.getScheme().toLowerCase( Locale.ROOT );
|
||||
if( !scheme.equalsIgnoreCase( "http" ) && !scheme.equalsIgnoreCase( "https" ) )
|
||||
{
|
||||
throw new HTTPRequestException( "Invalid protocol '" + scheme + "'" );
|
||||
}
|
||||
|
||||
NetworkUtils.checkHost( url.getHost() );
|
||||
}
|
||||
|
||||
public void request( URI uri, HttpMethod method )
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit( () -> doRequest( uri, method ) );
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doRequest( URI uri, HttpMethod method )
|
||||
{
|
||||
// If we're cancelled, abort.
|
||||
if( isClosed() ) return;
|
||||
|
||||
try
|
||||
{
|
||||
boolean ssl = uri.getScheme().equalsIgnoreCase( "https" );
|
||||
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl );
|
||||
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if( isClosed() ) return;
|
||||
|
||||
// Add request size to the tracker before opening the connection
|
||||
environment.addTrackingChange( TrackingField.HTTP_REQUESTS, 1 );
|
||||
environment.addTrackingChange( TrackingField.HTTP_UPLOAD, getHeaderSize( headers ) + postBuffer.capacity() );
|
||||
|
||||
HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method );
|
||||
connectFuture = new Bootstrap()
|
||||
.group( NetworkUtils.LOOP_GROUP )
|
||||
.channelFactory( NioSocketChannel::new )
|
||||
.handler( new ChannelInitializer<SocketChannel>()
|
||||
{
|
||||
@Override
|
||||
protected void initChannel( SocketChannel ch )
|
||||
{
|
||||
|
||||
ch.config().setConnectTimeoutMillis( TIMEOUT );
|
||||
|
||||
ChannelPipeline p = ch.pipeline();
|
||||
if( sslContext != null )
|
||||
{
|
||||
p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) );
|
||||
}
|
||||
p.addLast(
|
||||
new ReadTimeoutHandler( TIMEOUT, TimeUnit.MILLISECONDS ),
|
||||
new HttpClientCodec(),
|
||||
new HttpContentDecompressor(),
|
||||
handler
|
||||
);
|
||||
}
|
||||
} )
|
||||
.remoteAddress( socketAddress )
|
||||
.connect()
|
||||
.addListener( c -> {
|
||||
if( !c.isSuccess() ) failure( c.cause() );
|
||||
} );
|
||||
|
||||
// Do an additional check for cancellation
|
||||
checkClosed();
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
failure( e.getMessage() );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
failure( "Could not connect" );
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error in HTTP request", e );
|
||||
}
|
||||
}
|
||||
|
||||
void failure( String message )
|
||||
{
|
||||
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message } );
|
||||
}
|
||||
|
||||
void failure( Throwable cause )
|
||||
{
|
||||
String message;
|
||||
if( cause instanceof HTTPRequestException )
|
||||
{
|
||||
message = cause.getMessage();
|
||||
}
|
||||
else if( cause instanceof TooLongFrameException )
|
||||
{
|
||||
message = "Response is too large";
|
||||
}
|
||||
else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
|
||||
{
|
||||
message = "Timed out";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Could not connect";
|
||||
}
|
||||
|
||||
failure( message );
|
||||
}
|
||||
|
||||
void failure( String message, ILuaObject object )
|
||||
{
|
||||
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message, object } );
|
||||
}
|
||||
|
||||
void success( ILuaObject object )
|
||||
{
|
||||
if( tryClose() ) environment.queueEvent( SUCCESS_EVENT, new Object[] { address, object } );
|
||||
}
|
||||
|
||||
protected void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture( executorFuture );
|
||||
connectFuture = closeChannel( connectFuture );
|
||||
currentRequest = closeCloseable( currentRequest );
|
||||
}
|
||||
|
||||
public static long getHeaderSize( HttpHeaders headers )
|
||||
{
|
||||
long size = 0;
|
||||
for( Map.Entry<String, String> header : headers )
|
||||
{
|
||||
size += header.getKey() == null ? 0 : header.getKey().length();
|
||||
size += header.getValue() == null ? 0 : header.getValue().length() + 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public ByteBuf body()
|
||||
{
|
||||
return postBuffer;
|
||||
}
|
||||
|
||||
public HttpHeaders headers()
|
||||
{
|
||||
return headers;
|
||||
}
|
||||
|
||||
public boolean isBinary()
|
||||
{
|
||||
return binary;
|
||||
}
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.CompositeByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.*;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize;
|
||||
|
||||
public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpObject> implements Closeable
|
||||
{
|
||||
/**
|
||||
* Same as {@link io.netty.handler.codec.MessageAggregator}.
|
||||
*/
|
||||
private static final int DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS = 1024;
|
||||
|
||||
private static final byte[] EMPTY_BYTES = new byte[0];
|
||||
|
||||
private final HttpRequest request;
|
||||
private boolean closed = false;
|
||||
|
||||
private final URI uri;
|
||||
private final HttpMethod method;
|
||||
|
||||
private Charset responseCharset;
|
||||
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
|
||||
private HttpResponseStatus responseStatus;
|
||||
private CompositeByteBuf responseBody;
|
||||
|
||||
HttpRequestHandler( HttpRequest request, URI uri, HttpMethod method )
|
||||
{
|
||||
this.request = request;
|
||||
|
||||
this.uri = uri;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
if( request.checkClosed() ) return;
|
||||
|
||||
ByteBuf body = request.body();
|
||||
body.resetReaderIndex().retain();
|
||||
|
||||
String requestUri = uri.getRawPath();
|
||||
if( uri.getRawQuery() != null ) requestUri += "?" + uri.getRawQuery();
|
||||
|
||||
FullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body );
|
||||
request.setMethod( method );
|
||||
request.headers().set( this.request.headers() );
|
||||
|
||||
// We force some headers to be always applied
|
||||
if( !request.headers().contains( HttpHeaderNames.ACCEPT_CHARSET ) )
|
||||
{
|
||||
request.headers().set( HttpHeaderNames.ACCEPT_CHARSET, "UTF-8" );
|
||||
}
|
||||
if( !request.headers().contains( HttpHeaderNames.USER_AGENT ) )
|
||||
{
|
||||
request.headers().set( HttpHeaderNames.USER_AGENT, ComputerCraft.MOD_ID + "/" + ComputerCraft.getVersion() );
|
||||
}
|
||||
request.headers().set( HttpHeaderNames.HOST, uri.getHost() );
|
||||
request.headers().set( HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE );
|
||||
|
||||
ctx.channel().writeAndFlush( request );
|
||||
|
||||
super.channelActive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
if( !closed ) request.failure( "Could not connect" );
|
||||
super.channelInactive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0( ChannelHandlerContext ctx, HttpObject message )
|
||||
{
|
||||
if( closed || request.checkClosed() ) return;
|
||||
|
||||
if( message instanceof HttpResponse )
|
||||
{
|
||||
HttpResponse response = (HttpResponse) message;
|
||||
|
||||
if( request.redirects.get() > 0 )
|
||||
{
|
||||
URI redirect = getRedirect( response.status(), response.headers() );
|
||||
if( redirect != null && !uri.equals( redirect ) && request.redirects.getAndDecrement() > 0 )
|
||||
{
|
||||
// If we have a redirect, and don't end up at the same place, then follow it.
|
||||
|
||||
// We mark ourselves as disposed first though, to avoid firing events when the channel
|
||||
// becomes inactive or disposed.
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
try
|
||||
{
|
||||
HttpRequest.checkUri( redirect );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
// If we cannot visit this uri, then fail.
|
||||
request.failure( e.getMessage() );
|
||||
return;
|
||||
}
|
||||
|
||||
request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 );
|
||||
responseStatus = response.status();
|
||||
responseHeaders.add( response.headers() );
|
||||
}
|
||||
|
||||
if( message instanceof HttpContent )
|
||||
{
|
||||
HttpContent content = (HttpContent) message;
|
||||
|
||||
if( responseBody == null )
|
||||
{
|
||||
responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS );
|
||||
}
|
||||
|
||||
ByteBuf partial = content.content();
|
||||
if( partial.isReadable() )
|
||||
{
|
||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
||||
if( ComputerCraft.httpMaxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > ComputerCraft.httpMaxDownload )
|
||||
{
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
request.failure( "Response is too large" );
|
||||
return;
|
||||
}
|
||||
|
||||
responseBody.addComponent( true, partial.retain() );
|
||||
}
|
||||
|
||||
if( message instanceof LastHttpContent )
|
||||
{
|
||||
LastHttpContent last = (LastHttpContent) message;
|
||||
responseHeaders.add( last.trailingHeaders() );
|
||||
|
||||
// Set the content length, if not already given.
|
||||
if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) )
|
||||
{
|
||||
responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() );
|
||||
}
|
||||
|
||||
ctx.close();
|
||||
sendResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause );
|
||||
request.failure( cause );
|
||||
}
|
||||
|
||||
private void sendResponse()
|
||||
{
|
||||
// Read the ByteBuf into a channel.
|
||||
CompositeByteBuf body = responseBody;
|
||||
byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes( body );
|
||||
|
||||
// Decode the headers
|
||||
HttpResponseStatus status = responseStatus;
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
for( Map.Entry<String, String> header : responseHeaders )
|
||||
{
|
||||
String existing = headers.get( header.getKey() );
|
||||
headers.put( header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue() );
|
||||
}
|
||||
|
||||
// Fire off a stats event
|
||||
request.environment().addTrackingChange( TrackingField.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length );
|
||||
|
||||
// Prepare to queue an event
|
||||
ArrayByteChannel contents = new ArrayByteChannel( bytes );
|
||||
final ILuaObject reader = request.isBinary()
|
||||
? new BinaryReadableHandle( contents )
|
||||
: new EncodedReadableHandle( EncodedReadableHandle.open( contents, responseCharset ) );
|
||||
ILuaObject stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers );
|
||||
|
||||
if( status.code() >= 200 && status.code() < 400 )
|
||||
{
|
||||
request.success( stream );
|
||||
}
|
||||
else
|
||||
{
|
||||
request.failure( status.reasonPhrase(), stream );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the redirect from this response
|
||||
*/
|
||||
private URI getRedirect( HttpResponseStatus status, HttpHeaders headers )
|
||||
{
|
||||
int code = status.code();
|
||||
if( code < 300 || code > 307 || code == 304 || code == 306 ) return null;
|
||||
|
||||
String location = headers.get( HttpHeaderNames.LOCATION );
|
||||
if( location == null ) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return uri.resolve( new URI( URLDecoder.decode( location, "UTF-8" ) ) );
|
||||
}
|
||||
catch( UnsupportedEncodingException | IllegalArgumentException | URISyntaxException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
if( responseBody != null )
|
||||
{
|
||||
responseBody.release();
|
||||
responseBody = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Wraps a {@link dan200.computercraft.core.apis.handles.HandleGeneric} and provides additional methods for
|
||||
* getting the response code and headers.
|
||||
*/
|
||||
public class HttpResponseHandle implements ILuaObject
|
||||
{
|
||||
private final String[] newMethods;
|
||||
private final int methodOffset;
|
||||
private final ILuaObject reader;
|
||||
private final int responseCode;
|
||||
private final String responseStatus;
|
||||
private final Map<String, String> responseHeaders;
|
||||
|
||||
public HttpResponseHandle(@Nonnull ILuaObject reader, int responseCode, String responseStatus, @Nonnull Map<String, String> responseHeaders)
|
||||
{
|
||||
this.reader = reader;
|
||||
this.responseCode = responseCode;
|
||||
this.responseStatus = responseStatus;
|
||||
this.responseHeaders = responseHeaders;
|
||||
|
||||
String[] oldMethods = reader.getMethodNames();
|
||||
final int methodOffset = this.methodOffset = oldMethods.length;
|
||||
|
||||
final String[] newMethods = this.newMethods = Arrays.copyOf( oldMethods, oldMethods.length + 2 );
|
||||
newMethods[methodOffset + 0] = "getResponseCode";
|
||||
newMethods[methodOffset + 1] = "getResponseHeaders";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String[] getMethodNames()
|
||||
{
|
||||
return newMethods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException
|
||||
{
|
||||
if( method < methodOffset ) return reader.callMethod( context, method, args );
|
||||
|
||||
switch( method - methodOffset )
|
||||
{
|
||||
case 0: // getResponseCode
|
||||
return new Object[] { responseCode, responseStatus };
|
||||
case 1: // getResponseHeaders
|
||||
return new Object[] { responseHeaders };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceQueue;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
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.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Provides functionality to verify and connect to a remote websocket.
|
||||
*/
|
||||
public class Websocket extends Resource<Websocket>
|
||||
{
|
||||
public static final int MAX_MESSAGE_SIZE = 64 * 1024;
|
||||
|
||||
static final String SUCCESS_EVENT = "websocket_success";
|
||||
static final String FAILURE_EVENT = "websocket_failure";
|
||||
static final String CLOSE_EVENT = "websocket_closed";
|
||||
static final String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
private Future<?> executorFuture;
|
||||
private ChannelFuture connectFuture;
|
||||
private WeakReference<WebsocketHandle> websocketHandle;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final URI uri;
|
||||
private final String address;
|
||||
private final HttpHeaders headers;
|
||||
|
||||
public Websocket( ResourceQueue<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers )
|
||||
{
|
||||
super( limiter );
|
||||
this.environment = environment;
|
||||
this.uri = uri;
|
||||
this.address = address;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public static URI checkUri( String address ) throws HTTPRequestException
|
||||
{
|
||||
URI uri = null;
|
||||
try
|
||||
{
|
||||
uri = new URI( address );
|
||||
}
|
||||
catch( URISyntaxException ignored )
|
||||
{
|
||||
}
|
||||
|
||||
if( uri == null || uri.getHost() == null )
|
||||
{
|
||||
try
|
||||
{
|
||||
uri = new URI( "ws://" + address );
|
||||
}
|
||||
catch( URISyntaxException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if( uri == null || uri.getHost() == null ) throw new HTTPRequestException( "URL malformed" );
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if( scheme == null )
|
||||
{
|
||||
try
|
||||
{
|
||||
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 + "'" );
|
||||
}
|
||||
|
||||
NetworkUtils.checkHost( uri.getHost() );
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void connect()
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit( this::doConnect );
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doConnect()
|
||||
{
|
||||
// If we're cancelled, abort.
|
||||
if( isClosed() ) return;
|
||||
|
||||
try
|
||||
{
|
||||
boolean ssl = uri.getScheme().equalsIgnoreCase( "wss" );
|
||||
|
||||
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl );
|
||||
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if( isClosed() ) return;
|
||||
|
||||
connectFuture = new Bootstrap()
|
||||
.group( NetworkUtils.LOOP_GROUP )
|
||||
.channel( NioSocketChannel.class )
|
||||
.handler( new ChannelInitializer<SocketChannel>()
|
||||
{
|
||||
@Override
|
||||
protected void initChannel( SocketChannel ch )
|
||||
{
|
||||
ChannelPipeline p = ch.pipeline();
|
||||
if( sslContext != null )
|
||||
{
|
||||
p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) );
|
||||
}
|
||||
|
||||
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
|
||||
uri, WebSocketVersion.V13, null, true, headers,
|
||||
ComputerCraft.httpMaxWebsocketMessage == 0 ? MAX_MESSAGE_SIZE : ComputerCraft.httpMaxWebsocketMessage
|
||||
);
|
||||
|
||||
p.addLast(
|
||||
new HttpClientCodec(),
|
||||
new HttpObjectAggregator( 8192 ),
|
||||
WebSocketClientCompressionHandler.INSTANCE,
|
||||
new WebsocketHandler( Websocket.this, handshaker )
|
||||
);
|
||||
}
|
||||
} )
|
||||
.remoteAddress( socketAddress )
|
||||
.connect();
|
||||
|
||||
// Do an additional check for cancellation
|
||||
checkClosed();
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
failure( e.getMessage() );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
failure( "Could not connect" );
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error in websocket", e );
|
||||
}
|
||||
}
|
||||
|
||||
void success( Channel channel )
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
|
||||
WebsocketHandle handle = new WebsocketHandle( this, channel );
|
||||
environment().queueEvent( SUCCESS_EVENT, new Object[] { address, handle } );
|
||||
this.websocketHandle = createOwnerReference( handle );
|
||||
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
void failure( String message )
|
||||
{
|
||||
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message } );
|
||||
}
|
||||
|
||||
void close( int status, String reason )
|
||||
{
|
||||
if( tryClose() )
|
||||
{
|
||||
environment.queueEvent( CLOSE_EVENT, new Object[] {
|
||||
address,
|
||||
Strings.isNullOrEmpty( reason ) ? null : reason,
|
||||
status < 0 ? null : status,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture( executorFuture );
|
||||
connectFuture = closeChannel( connectFuture );
|
||||
|
||||
WeakReference<WebsocketHandle> websocketHandleRef = this.websocketHandle;
|
||||
WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
||||
if( websocketHandle != null ) IoUtil.closeQuietly( websocketHandle );
|
||||
this.websocketHandle = null;
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment()
|
||||
{
|
||||
return environment;
|
||||
}
|
||||
|
||||
public String address()
|
||||
{
|
||||
return address;
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaObject;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static dan200.computercraft.core.apis.ArgumentHelper.optBoolean;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
|
||||
public class WebsocketHandle implements ILuaObject, Closeable
|
||||
{
|
||||
private final Websocket websocket;
|
||||
private boolean closed = false;
|
||||
|
||||
private Channel channel;
|
||||
|
||||
public WebsocketHandle( Websocket websocket, Channel channel )
|
||||
{
|
||||
this.websocket = websocket;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@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: // receive
|
||||
while( true )
|
||||
{
|
||||
checkOpen();
|
||||
|
||||
Object[] event = context.pullEvent( MESSAGE_EVENT );
|
||||
if( event.length >= 3 && Objects.equal( event[1], websocket.address() ) )
|
||||
{
|
||||
return Arrays.copyOfRange( event, 2, event.length );
|
||||
}
|
||||
}
|
||||
|
||||
case 1: // send
|
||||
{
|
||||
checkOpen();
|
||||
|
||||
String text = arguments.length > 0 && arguments[0] != null ? arguments[0].toString() : "";
|
||||
if( ComputerCraft.httpMaxWebsocketMessage != 0 && text.length() > ComputerCraft.httpMaxWebsocketMessage )
|
||||
{
|
||||
throw new LuaException( "Message is too large" );
|
||||
}
|
||||
|
||||
boolean binary = optBoolean( arguments, 1, false );
|
||||
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() );
|
||||
|
||||
Channel channel = this.channel;
|
||||
if( channel != null )
|
||||
{
|
||||
channel.writeAndFlush( binary
|
||||
? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( StringUtil.encodeString( text ) ) )
|
||||
: new TextWebSocketFrame( text ) );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
case 2: // close
|
||||
close();
|
||||
websocket.close();
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOpen() throws LuaException
|
||||
{
|
||||
if( closed ) throw new LuaException( "attempt to use a closed file" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
|
||||
Channel channel = this.channel;
|
||||
if( channel != null )
|
||||
{
|
||||
channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ConnectTimeoutException;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.TooLongFrameException;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.websocketx.*;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
|
||||
public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
|
||||
{
|
||||
private final Websocket websocket;
|
||||
private final WebSocketClientHandshaker handshaker;
|
||||
|
||||
public WebsocketHandler( Websocket websocket, WebSocketClientHandshaker handshaker )
|
||||
{
|
||||
this.handshaker = handshaker;
|
||||
this.websocket = websocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
handshaker.handshake( ctx.channel() );
|
||||
super.channelActive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
websocket.close( -1, "Websocket is inactive" );
|
||||
super.channelInactive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0( ChannelHandlerContext ctx, Object msg )
|
||||
{
|
||||
if( websocket.isClosed() ) return;
|
||||
|
||||
if( !handshaker.isHandshakeComplete() )
|
||||
{
|
||||
handshaker.finishHandshake( ctx.channel(), (FullHttpResponse) msg );
|
||||
websocket.success( ctx.channel() );
|
||||
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 )
|
||||
{
|
||||
String data = ((TextWebSocketFrame) frame).text();
|
||||
|
||||
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() );
|
||||
websocket.environment().queueEvent( MESSAGE_EVENT, new Object[] { websocket.address(), data, false } );
|
||||
}
|
||||
else if( frame instanceof BinaryWebSocketFrame )
|
||||
{
|
||||
byte[] converted = NetworkUtils.toBytes( frame.content() );
|
||||
|
||||
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length );
|
||||
websocket.environment().queueEvent( MESSAGE_EVENT, new Object[] { websocket.address(), converted, true } );
|
||||
}
|
||||
else if( frame instanceof CloseWebSocketFrame )
|
||||
{
|
||||
CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) frame;
|
||||
websocket.close( closeFrame.statusCode(), closeFrame.reasonText() );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
|
||||
{
|
||||
ctx.close();
|
||||
|
||||
String message;
|
||||
if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException )
|
||||
{
|
||||
message = cause.getMessage();
|
||||
}
|
||||
else if( cause instanceof TooLongFrameException )
|
||||
{
|
||||
message = "Message is too large";
|
||||
}
|
||||
else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
|
||||
{
|
||||
message = "Timed out";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Could not connect";
|
||||
}
|
||||
|
||||
websocket.failure( message );
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
@ -334,7 +335,7 @@ public class FileSystem
|
||||
// Close all dangling open files
|
||||
synchronized( m_openFiles )
|
||||
{
|
||||
for( Closeable file : m_openFiles.values() ) closeQuietly( file );
|
||||
for( Closeable file : m_openFiles.values() ) IoUtil.closeQuietly( file );
|
||||
m_openFiles.clear();
|
||||
while( m_openFileQueue.poll() != null ) ;
|
||||
}
|
||||
@ -655,7 +656,7 @@ public class FileSystem
|
||||
while( (ref = m_openFileQueue.poll()) != null )
|
||||
{
|
||||
Closeable file = m_openFiles.remove( ref );
|
||||
if( file != null ) closeQuietly( file );
|
||||
if( file != null ) IoUtil.closeQuietly( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -667,7 +668,7 @@ public class FileSystem
|
||||
if( ComputerCraft.maximumFilesOpen > 0 &&
|
||||
m_openFiles.size() >= ComputerCraft.maximumFilesOpen )
|
||||
{
|
||||
closeQuietly( file );
|
||||
IoUtil.closeQuietly( file );
|
||||
throw new FileSystemException( "Too many files already open" );
|
||||
}
|
||||
|
||||
@ -881,15 +882,4 @@ public class FileSystem
|
||||
return local;
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeQuietly( Closeable c )
|
||||
{
|
||||
try
|
||||
{
|
||||
c.close();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.google.common.base.Converter;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.turtle.event.TurtleAction;
|
||||
import dan200.computercraft.core.apis.AddressPredicate;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import net.minecraftforge.common.config.ConfigCategory;
|
||||
import net.minecraftforge.common.config.ConfigElement;
|
||||
import net.minecraftforge.common.config.Configuration;
|
||||
@ -35,11 +36,6 @@ public class Config
|
||||
|
||||
private static Configuration config;
|
||||
|
||||
private static Property httpEnable;
|
||||
private static Property httpWebsocketEnable;
|
||||
private static Property httpWhitelist;
|
||||
private static Property httpBlacklist;
|
||||
|
||||
private static Property computerSpaceLimit;
|
||||
private static Property floppySpaceLimit;
|
||||
private static Property maximumFilesOpen;
|
||||
@ -49,12 +45,16 @@ public class Config
|
||||
private static Property computerThreads;
|
||||
private static Property logComputerErrors;
|
||||
|
||||
private static Property turtlesNeedFuel;
|
||||
private static Property turtleFuelLimit;
|
||||
private static Property advancedTurtleFuelLimit;
|
||||
private static Property turtlesObeyBlockProtection;
|
||||
private static Property turtlesCanPush;
|
||||
private static Property turtleDisabledActions;
|
||||
private static Property httpEnable;
|
||||
private static Property httpWebsocketEnable;
|
||||
private static Property httpWhitelist;
|
||||
private static Property httpBlacklist;
|
||||
|
||||
private static Property httpMaxRequests;
|
||||
private static Property httpMaxDownload;
|
||||
private static Property httpMaxUpload;
|
||||
private static Property httpMaxWebsockets;
|
||||
private static Property httpMaxWebsocketMessage;
|
||||
|
||||
private static Property commandBlockEnabled;
|
||||
private static Property modemRange;
|
||||
@ -63,6 +63,13 @@ public class Config
|
||||
private static Property modemHighAltitudeRangeDuringStorm;
|
||||
private static Property maxNotesPerTick;
|
||||
|
||||
private static Property turtlesNeedFuel;
|
||||
private static Property turtleFuelLimit;
|
||||
private static Property advancedTurtleFuelLimit;
|
||||
private static Property turtlesObeyBlockProtection;
|
||||
private static Property turtlesCanPush;
|
||||
private static Property turtleDisabledActions;
|
||||
|
||||
public static void load( File configFile )
|
||||
{
|
||||
config = new Configuration( configFile );
|
||||
@ -135,9 +142,31 @@ public class Config
|
||||
"If this is empty then all whitelisted domains will be accessible. Example: \"*.github.com\" will block access to all subdomains of github.com.\n" +
|
||||
"You can use domain names (\"pastebin.com\"), wilcards (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\")." );
|
||||
|
||||
httpMaxRequests = config.get( CATEGORY_HTTP, "max_requests", ComputerCraft.httpMaxRequests );
|
||||
httpMaxRequests.setComment( "The number of http requests a computer can make at one time. Additional requests will be queued, and sent when the running requests have finished. Set to 0 for unlimited." );
|
||||
httpMaxRequests.setMinValue( 0 );
|
||||
|
||||
httpMaxDownload = config.get( CATEGORY_HTTP, "max_download", (int) ComputerCraft.httpMaxDownload );
|
||||
httpMaxDownload.setComment( "The maximum size (in bytes) that a computer can download in a single request. Note that responses may receive more data than allowed, but this data will not be returned to the client." );
|
||||
httpMaxDownload.setMinValue( 0 );
|
||||
|
||||
httpMaxUpload = config.get( CATEGORY_HTTP, "max_upload", (int) ComputerCraft.httpMaxUpload );
|
||||
httpMaxUpload.setComment( "The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text." );
|
||||
httpMaxUpload.setMinValue( 0 );
|
||||
|
||||
httpMaxWebsockets = config.get( CATEGORY_HTTP, "max_websockets", ComputerCraft.httpMaxWebsockets );
|
||||
httpMaxWebsockets.setComment( "The number of websockets a computer can have open at one time. Set to 0 for unlimited." );
|
||||
httpMaxWebsockets.setMinValue( 1 );
|
||||
|
||||
httpMaxWebsocketMessage = config.get( CATEGORY_HTTP, "max_websocket_message", ComputerCraft.httpMaxWebsocketMessage );
|
||||
httpMaxWebsocketMessage.setComment( "The maximum size (in bytes) that a computer can send or receive in one websocket packet." );
|
||||
httpMaxWebsocketMessage.setMinValue( 0 );
|
||||
httpMaxWebsocketMessage.setMaxValue( Websocket.MAX_MESSAGE_SIZE );
|
||||
|
||||
setOrder(
|
||||
CATEGORY_HTTP,
|
||||
httpEnable, httpWebsocketEnable, httpWhitelist, httpBlacklist
|
||||
httpEnable, httpWebsocketEnable, httpWhitelist, httpBlacklist,
|
||||
httpMaxRequests, httpMaxDownload, httpMaxUpload, httpMaxWebsockets, httpMaxWebsocketMessage
|
||||
);
|
||||
}
|
||||
|
||||
@ -286,6 +315,12 @@ public class Config
|
||||
ComputerCraft.http_whitelist = new AddressPredicate( httpWhitelist.getStringList() );
|
||||
ComputerCraft.http_blacklist = new AddressPredicate( httpBlacklist.getStringList() );
|
||||
|
||||
ComputerCraft.httpMaxRequests = Math.max( 1, httpMaxRequests.getInt() );
|
||||
ComputerCraft.httpMaxDownload = Math.max( 0, httpMaxDownload.getLong() );
|
||||
ComputerCraft.httpMaxUpload = Math.max( 0, httpMaxUpload.getLong() );
|
||||
ComputerCraft.httpMaxWebsockets = Math.max( 1, httpMaxWebsockets.getInt() );
|
||||
ComputerCraft.httpMaxWebsocketMessage = Math.max( 0, httpMaxWebsocketMessage.getInt() );
|
||||
|
||||
// Peripheral
|
||||
ComputerCraft.enableCommandBlock = commandBlockEnabled.getBoolean();
|
||||
ComputerCraft.maxNotesPerTick = Math.max( 1, maxNotesPerTick.getInt() );
|
||||
|
@ -64,6 +64,13 @@ public class ClientComputer extends ClientTerminal implements IComputer
|
||||
return m_instanceID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public int getID()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public String getLabel()
|
||||
|
@ -13,6 +13,9 @@ public interface IComputer extends ITerminal
|
||||
{
|
||||
int getInstanceID();
|
||||
|
||||
@Deprecated
|
||||
int getID();
|
||||
|
||||
@Deprecated
|
||||
String getLabel();
|
||||
|
||||
|
@ -226,6 +226,8 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
|
||||
return m_instanceID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings( "deprecation" )
|
||||
public int getID()
|
||||
{
|
||||
return m_computer.getID();
|
||||
|
@ -9,6 +9,7 @@ package dan200.computercraft.shared.util;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class IDAssigner
|
||||
{
|
||||
@ -71,14 +72,7 @@ public class IDAssigner
|
||||
{
|
||||
FileInputStream in = new FileInputStream( lastidFile );
|
||||
InputStreamReader isr;
|
||||
try
|
||||
{
|
||||
isr = new InputStreamReader( in, "UTF-8" );
|
||||
}
|
||||
catch( UnsupportedEncodingException e )
|
||||
{
|
||||
isr = new InputStreamReader( in );
|
||||
}
|
||||
isr = new InputStreamReader( in, StandardCharsets.UTF_8 );
|
||||
try( BufferedReader br = new BufferedReader( isr ) )
|
||||
{
|
||||
idString = br.readLine();
|
||||
|
24
src/main/java/dan200/computercraft/shared/util/IoUtil.java
Normal file
24
src/main/java/dan200/computercraft/shared/util/IoUtil.java
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public class IoUtil
|
||||
{
|
||||
public static void closeQuietly( Closeable closeable )
|
||||
{
|
||||
try
|
||||
{
|
||||
closeable.close();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -59,6 +59,12 @@ gui.computercraft:config.http.websocket_enabled=Enable websockets
|
||||
gui.computercraft:config.http.whitelist=HTTP whitelist
|
||||
gui.computercraft:config.http.blacklist=HTTP blacklist
|
||||
|
||||
gui.computercraft:config.http.max_requests=Maximum concurrent requests
|
||||
gui.computercraft:config.http.max_download=Maximum response size
|
||||
gui.computercraft:config.http.max_upload=Maximum request
|
||||
gui.computercraft:config.http.max_websockets=Maximum concurrent websockets
|
||||
gui.computercraft:config.http.max_websocket_message=Maximum websocket message size
|
||||
|
||||
gui.computercraft:config.peripheral=Peripherals
|
||||
gui.computercraft:config.peripheral.command_block_enabled=Enable command block peripheral
|
||||
gui.computercraft:config.peripheral.modem_range=Modem range (default)
|
||||
|
Loading…
Reference in New Issue
Block a user