1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-17 10:50:01 +00:00

Document HTTP rules a little better

It turns out we don't document the "port" option anywhere, so probably
worth doing a bit of an overhaul here.

 - Expand the top-level HTTP rules comment, clarifying how things are
   matched and describing each field.

 - Improve the comments on the default HTTP rule. We now also describe
   the $private rule and its motivation.

 - Don't drop/ignore invalid rules. This gets written back to the
   original config file, so is very annoying! Instead we now log an
   error and convert the rule into a "deny all" rule, which should make
   it obvious something is wrong.
This commit is contained in:
Jonathan Coates 2023-07-01 15:57:30 +01:00
parent 655d5aeca8
commit ecf880ed82
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
8 changed files with 124 additions and 111 deletions

View File

@ -4,21 +4,19 @@
package dan200.computercraft.shared.config;
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.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.apis.http.options.InvalidRuleException;
import dan200.computercraft.core.apis.http.options.PartialOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentHashMap;
import java.util.*;
import java.util.function.Consumer;
/**
* Parses, checks and generates {@link Config}s for {@link AddressRule}.
@ -26,49 +24,65 @@
class AddressRuleConfig {
private static final Logger LOG = LoggerFactory.getLogger(AddressRuleConfig.class);
public static UnmodifiableConfig makeRule(String host, Action action) {
var config = InMemoryCommentedFormat.defaultInstance().createConfig(ConcurrentHashMap::new);
config.add("host", host);
config.add("action", action.name().toLowerCase(Locale.ROOT));
private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial());
if (host.equals("*") && action == Action.ALLOW) {
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);
public static List<UnmodifiableConfig> defaultRules() {
return List.of(
makeRule(config -> {
config.setComment("host", """
The magic "$private" host matches all private address ranges, such as localhost and 192.168.0.0/16.
This rule prevents computers accessing internal services, and is strongly recommended.""");
config.add("host", "$private");
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("action", "Deny all requests to private IP addresses.");
config.add("action", Action.DENY.name().toLowerCase(Locale.ROOT));
}),
makeRule(config -> {
config.setComment("host", """
The wildcard "*" rule matches all remaining hosts.""");
config.add("host", "*");
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);
config.setComment("action", "Allow all non-denied hosts.");
config.add("action", Action.ALLOW.name().toLowerCase(Locale.ROOT));
config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured.");
config.set("use_proxy", false);
}
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);
config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured.");
config.set("use_proxy", false);
})
);
}
private static UnmodifiableConfig makeRule(Consumer<CommentedConfig> setup) {
var config = InMemoryCommentedFormat.defaultInstance().createConfig(LinkedHashMap::new);
setup.accept(config);
return config;
}
public static boolean checkRule(UnmodifiableConfig builder) {
var hostObj = get(builder, "host", String.class).orElse(null);
var port = unboxOptInt(get(builder, "port", Number.class));
return hostObj != null && checkEnum(builder, "action", Action.class)
&& check(builder, "port", Number.class)
&& check(builder, "max_upload", Number.class)
&& check(builder, "max_download", Number.class)
&& check(builder, "websocket_message", Number.class)
&& check(builder, "use_proxy", Boolean.class)
&& AddressRule.parse(hostObj, port, PartialOptions.DEFAULT) != null;
public static AddressRule parseRule(UnmodifiableConfig builder) {
try {
return doParseRule(builder);
} catch (InvalidRuleException e) {
LOG.error("Malformed HTTP rule: {} HTTP will NOT work until this is fixed.", e.getMessage());
return REJECT_ALL;
}
}
@Nullable
public static AddressRule parseRule(UnmodifiableConfig builder) {
public static AddressRule doParseRule(UnmodifiableConfig builder) {
var hostObj = get(builder, "host", String.class).orElse(null);
if (hostObj == null) return null;
if (hostObj == null) throw new InvalidRuleException("No 'host' specified");
var action = getEnum(builder, "action", Action.class).orElse(null);
var port = unboxOptInt(get(builder, "port", Number.class));
@ -88,38 +102,19 @@ public static AddressRule parseRule(UnmodifiableConfig builder) {
return AddressRule.parse(hostObj, port, options);
}
private static <T> boolean check(UnmodifiableConfig config, String field, Class<T> klass) {
var value = config.get(field);
if (value == null || klass.isInstance(value)) return true;
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) {
var value = config.get(field);
if (value == null) return true;
if (!(value instanceof String)) {
LOG.warn("HTTP rule's {} is not a string", field);
return false;
}
if (parseEnum(klass, (String) value) == null) {
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) {
var value = config.get(field);
return klass.isInstance(value) ? Optional.of(klass.cast(value)) : Optional.empty();
if (value == null) return Optional.empty();
if (klass.isInstance(value)) return Optional.of(klass.cast(value));
throw new InvalidRuleException(String.format(
"Field '%s' should be a '%s' but is a %s.",
field, klass.getSimpleName(), value.getClass().getSimpleName()
));
}
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));
return get(config, field, String.class).map(x -> parseEnum(field, klass, x));
}
private static OptionalLong unboxOptLong(Optional<? extends Number> value) {
@ -130,11 +125,14 @@ private static OptionalInt unboxOptInt(Optional<? extends Number> value) {
return value.map(Number::intValue).map(OptionalInt::of).orElse(OptionalInt.empty());
}
@Nullable
private static <T extends Enum<T>> T parseEnum(Class<T> klass, String x) {
private static <T extends Enum<T>> T parseEnum(String field, Class<T> klass, String x) {
for (var value : klass.getEnumConstants()) {
if (value.name().equalsIgnoreCase(x)) return value;
}
return null;
throw new InvalidRuleException(String.format(
"Field '%s' should be one of %s, but is '%s'.",
field, Arrays.stream(klass.getEnumConstants()).map(Enum::name).toList(), x
));
}
}

View File

@ -9,7 +9,6 @@
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.ProxyType;
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
@ -20,9 +19,7 @@
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class ConfigSpec {
@ -182,9 +179,9 @@ private ConfigSpec() {
httpEnabled = builder
.comment("""
Enable the "http" API on Computers. This also disables the "pastebin" and "wget"
programs, that many users rely on. It's recommended to leave this on and use the
"rules" config option to impose more fine-grained control.""")
Enable the "http" API on Computers. Disabling this also disables the "pastebin" and
"wget" programs, that many users rely on. It's recommended to leave this on and use
the "rules" config option to impose more fine-grained control.""")
.define("enabled", CoreConfig.httpEnabled);
httpWebsocketEnabled = builder
@ -194,16 +191,23 @@ private ConfigSpec() {
httpRules = builder
.comment("""
A list of rules which control behaviour of the "http" API for specific domains or
IPs. Each rule is an item with a 'host' to match against, and a series of
properties. Rules are evaluated in order, meaning earlier rules override later
ones.
The host may be a domain name ("pastebin.com"), wildcard ("*.pastebin.com") or
CIDR notation ("127.0.0.0/8").
If no rules, the domain is blocked.""")
.defineList("rules", Arrays.asList(
AddressRuleConfig.makeRule("$private", Action.DENY),
AddressRuleConfig.makeRule("*", Action.ALLOW)
), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule((UnmodifiableConfig) x));
IPs. Each rule matches against a hostname and an optional port, and then sets several
properties for the request. Rules are evaluated in order, meaning earlier rules override
later ones.
Valid properties:
- "host" (required): The domain or IP address this rule matches. This may be a domain name
("pastebin.com"), wildcard ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
- "port" (optional): Only match requests for a specific port, such as 80 or 443.
- "action" (optional): Whether to allow or deny this request.
- "max_download" (optional): The maximum size (in bytes) that a computer can download in this
request.
- "max_upload" (optional): The maximum size (in bytes) that a computer can upload in a this request.
- "max_websocket_message" (optional): The maximum size (in bytes) that a computer can send or
receive in one websocket packet.
- "use_proxy" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.""")
.defineList("rules", AddressRuleConfig.defaultRules(), x -> x instanceof UnmodifiableConfig);
httpMaxRequests = builder
.comment("""
@ -395,8 +399,8 @@ public static void syncServer(@Nullable Path path) {
// HTTP
CoreConfig.httpEnabled = httpEnabled.get();
CoreConfig.httpWebsocketEnabled = httpWebsocketEnabled.get();
CoreConfig.httpRules = httpRules.get().stream()
.map(AddressRuleConfig::parseRule).filter(Objects::nonNull).toList();
CoreConfig.httpRules = httpRules.get().stream().map(AddressRuleConfig::parseRule).toList();
CoreConfig.httpMaxRequests = httpMaxRequests.get();
CoreConfig.httpMaxWebsockets = httpMaxWebsockets.get();

View File

@ -5,10 +5,7 @@
package dan200.computercraft.core.apis.http.options;
import com.google.common.net.InetAddresses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.regex.Pattern;
@ -19,8 +16,6 @@
* @see AddressRule#apply(Iterable, String, InetSocketAddress) for the actual handling of this rule.
*/
interface AddressPredicate {
Logger LOG = LoggerFactory.getLogger(AddressPredicate.class);
default boolean matches(String domain) {
return false;
}
@ -51,28 +46,25 @@ public boolean matches(InetAddress address) {
return true;
}
@Nullable
public static HostRange parse(String addressStr, String prefixSizeStr) {
int prefixSize;
try {
prefixSize = Integer.parseInt(prefixSizeStr);
} catch (NumberFormatException e) {
LOG.error(
"Malformed http whitelist/blacklist entry '{}': Cannot extract size of CIDR mask from '{}'.",
throw new InvalidRuleException(String.format(
"Invalid host host '%s': Cannot extract size of CIDR mask from '%s'.",
addressStr + '/' + prefixSizeStr, prefixSizeStr
);
return null;
));
}
InetAddress address;
try {
address = InetAddresses.forString(addressStr);
} catch (IllegalArgumentException e) {
LOG.error(
"Malformed http whitelist/blacklist entry '{}': Cannot extract IP address from '{}'.",
throw new InvalidRuleException(String.format(
"Invalid host '%s': Cannot extract IP address from '%s'.",
addressStr + '/' + prefixSizeStr, addressStr
);
return null;
));
}
// Mask the bytes of the IP address.
@ -112,7 +104,6 @@ public boolean matches(InetAddress socketAddress) {
}
}
final class PrivatePattern implements AddressPredicate {
static final PrivatePattern INSTANCE = new PrivatePattern();

View File

@ -35,14 +35,13 @@ private AddressRule(AddressPredicate predicate, OptionalInt port, PartialOptions
this.port = port;
}
@Nullable
public static AddressRule parse(String filter, OptionalInt port, PartialOptions partial) {
var cidr = filter.indexOf('/');
if (cidr >= 0) {
var addressStr = filter.substring(0, cidr);
var prefixSizeStr = filter.substring(cidr + 1);
var range = HostRange.parse(addressStr, prefixSizeStr);
return range == null ? null : new AddressRule(range, port, partial);
return new AddressRule(range, port, partial);
} else if (filter.equalsIgnoreCase("$private")) {
return new AddressRule(PrivatePattern.INSTANCE, port, partial);
} else {

View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.http.options;
import java.io.Serial;
import java.util.OptionalInt;
/**
* Throw when a {@link AddressRule} cannot be parsed.
*
* @see AddressRule#parse(String, OptionalInt, PartialOptions)
* @see AddressPredicate.HostRange#parse(String, String)
*/
public class InvalidRuleException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1303376302865132758L;
public InvalidRuleException(String message) {
super(message);
}
}

View File

@ -90,9 +90,8 @@ while running do
local results = table.pack(exception.try(func))
if results[1] then
local n = 1
while n < results.n do
local value = results[n + 1]
for i = 2, results.n do
local value = results[i]
local ok, serialised = pcall(pretty.pretty, value, {
function_args = settings.get("lua.function_args"),
function_source = settings.get("lua.function_source"),
@ -102,7 +101,6 @@ while running do
else
print(tostring(value))
end
n = n + 1
end
else
printError(results[2])

View File

@ -99,7 +99,7 @@
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1",
"gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.",
"gui.computercraft.config.http.enabled": "Enable the HTTP API",
"gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. This also disables the \"pastebin\" and \"wget\"\nprograms, that many users rely on. It's recommended to leave this on and use the\n\"rules\" config option to impose more fine-grained control.",
"gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.",
"gui.computercraft.config.http.max_requests": "Maximum concurrent requests",
"gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0",
"gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets",
@ -113,7 +113,7 @@
"gui.computercraft.config.http.proxy.type": "Proxy type",
"gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.config.http.rules": "Allow/deny rules",
"gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule is an item with a 'host' to match against, and a series of\nproperties. Rules are evaluated in order, meaning earlier rules override later\nones.\nThe host may be a domain name (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or\nCIDR notation (\"127.0.0.0/8\").\nIf no rules, the domain is blocked.",
"gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.",
"gui.computercraft.config.http.tooltip": "Controls the HTTP API",
"gui.computercraft.config.http.websocket_enabled": "Enable websockets",
"gui.computercraft.config.http.websocket_enabled.tooltip": "Enable use of http websockets. This requires the \"http_enable\" option to also be true.",

View File

@ -99,7 +99,7 @@
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1",
"gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.",
"gui.computercraft.config.http.enabled": "Enable the HTTP API",
"gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. This also disables the \"pastebin\" and \"wget\"\nprograms, that many users rely on. It's recommended to leave this on and use the\n\"rules\" config option to impose more fine-grained control.",
"gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.",
"gui.computercraft.config.http.max_requests": "Maximum concurrent requests",
"gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0",
"gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets",
@ -113,7 +113,7 @@
"gui.computercraft.config.http.proxy.type": "Proxy type",
"gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.config.http.rules": "Allow/deny rules",
"gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule is an item with a 'host' to match against, and a series of\nproperties. Rules are evaluated in order, meaning earlier rules override later\nones.\nThe host may be a domain name (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or\nCIDR notation (\"127.0.0.0/8\").\nIf no rules, the domain is blocked.",
"gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.",
"gui.computercraft.config.http.tooltip": "Controls the HTTP API",
"gui.computercraft.config.http.websocket_enabled": "Enable websockets",
"gui.computercraft.config.http.websocket_enabled.tooltip": "Enable use of http websockets. This requires the \"http_enable\" option to also be true.",