mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-12 02:10:30 +00:00
Make many more http options domain-specific
timetout, max_upload, max_download and max_websocket_message may now be configured on a domain-by-domain basis. This uses the same system that we use for the block/allow-list from before: Example: [[http.rules]] host = "*" action = "allow" max_upload = 4194304 max_download = 16777216 timeout = 30000
This commit is contained in:
parent
4f8217d1ab
commit
7af63d052d
@ -6,7 +6,8 @@
|
||||
package dan200.computercraft;
|
||||
|
||||
import dan200.computercraft.api.turtle.event.TurtleAction;
|
||||
import dan200.computercraft.core.apis.http.AddressRule;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||
import dan200.computercraft.shared.Config;
|
||||
import dan200.computercraft.shared.computer.blocks.BlockComputer;
|
||||
import dan200.computercraft.shared.computer.core.ClientComputerRegistry;
|
||||
@ -70,7 +71,7 @@ public final class ComputerCraft
|
||||
public static boolean disable_lua51_features = false;
|
||||
public static String default_computer_settings = "";
|
||||
public static boolean debug_enable = true;
|
||||
public static boolean logPeripheralErrors = true;
|
||||
public static boolean logComputerErrors = true;
|
||||
public static boolean commandRequireCreative = true;
|
||||
|
||||
public static int computer_threads = 1;
|
||||
@ -80,16 +81,16 @@ public final class ComputerCraft
|
||||
public static boolean httpEnabled = true;
|
||||
public static boolean httpWebsocketEnabled = true;
|
||||
public static List<AddressRule> httpRules = Collections.unmodifiableList( Stream.concat(
|
||||
Stream.of( DEFAULT_HTTP_DENY ).map( x -> AddressRule.parse( x, AddressRule.Action.DENY ) ).filter( Objects::nonNull ),
|
||||
Stream.of( DEFAULT_HTTP_ALLOW ).map( x -> AddressRule.parse( x, AddressRule.Action.ALLOW ) ).filter( Objects::nonNull )
|
||||
Stream.of( DEFAULT_HTTP_DENY )
|
||||
.map( x -> AddressRule.parse( x, Action.DENY.toPartial() ) )
|
||||
.filter( Objects::nonNull ),
|
||||
Stream.of( DEFAULT_HTTP_ALLOW )
|
||||
.map( x -> AddressRule.parse( x, Action.ALLOW.toPartial() ) )
|
||||
.filter( Objects::nonNull )
|
||||
).collect( Collectors.toList() ) );
|
||||
|
||||
public static int httpTimeout = 30000;
|
||||
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 = 128 * 1024;
|
||||
|
||||
public static boolean enableCommandBlock = false;
|
||||
public static int modem_range = 64;
|
||||
|
@ -119,12 +119,6 @@ public class HTTPAPI implements ILuaAPI
|
||||
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 )
|
||||
{
|
||||
throw new HTTPRequestException( "Request body is too large" );
|
||||
}
|
||||
|
||||
// Make the request
|
||||
request.queue( r -> r.request( uri, httpMethod ) );
|
||||
|
||||
|
@ -7,6 +7,7 @@ package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@ -46,7 +47,9 @@ public class CheckUrl extends Resource<CheckUrl>
|
||||
|
||||
try
|
||||
{
|
||||
NetworkUtils.getAddress( host, 80, false );
|
||||
InetSocketAddress address = NetworkUtils.getAddress( host, 80, false );
|
||||
NetworkUtils.getOptions( host, address );
|
||||
|
||||
if( tryClose() ) environment.queueEvent( EVENT, address, true );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
|
@ -6,6 +6,9 @@
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.shared.util.ThreadUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
@ -15,7 +18,6 @@ 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;
|
||||
@ -106,22 +108,29 @@ public final class NetworkUtils
|
||||
* @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.
|
||||
* @throws HTTPRequestException If the host is not malformed.
|
||||
*/
|
||||
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( AddressRule.apply( ComputerCraft.httpRules, host, address ) == AddressRule.Action.DENY )
|
||||
{
|
||||
throw new HTTPRequestException( "Domain not permitted" );
|
||||
return socketAddress;
|
||||
}
|
||||
|
||||
return socketAddress;
|
||||
/**
|
||||
* Get options for a specific domain.
|
||||
*
|
||||
* @param host The host to resolve.
|
||||
* @param address The address, resolved by {@link #getAddress(String, int, boolean)}.
|
||||
* @return The options for this host.
|
||||
* @throws HTTPRequestException If the host is not permitted
|
||||
*/
|
||||
public static Options getOptions( String host, InetSocketAddress address ) throws HTTPRequestException
|
||||
{
|
||||
Options options = AddressRule.apply( ComputerCraft.httpRules, host, address.getAddress() );
|
||||
if( options.action == Action.DENY ) throw new HTTPRequestException( "Domain not permitted" );
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public enum Action
|
||||
{
|
||||
ALLOW,
|
||||
DENY;
|
||||
|
||||
private final PartialOptions partial = new PartialOptions( this, null, null, null, null );
|
||||
|
||||
@Nonnull
|
||||
public PartialOptions toPartial()
|
||||
{
|
||||
return partial;
|
||||
}
|
||||
}
|
@ -3,11 +3,12 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
@ -18,6 +19,11 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public final class AddressRule
|
||||
{
|
||||
public static final long MAX_DOWNLOAD = 16 * 1024 * 1024;
|
||||
public static final long MAX_UPLOAD = 4 * 1024 * 1024;
|
||||
public static final int TIMEOUT = 30_000;
|
||||
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
|
||||
|
||||
private static final class HostRange
|
||||
{
|
||||
private final byte[] min;
|
||||
@ -44,25 +50,19 @@ public final class AddressRule
|
||||
}
|
||||
}
|
||||
|
||||
public enum Action
|
||||
{
|
||||
ALLOW,
|
||||
DENY,
|
||||
}
|
||||
|
||||
private final HostRange ip;
|
||||
private final Pattern domainPattern;
|
||||
private final Action action;
|
||||
private final PartialOptions partial;
|
||||
|
||||
private AddressRule( HostRange ip, Pattern domainPattern, Action action )
|
||||
private AddressRule( @Nullable HostRange ip, @Nullable Pattern domainPattern, @Nonnull PartialOptions partial )
|
||||
{
|
||||
this.ip = ip;
|
||||
this.domainPattern = domainPattern;
|
||||
this.action = action;
|
||||
this.partial = partial;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AddressRule parse( String filter, Action action )
|
||||
public static AddressRule parse( String filter, @Nonnull PartialOptions partial )
|
||||
{
|
||||
int cidr = filter.indexOf( '/' );
|
||||
if( cidr >= 0 )
|
||||
@ -117,12 +117,12 @@ public final class AddressRule
|
||||
size -= 8;
|
||||
}
|
||||
|
||||
return new AddressRule( new HostRange( minBytes, maxBytes ), null, action );
|
||||
return new AddressRule( new HostRange( minBytes, maxBytes ), null, partial );
|
||||
}
|
||||
else
|
||||
{
|
||||
Pattern pattern = Pattern.compile( "^\\Q" + filter.replaceAll( "\\*", "\\\\E.*\\\\Q" ) + "\\E$" );
|
||||
return new AddressRule( null, pattern, action );
|
||||
return new AddressRule( null, pattern, partial );
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ public final class AddressRule
|
||||
* @param address The address to check.
|
||||
* @return Whether it matches any of these patterns.
|
||||
*/
|
||||
public boolean matches( String domain, InetAddress address )
|
||||
private boolean matches( String domain, InetAddress address )
|
||||
{
|
||||
if( domainPattern != null )
|
||||
{
|
||||
@ -155,13 +155,32 @@ public final class AddressRule
|
||||
return ip != null && ip.contains( address );
|
||||
}
|
||||
|
||||
public static Action apply( Iterable<? extends AddressRule> rules, String domain, InetAddress address )
|
||||
public static Options apply( Iterable<? extends AddressRule> rules, String domain, InetAddress address )
|
||||
{
|
||||
PartialOptions options = null;
|
||||
boolean hasMany = false;
|
||||
|
||||
for( AddressRule rule : rules )
|
||||
{
|
||||
if( rule.matches( domain, address ) ) return rule.action;
|
||||
if( !rule.matches( domain, address ) ) continue;
|
||||
|
||||
if( options == null )
|
||||
{
|
||||
options = rule.partial;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if( !hasMany )
|
||||
{
|
||||
options = options.copy();
|
||||
hasMany = true;
|
||||
}
|
||||
|
||||
return Action.DENY;
|
||||
options.merge( rule.partial );
|
||||
}
|
||||
}
|
||||
|
||||
return (options == null ? PartialOptions.DEFAULT : options).toOptions();
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||
import com.electronwill.nightconfig.core.Config;
|
||||
import com.electronwill.nightconfig.core.InMemoryCommentedFormat;
|
||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Parses, checks and generates {@link Config}s for {@link AddressRule}.
|
||||
*/
|
||||
public class AddressRuleConfig
|
||||
{
|
||||
public static UnmodifiableConfig makeRule( String host, Action action )
|
||||
{
|
||||
CommentedConfig config = InMemoryCommentedFormat.defaultInstance().createConfig( ConcurrentHashMap::new );
|
||||
config.add( "host", host );
|
||||
config.add( "action", action.name().toLowerCase( Locale.ROOT ) );
|
||||
|
||||
if( host.equals( "*" ) && action == Action.ALLOW )
|
||||
{
|
||||
config.setComment( "timeout", "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited." );
|
||||
config.add( "timeout", AddressRule.TIMEOUT );
|
||||
|
||||
config.setComment( "max_download", "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." );
|
||||
config.set( "max_download", AddressRule.MAX_DOWNLOAD );
|
||||
|
||||
config.setComment( "max_upload", "The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text." );
|
||||
config.set( "max_upload", AddressRule.MAX_UPLOAD );
|
||||
|
||||
config.setComment( "max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet." );
|
||||
config.set( "max_websocket_message", AddressRule.WEBSOCKET_MESSAGE );
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public static boolean checkRule( UnmodifiableConfig builder )
|
||||
{
|
||||
String hostObj = get( builder, "host", String.class ).orElse( null );
|
||||
return hostObj != null && checkEnum( builder, "action", Action.class )
|
||||
&& check( builder, "timeout", Number.class )
|
||||
&& check( builder, "max_upload", Number.class )
|
||||
&& check( builder, "max_download", Number.class )
|
||||
&& check( builder, "websocket_message", Number.class )
|
||||
&& AddressRule.parse( hostObj, PartialOptions.DEFAULT ) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AddressRule parseRule( UnmodifiableConfig builder )
|
||||
{
|
||||
String hostObj = get( builder, "host", String.class ).orElse( null );
|
||||
if( hostObj == null ) return null;
|
||||
|
||||
Action action = getEnum( builder, "action", Action.class ).orElse( null );
|
||||
Integer timeout = get( builder, "timeout", Number.class ).map( Number::intValue ).orElse( null );
|
||||
Long maxUpload = get( builder, "max_upload", Number.class ).map( Number::longValue ).orElse( null );
|
||||
Long maxDownload = get( builder, "max_download", Number.class ).map( Number::longValue ).orElse( null );
|
||||
Integer websocketMessage = get( builder, "websocket_message", Number.class ).map( Number::intValue ).orElse( null );
|
||||
|
||||
PartialOptions options = new PartialOptions(
|
||||
action,
|
||||
maxUpload,
|
||||
maxDownload,
|
||||
timeout,
|
||||
websocketMessage
|
||||
);
|
||||
|
||||
return AddressRule.parse( hostObj, options );
|
||||
}
|
||||
|
||||
private static <T> boolean check( UnmodifiableConfig config, String field, Class<T> klass )
|
||||
{
|
||||
Object value = config.get( field );
|
||||
if( value == null || klass.isInstance( value ) ) return true;
|
||||
|
||||
ComputerCraft.log.warn( "HTTP rule's {} is not a {}.", field, klass.getSimpleName() );
|
||||
return false;
|
||||
}
|
||||
|
||||
private static <T extends Enum<T>> boolean checkEnum( UnmodifiableConfig config, String field, Class<T> klass )
|
||||
{
|
||||
Object value = config.get( field );
|
||||
if( value == null ) return true;
|
||||
|
||||
if( !(value instanceof String) )
|
||||
{
|
||||
ComputerCraft.log.warn( "HTTP rule's {} is not a string", field );
|
||||
return false;
|
||||
}
|
||||
|
||||
if( parseEnum( klass, (String) value ) == null )
|
||||
{
|
||||
ComputerCraft.log.warn( "HTTP rule's {} is not a known option", field );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static <T> Optional<T> get( UnmodifiableConfig config, String field, Class<T> klass )
|
||||
{
|
||||
Object value = config.get( field );
|
||||
return klass.isInstance( value ) ? Optional.of( klass.cast( value ) ) : Optional.empty();
|
||||
}
|
||||
|
||||
private static <T extends Enum<T>> Optional<T> getEnum( UnmodifiableConfig config, String field, Class<T> klass )
|
||||
{
|
||||
return get( config, field, String.class ).map( x -> parseEnum( klass, x ) );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T extends Enum<T>> T parseEnum( Class<T> klass, String x )
|
||||
{
|
||||
for( T value : klass.getEnumConstants() )
|
||||
{
|
||||
if( value.name().equalsIgnoreCase( x ) ) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Options about a specific domain.
|
||||
*/
|
||||
public final class Options
|
||||
{
|
||||
@Nonnull
|
||||
public final Action action;
|
||||
public final long maxUpload;
|
||||
public final long maxDownload;
|
||||
public final int timeout;
|
||||
public final int websocketMessage;
|
||||
|
||||
Options( @Nonnull Action action, long maxUpload, long maxDownload, int timeout, int websocketMessage )
|
||||
{
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.timeout = timeout;
|
||||
this.websocketMessage = websocketMessage;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public final class PartialOptions
|
||||
{
|
||||
static final PartialOptions DEFAULT = new PartialOptions( null, null, null, null, null );
|
||||
|
||||
Action action;
|
||||
Long maxUpload;
|
||||
Long maxDownload;
|
||||
Integer timeout;
|
||||
Integer websocketMessage;
|
||||
|
||||
Options options;
|
||||
|
||||
PartialOptions( Action action, Long maxUpload, Long maxDownload, Integer timeout, Integer websocketMessage )
|
||||
{
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.timeout = timeout;
|
||||
this.websocketMessage = websocketMessage;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
Options toOptions()
|
||||
{
|
||||
if( options != null ) return options;
|
||||
|
||||
return options = new Options(
|
||||
action == null ? Action.DENY : action,
|
||||
maxUpload == null ? AddressRule.MAX_UPLOAD : maxUpload,
|
||||
maxDownload == null ? AddressRule.MAX_DOWNLOAD : maxDownload,
|
||||
timeout == null ? AddressRule.TIMEOUT : timeout,
|
||||
websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : websocketMessage
|
||||
);
|
||||
}
|
||||
|
||||
void merge( @Nonnull PartialOptions other )
|
||||
{
|
||||
if( action == null && other.action != null ) action = other.action;
|
||||
if( maxUpload == null && other.maxUpload != null ) maxUpload = other.maxUpload;
|
||||
if( maxDownload == null && other.maxDownload != null ) maxDownload = other.maxDownload;
|
||||
if( timeout == null && other.timeout != null ) timeout = other.timeout;
|
||||
if( websocketMessage == null && other.websocketMessage != null ) websocketMessage = other.websocketMessage;
|
||||
}
|
||||
|
||||
PartialOptions copy()
|
||||
{
|
||||
return new PartialOptions( action, maxUpload, maxDownload, timeout, websocketMessage );
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ 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.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
@ -136,16 +137,24 @@ public class HttpRequest extends Resource<HttpRequest>
|
||||
{
|
||||
boolean ssl = uri.getScheme().equalsIgnoreCase( "https" );
|
||||
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl );
|
||||
Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress );
|
||||
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if( isClosed() ) return;
|
||||
|
||||
long requestBody = getHeaderSize( headers ) + postBuffer.capacity();
|
||||
if( options.maxUpload != 0 && requestBody > options.maxUpload )
|
||||
{
|
||||
failure( "Request body is too large" );
|
||||
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() );
|
||||
environment.addTrackingChange( TrackingField.HTTP_UPLOAD, requestBody );
|
||||
|
||||
HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method );
|
||||
HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method, options );
|
||||
connectFuture = new Bootstrap()
|
||||
.group( NetworkUtils.LOOP_GROUP )
|
||||
.channelFactory( NioSocketChannel::new )
|
||||
@ -155,9 +164,9 @@ public class HttpRequest extends Resource<HttpRequest>
|
||||
protected void initChannel( SocketChannel ch )
|
||||
{
|
||||
|
||||
if( ComputerCraft.httpTimeout > 0 )
|
||||
if( options.timeout > 0 )
|
||||
{
|
||||
ch.config().setConnectTimeoutMillis( ComputerCraft.httpTimeout );
|
||||
ch.config().setConnectTimeoutMillis( options.timeout );
|
||||
}
|
||||
|
||||
ChannelPipeline p = ch.pipeline();
|
||||
@ -166,9 +175,9 @@ public class HttpRequest extends Resource<HttpRequest>
|
||||
p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) );
|
||||
}
|
||||
|
||||
if( ComputerCraft.httpTimeout > 0 )
|
||||
if( options.timeout > 0 )
|
||||
{
|
||||
p.addLast( new ReadTimeoutHandler( ComputerCraft.httpTimeout, TimeUnit.MILLISECONDS ) );
|
||||
p.addLast( new ReadTimeoutHandler( options.timeout, TimeUnit.MILLISECONDS ) );
|
||||
}
|
||||
|
||||
p.addLast(
|
||||
@ -194,7 +203,7 @@ public class HttpRequest extends Resource<HttpRequest>
|
||||
catch( Exception e )
|
||||
{
|
||||
failure( "Could not connect" );
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error in HTTP request", e );
|
||||
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in HTTP request", e );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.CompositeByteBuf;
|
||||
@ -45,18 +46,20 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
|
||||
private final URI uri;
|
||||
private final HttpMethod method;
|
||||
private final Options options;
|
||||
|
||||
private Charset responseCharset;
|
||||
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
|
||||
private HttpResponseStatus responseStatus;
|
||||
private CompositeByteBuf responseBody;
|
||||
|
||||
HttpRequestHandler( HttpRequest request, URI uri, HttpMethod method )
|
||||
HttpRequestHandler( HttpRequest request, URI uri, HttpMethod method, Options options )
|
||||
{
|
||||
this.request = request;
|
||||
|
||||
this.uri = uri;
|
||||
this.method = method;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -153,7 +156,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
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 )
|
||||
if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload )
|
||||
{
|
||||
closed = true;
|
||||
ctx.close();
|
||||
@ -185,7 +188,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
@Override
|
||||
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause );
|
||||
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause );
|
||||
request.failure( cause );
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ 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.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
@ -130,6 +131,7 @@ public class Websocket extends Resource<Websocket>
|
||||
boolean ssl = uri.getScheme().equalsIgnoreCase( "wss" );
|
||||
|
||||
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl );
|
||||
Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress );
|
||||
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
@ -151,14 +153,14 @@ public class Websocket extends Resource<Websocket>
|
||||
|
||||
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
|
||||
uri, WebSocketVersion.V13, null, true, headers,
|
||||
ComputerCraft.httpMaxWebsocketMessage == 0 ? MAX_MESSAGE_SIZE : ComputerCraft.httpMaxWebsocketMessage
|
||||
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
||||
);
|
||||
|
||||
p.addLast(
|
||||
new HttpClientCodec(),
|
||||
new HttpObjectAggregator( 8192 ),
|
||||
WebSocketClientCompressionHandler.INSTANCE,
|
||||
new WebsocketHandler( Websocket.this, handshaker )
|
||||
new WebsocketHandler( Websocket.this, handshaker, options )
|
||||
);
|
||||
}
|
||||
} )
|
||||
@ -178,15 +180,15 @@ public class Websocket extends Resource<Websocket>
|
||||
catch( Exception e )
|
||||
{
|
||||
failure( "Could not connect" );
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error in websocket", e );
|
||||
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in websocket", e );
|
||||
}
|
||||
}
|
||||
|
||||
void success( Channel channel )
|
||||
void success( Channel channel, Options options )
|
||||
{
|
||||
if( isClosed() ) return;
|
||||
|
||||
WebsocketHandle handle = new WebsocketHandle( this, channel );
|
||||
WebsocketHandle handle = new WebsocketHandle( this, options, channel );
|
||||
environment().queueEvent( SUCCESS_EVENT, address, handle );
|
||||
websocketHandle = createOwnerReference( handle );
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@ -28,13 +28,15 @@ import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EV
|
||||
public class WebsocketHandle implements Closeable
|
||||
{
|
||||
private final Websocket websocket;
|
||||
private final Options options;
|
||||
private boolean closed = false;
|
||||
|
||||
private Channel channel;
|
||||
|
||||
public WebsocketHandle( Websocket websocket, Channel channel )
|
||||
public WebsocketHandle( Websocket websocket, Options options, Channel channel )
|
||||
{
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@ -55,7 +57,7 @@ public class WebsocketHandle implements Closeable
|
||||
checkOpen();
|
||||
|
||||
String text = StringUtil.toString( args.get( 0 ) );
|
||||
if( ComputerCraft.httpMaxWebsocketMessage != 0 && text.length() > ComputerCraft.httpMaxWebsocketMessage )
|
||||
if( options.websocketMessage != 0 && text.length() > options.websocketMessage )
|
||||
{
|
||||
throw new LuaException( "Message is too large" );
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ 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.apis.http.options.Options;
|
||||
import dan200.computercraft.core.tracking.TrackingField;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ConnectTimeoutException;
|
||||
@ -23,11 +24,13 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
|
||||
{
|
||||
private final Websocket websocket;
|
||||
private final WebSocketClientHandshaker handshaker;
|
||||
private final Options options;
|
||||
|
||||
public WebsocketHandler( Websocket websocket, WebSocketClientHandshaker handshaker )
|
||||
public WebsocketHandler( Websocket websocket, WebSocketClientHandshaker handshaker, Options options )
|
||||
{
|
||||
this.handshaker = handshaker;
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -52,7 +55,7 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
|
||||
if( !handshaker.isHandshakeComplete() )
|
||||
{
|
||||
handshaker.finishHandshake( ctx.channel(), (FullHttpResponse) msg );
|
||||
websocket.success( ctx.channel() );
|
||||
websocket.success( ctx.channel(), options );
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -518,7 +518,7 @@ public final class ComputerThread
|
||||
|
||||
private static void timeoutTask( ComputerExecutor executor, Thread thread, long time )
|
||||
{
|
||||
if( !ComputerCraft.logPeripheralErrors ) return;
|
||||
if( !ComputerCraft.logComputerErrors ) return;
|
||||
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append( "Terminating computer #" ).append( executor.getComputer().getID() )
|
||||
|
@ -54,7 +54,7 @@ class BasicFunction extends VarArgFunction
|
||||
}
|
||||
catch( Throwable t )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors )
|
||||
if( ComputerCraft.logComputerErrors )
|
||||
{
|
||||
ComputerCraft.log.error( "Error calling " + name + " on " + instance, t );
|
||||
}
|
||||
|
@ -323,7 +323,7 @@ public class CobaltLuaMachine implements ILuaMachine
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
if( ComputerCraft.logPeripheralErrors )
|
||||
if( ComputerCraft.logComputerErrors )
|
||||
{
|
||||
ComputerCraft.log.warn( "Received unknown type '{}', returning nil.", object.getClass().getName() );
|
||||
}
|
||||
@ -532,7 +532,7 @@ public class CobaltLuaMachine implements ILuaMachine
|
||||
}
|
||||
catch( Throwable t )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error running task", t );
|
||||
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
|
||||
m_computer.queueEvent( "task_complete", new Object[] {
|
||||
taskID, false, "Java Exception Thrown: " + t,
|
||||
} );
|
||||
|
@ -64,7 +64,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
|
||||
}
|
||||
catch( Throwable t )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors )
|
||||
if( ComputerCraft.logComputerErrors )
|
||||
{
|
||||
ComputerCraft.log.error( "Error calling " + name + " on " + instance, t );
|
||||
}
|
||||
@ -95,7 +95,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
|
||||
}
|
||||
catch( Throwable t )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors )
|
||||
if( ComputerCraft.logComputerErrors )
|
||||
{
|
||||
ComputerCraft.log.error( "Error calling " + name + " on " + container.callback, t );
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Converter;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.turtle.event.TurtleAction;
|
||||
import dan200.computercraft.core.apis.http.AddressRule;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
@ -21,7 +21,6 @@ import net.minecraftforge.fml.ModLoadingContext;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@ -55,12 +54,8 @@ public final class Config
|
||||
private static final ConfigValue<Boolean> httpWebsocketEnabled;
|
||||
private static final ConfigValue<List<? extends UnmodifiableConfig>> httpRules;
|
||||
|
||||
private static final ConfigValue<Integer> httpTimeout;
|
||||
private static final ConfigValue<Integer> httpMaxRequests;
|
||||
private static final ConfigValue<Integer> httpMaxDownload;
|
||||
private static final ConfigValue<Integer> httpMaxUpload;
|
||||
private static final ConfigValue<Integer> httpMaxWebsockets;
|
||||
private static final ConfigValue<Integer> httpMaxWebsocketMessage;
|
||||
|
||||
private static final ConfigValue<Boolean> commandBlockEnabled;
|
||||
private static final ConfigValue<Integer> modemRange;
|
||||
@ -121,7 +116,7 @@ public final class Config
|
||||
logComputerErrors = builder
|
||||
.comment( "Log exceptions thrown by peripherals and other Lua objects.\n" +
|
||||
"This makes it easier for mod authors to debug problems, but may result in log spam should people use buggy methods." )
|
||||
.define( "log_computer_errors", ComputerCraft.logPeripheralErrors );
|
||||
.define( "log_computer_errors", ComputerCraft.logComputerErrors );
|
||||
}
|
||||
|
||||
{
|
||||
@ -164,42 +159,25 @@ public final class Config
|
||||
.define( "websocket_enabled", ComputerCraft.httpWebsocketEnabled );
|
||||
|
||||
httpRules = builder
|
||||
.comment( "A list of rules which control which domains or IPs are allowed through the \"http\" API on computers.\n" +
|
||||
"Each rule is an item with a 'host' to match against, and an action. " +
|
||||
.comment( "A list of rules which control behaviour of the \"http\" API for specific domains or IPs.\n" +
|
||||
"Each rule is an item with a 'host' to match against, and a series of properties. " +
|
||||
"The host may be a domain name (\"pastebin.com\"),\n" +
|
||||
"wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\"). 'action' maybe 'allow' or 'block'. If no rules" +
|
||||
"match, the domain will be blocked." )
|
||||
"wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\"). If no rules, the domain is blocked." )
|
||||
.defineList( "rules",
|
||||
Stream.concat(
|
||||
Stream.of( ComputerCraft.DEFAULT_HTTP_DENY ).map( x -> makeRule( x, "deny" ) ),
|
||||
Stream.of( ComputerCraft.DEFAULT_HTTP_ALLOW ).map( x -> makeRule( x, "allow" ) )
|
||||
Stream.of( ComputerCraft.DEFAULT_HTTP_DENY ).map( x -> AddressRuleConfig.makeRule( x, Action.DENY ) ),
|
||||
Stream.of( ComputerCraft.DEFAULT_HTTP_ALLOW ).map( x -> AddressRuleConfig.makeRule( x, Action.ALLOW ) )
|
||||
).collect( Collectors.toList() ),
|
||||
x -> x instanceof UnmodifiableConfig && parseRule( (UnmodifiableConfig) x ) != null );
|
||||
|
||||
httpTimeout = builder
|
||||
.comment( "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited." )
|
||||
.defineInRange( "timeout", ComputerCraft.httpTimeout, 0, Integer.MAX_VALUE );
|
||||
x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule( (UnmodifiableConfig) x ) );
|
||||
|
||||
httpMaxRequests = builder
|
||||
.comment( "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." )
|
||||
.defineInRange( "max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE );
|
||||
|
||||
httpMaxDownload = builder
|
||||
.comment( "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." )
|
||||
.defineInRange( "max_download", (int) ComputerCraft.httpMaxDownload, 0, Integer.MAX_VALUE );
|
||||
|
||||
httpMaxUpload = builder
|
||||
.comment( "The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text." )
|
||||
.defineInRange( "max_upload", (int) ComputerCraft.httpMaxUpload, 0, Integer.MAX_VALUE );
|
||||
|
||||
httpMaxWebsockets = builder
|
||||
.comment( "The number of websockets a computer can have open at one time. Set to 0 for unlimited." )
|
||||
.defineInRange( "max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE );
|
||||
|
||||
httpMaxWebsocketMessage = builder
|
||||
.comment( "The maximum size (in bytes) that a computer can send or receive in one websocket packet." )
|
||||
.defineInRange( "max_websocket_message", ComputerCraft.httpMaxWebsocketMessage, 0, Websocket.MAX_MESSAGE_SIZE );
|
||||
|
||||
builder.pop();
|
||||
}
|
||||
|
||||
@ -291,7 +269,7 @@ public final class Config
|
||||
ComputerCraft.default_computer_settings = defaultComputerSettings.get();
|
||||
ComputerCraft.debug_enable = debugEnabled.get();
|
||||
ComputerCraft.computer_threads = computerThreads.get();
|
||||
ComputerCraft.logPeripheralErrors = logComputerErrors.get();
|
||||
ComputerCraft.logComputerErrors = logComputerErrors.get();
|
||||
|
||||
// Execution
|
||||
ComputerCraft.computer_threads = computerThreads.get();
|
||||
@ -302,14 +280,10 @@ public final class Config
|
||||
ComputerCraft.httpEnabled = httpEnabled.get();
|
||||
ComputerCraft.httpWebsocketEnabled = httpWebsocketEnabled.get();
|
||||
ComputerCraft.httpRules = Collections.unmodifiableList( httpRules.get().stream()
|
||||
.map( Config::parseRule ).filter( Objects::nonNull ).collect( Collectors.toList() ) );
|
||||
.map( AddressRuleConfig::parseRule ).filter( Objects::nonNull ).collect( Collectors.toList() ) );
|
||||
|
||||
ComputerCraft.httpTimeout = httpTimeout.get();
|
||||
ComputerCraft.httpMaxRequests = httpMaxRequests.get();
|
||||
ComputerCraft.httpMaxDownload = httpMaxDownload.get();
|
||||
ComputerCraft.httpMaxUpload = httpMaxUpload.get();
|
||||
ComputerCraft.httpMaxWebsockets = httpMaxWebsockets.get();
|
||||
ComputerCraft.httpMaxWebsocketMessage = httpMaxWebsocketMessage.get();
|
||||
|
||||
// Peripheral
|
||||
ComputerCraft.enableCommandBlock = commandBlockEnabled.get();
|
||||
@ -362,28 +336,4 @@ public final class Config
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static UnmodifiableConfig makeRule( String host, String action )
|
||||
{
|
||||
com.electronwill.nightconfig.core.Config config = com.electronwill.nightconfig.core.Config.inMemory();
|
||||
config.add( "host", host );
|
||||
config.add( "action", action );
|
||||
return config;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static AddressRule parseRule( UnmodifiableConfig builder )
|
||||
{
|
||||
Object hostObj = builder.get( "host" );
|
||||
Object actionObj = builder.get( "action" );
|
||||
if( !(hostObj instanceof String) || !(actionObj instanceof String) ) return null;
|
||||
|
||||
String host = (String) hostObj, action = (String) actionObj;
|
||||
for( AddressRule.Action candiate : AddressRule.Action.values() )
|
||||
{
|
||||
if( candiate.name().equalsIgnoreCase( action ) ) return AddressRule.parse( host, candiate );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ public class CommandAPI implements ILuaAPI
|
||||
}
|
||||
catch( Throwable t )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error running command.", t );
|
||||
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running command.", t );
|
||||
return new Object[] { false, createOutput( "Java Exception Thrown: " + t ) };
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ public class ComputerTestDelegate
|
||||
@BeforeEach
|
||||
public void before() throws IOException
|
||||
{
|
||||
ComputerCraft.logPeripheralErrors = true;
|
||||
ComputerCraft.logComputerErrors = true;
|
||||
|
||||
if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." );
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class ComputerBootstrap
|
||||
|
||||
public static void run( IWritableMount mount, Consumer<Computer> setup, int maxTicks )
|
||||
{
|
||||
ComputerCraft.logPeripheralErrors = true;
|
||||
ComputerCraft.logComputerErrors = true;
|
||||
ComputerCraft.maxMainComputerTime = ComputerCraft.maxMainGlobalTime = Integer.MAX_VALUE;
|
||||
|
||||
Terminal term = new Terminal( ComputerCraft.terminalWidth_computer, ComputerCraft.terminalHeight_computer );
|
||||
|
Loading…
Reference in New Issue
Block a user