mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-12-05 13:58:06 +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:
@@ -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 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
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 @@ class AddressRuleConfig {
|
||||
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 @@ class AddressRuleConfig {
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
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 org.apache.logging.log4j.core.filter.MarkerFilter;
|
||||
|
||||
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 @@ public final class 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 @@ public final class 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 final class ConfigSpec {
|
||||
// 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();
|
||||
|
||||
Reference in New Issue
Block a user