mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-15 16:32:52 +00:00
Add support for proxying HTTP requests (#1461)
This commit is contained in:
parent
d45f2bfe80
commit
c91bb5ac33
@ -83,6 +83,8 @@ kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core",
|
|||||||
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
|
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
netty-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
netty-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
||||||
|
netty-socks = { module = "io.netty:netty-codec-socks", version.ref = "netty" }
|
||||||
|
netty-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "netty" }
|
||||||
nightConfig-core = { module = "com.electronwill.night-config:core", version.ref = "nightConfig" }
|
nightConfig-core = { module = "com.electronwill.night-config:core", version.ref = "nightConfig" }
|
||||||
nightConfig-toml = { module = "com.electronwill.night-config:toml", version.ref = "nightConfig" }
|
nightConfig-toml = { module = "com.electronwill.night-config:toml", version.ref = "nightConfig" }
|
||||||
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||||
|
@ -45,6 +45,9 @@ class AddressRuleConfig {
|
|||||||
|
|
||||||
config.setComment("max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet.");
|
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.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
@ -58,6 +61,7 @@ class AddressRuleConfig {
|
|||||||
&& check(builder, "max_upload", Number.class)
|
&& check(builder, "max_upload", Number.class)
|
||||||
&& check(builder, "max_download", Number.class)
|
&& check(builder, "max_download", Number.class)
|
||||||
&& check(builder, "websocket_message", Number.class)
|
&& check(builder, "websocket_message", Number.class)
|
||||||
|
&& check(builder, "use_proxy", Boolean.class)
|
||||||
&& AddressRule.parse(hostObj, port, PartialOptions.DEFAULT) != null;
|
&& AddressRule.parse(hostObj, port, PartialOptions.DEFAULT) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +75,14 @@ class AddressRuleConfig {
|
|||||||
var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue));
|
var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue));
|
||||||
var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue));
|
var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue));
|
||||||
var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue));
|
var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue));
|
||||||
|
var useProxy = get(builder, "use_proxy", Boolean.class);
|
||||||
|
|
||||||
var options = new PartialOptions(
|
var options = new PartialOptions(
|
||||||
action,
|
action,
|
||||||
maxUpload,
|
maxUpload,
|
||||||
maxDownload,
|
maxDownload,
|
||||||
websocketMessage
|
websocketMessage,
|
||||||
|
useProxy
|
||||||
);
|
);
|
||||||
|
|
||||||
return AddressRule.parse(hostObj, port, options);
|
return AddressRule.parse(hostObj, port, options);
|
||||||
|
@ -8,6 +8,7 @@ import com.google.common.base.Splitter;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -141,6 +142,18 @@ public interface ConfigFile {
|
|||||||
* @param onChange The function to run on change.
|
* @param onChange The function to run on change.
|
||||||
* @return The built config file.
|
* @return The built config file.
|
||||||
*/
|
*/
|
||||||
public abstract ConfigFile build(Runnable onChange);
|
public abstract ConfigFile build(ConfigListener onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ConfigListener {
|
||||||
|
/**
|
||||||
|
* The function called then a config file is changed.
|
||||||
|
*
|
||||||
|
* @param path The path to the config file. This will be {@code null} when the config file does not exist on
|
||||||
|
* disk, such as when synced from a server to the client.
|
||||||
|
* @see Builder#build(ConfigListener)
|
||||||
|
*/
|
||||||
|
void onConfigChanged(@Nullable Path path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,12 @@
|
|||||||
package dan200.computercraft.shared.config;
|
package dan200.computercraft.shared.config;
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
|
import dan200.computercraft.api.ComputerCraftAPI;
|
||||||
import dan200.computercraft.core.CoreConfig;
|
import dan200.computercraft.core.CoreConfig;
|
||||||
import dan200.computercraft.core.Logging;
|
import dan200.computercraft.core.Logging;
|
||||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||||
import dan200.computercraft.core.apis.http.options.Action;
|
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.core.computer.mainthread.MainThreadConfig;
|
||||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||||
@ -16,6 +18,8 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.core.Filter;
|
import org.apache.logging.log4j.core.Filter;
|
||||||
import org.apache.logging.log4j.core.filter.MarkerFilter;
|
import org.apache.logging.log4j.core.filter.MarkerFilter;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -49,6 +53,10 @@ public final class ConfigSpec {
|
|||||||
public static final ConfigFile.Value<Integer> httpDownloadBandwidth;
|
public static final ConfigFile.Value<Integer> httpDownloadBandwidth;
|
||||||
public static final ConfigFile.Value<Integer> httpUploadBandwidth;
|
public static final ConfigFile.Value<Integer> httpUploadBandwidth;
|
||||||
|
|
||||||
|
public static final ConfigFile.Value<ProxyType> httpProxyType;
|
||||||
|
public static final ConfigFile.Value<String> httpProxyHost;
|
||||||
|
public static final ConfigFile.Value<Integer> httpProxyPort;
|
||||||
|
|
||||||
public static final ConfigFile.Value<Boolean> commandBlockEnabled;
|
public static final ConfigFile.Value<Boolean> commandBlockEnabled;
|
||||||
public static final ConfigFile.Value<Integer> modemRange;
|
public static final ConfigFile.Value<Integer> modemRange;
|
||||||
public static final ConfigFile.Value<Integer> modemHighAltitudeRange;
|
public static final ConfigFile.Value<Integer> modemHighAltitudeRange;
|
||||||
@ -222,6 +230,30 @@ public final class ConfigSpec {
|
|||||||
|
|
||||||
builder.pop();
|
builder.pop();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.comment("""
|
||||||
|
Tunnels HTTP and websocket requests through a proxy server. Only affects HTTP
|
||||||
|
rules with "use_proxy" set to true (off by default).
|
||||||
|
If authentication is required for the proxy, create a "computercraft-proxy.pw"
|
||||||
|
file in the same directory as "computercraft-server.toml", containing the
|
||||||
|
username and password separated by a colon, e.g. "myuser:mypassword". For
|
||||||
|
SOCKS4 proxies only the username is required.""")
|
||||||
|
.push("proxy");
|
||||||
|
|
||||||
|
httpProxyType = builder
|
||||||
|
.comment("The type of proxy to use.")
|
||||||
|
.defineEnum("type", CoreConfig.httpProxyType);
|
||||||
|
|
||||||
|
httpProxyHost = builder
|
||||||
|
.comment("The hostname or IP address of the proxy server.")
|
||||||
|
.define("host", CoreConfig.httpProxyHost);
|
||||||
|
|
||||||
|
httpProxyPort = builder
|
||||||
|
.comment("The port of the proxy server.")
|
||||||
|
.defineInRange("port", CoreConfig.httpProxyPort, 1, 65536);
|
||||||
|
|
||||||
|
builder.pop();
|
||||||
|
|
||||||
builder.pop();
|
builder.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,7 +371,7 @@ public final class ConfigSpec {
|
|||||||
clientSpec = clientBuilder.build(ConfigSpec::syncClient);
|
clientSpec = clientBuilder.build(ConfigSpec::syncClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void syncServer() {
|
public static void syncServer(@Nullable Path path) {
|
||||||
// General
|
// General
|
||||||
Config.computerSpaceLimit = computerSpaceLimit.get();
|
Config.computerSpaceLimit = computerSpaceLimit.get();
|
||||||
Config.floppySpaceLimit = floppySpaceLimit.get();
|
Config.floppySpaceLimit = floppySpaceLimit.get();
|
||||||
@ -370,6 +402,13 @@ public final class ConfigSpec {
|
|||||||
CoreConfig.httpMaxWebsockets = httpMaxWebsockets.get();
|
CoreConfig.httpMaxWebsockets = httpMaxWebsockets.get();
|
||||||
CoreConfig.httpDownloadBandwidth = httpDownloadBandwidth.get();
|
CoreConfig.httpDownloadBandwidth = httpDownloadBandwidth.get();
|
||||||
CoreConfig.httpUploadBandwidth = httpUploadBandwidth.get();
|
CoreConfig.httpUploadBandwidth = httpUploadBandwidth.get();
|
||||||
|
|
||||||
|
CoreConfig.httpProxyType = httpProxyType.get();
|
||||||
|
CoreConfig.httpProxyHost = httpProxyHost.get();
|
||||||
|
CoreConfig.httpProxyPort = httpProxyPort.get();
|
||||||
|
|
||||||
|
if (path != null) ProxyPasswordConfig.init(path.resolveSibling(ComputerCraftAPI.MOD_ID + "-proxy.pw"));
|
||||||
|
|
||||||
NetworkUtils.reloadConfig();
|
NetworkUtils.reloadConfig();
|
||||||
|
|
||||||
// Peripheral
|
// Peripheral
|
||||||
@ -396,7 +435,7 @@ public final class ConfigSpec {
|
|||||||
Config.monitorHeight = monitorHeight.get();
|
Config.monitorHeight = monitorHeight.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void syncClient() {
|
public static void syncClient(@Nullable Path path) {
|
||||||
Config.monitorRenderer = monitorRenderer.get();
|
Config.monitorRenderer = monitorRenderer.get();
|
||||||
Config.monitorDistance = monitorDistance.get();
|
Config.monitorDistance = monitorDistance.get();
|
||||||
Config.uploadNagDelay = uploadNagDelay.get();
|
Config.uploadNagDelay = uploadNagDelay.get();
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.shared.config;
|
||||||
|
|
||||||
|
import dan200.computercraft.core.CoreConfig;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
record ProxyPasswordConfig(String username, String password) {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ProxyPasswordConfig.class);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static ProxyPasswordConfig loadFromFile(@Nullable Path path) {
|
||||||
|
if (path == null || !path.toFile().exists()) return null;
|
||||||
|
|
||||||
|
try (var br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
|
||||||
|
var line = br.readLine();
|
||||||
|
if (line == null) return null;
|
||||||
|
|
||||||
|
var parts = line.trim().split(":", 2);
|
||||||
|
if (parts.length == 0) return null;
|
||||||
|
|
||||||
|
return new ProxyPasswordConfig(parts[0], parts.length == 2 ? parts[1] : "");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to load proxy password from {}.", path, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init(@Nullable Path path) {
|
||||||
|
var config = loadFromFile(path);
|
||||||
|
if (config == null) {
|
||||||
|
CoreConfig.httpProxyUsername = "";
|
||||||
|
CoreConfig.httpProxyPassword = "";
|
||||||
|
} else {
|
||||||
|
CoreConfig.httpProxyUsername = config.username;
|
||||||
|
CoreConfig.httpProxyPassword = config.password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,8 @@ dependencies {
|
|||||||
implementation(libs.guava)
|
implementation(libs.guava)
|
||||||
implementation(libs.jzlib)
|
implementation(libs.jzlib)
|
||||||
implementation(libs.netty.http)
|
implementation(libs.netty.http)
|
||||||
|
implementation(libs.netty.socks)
|
||||||
|
implementation(libs.netty.proxy)
|
||||||
implementation(libs.slf4j)
|
implementation(libs.slf4j)
|
||||||
implementation(libs.asm)
|
implementation(libs.asm)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ package dan200.computercraft.core;
|
|||||||
|
|
||||||
import dan200.computercraft.core.apis.http.options.Action;
|
import dan200.computercraft.core.apis.http.options.Action;
|
||||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||||
|
import dan200.computercraft.core.apis.http.options.ProxyType;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
@ -34,4 +35,9 @@ public final class CoreConfig {
|
|||||||
public static int httpMaxWebsockets = 4;
|
public static int httpMaxWebsockets = 4;
|
||||||
public static int httpDownloadBandwidth = 32 * 1024 * 1024;
|
public static int httpDownloadBandwidth = 32 * 1024 * 1024;
|
||||||
public static int httpUploadBandwidth = 32 * 1024 * 1024;
|
public static int httpUploadBandwidth = 32 * 1024 * 1024;
|
||||||
|
public static ProxyType httpProxyType = ProxyType.HTTP;
|
||||||
|
public static String httpProxyHost = "";
|
||||||
|
public static int httpProxyPort = 8080;
|
||||||
|
public static String httpProxyUsername = "";
|
||||||
|
public static String httpProxyPassword = "";
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.apis.http;
|
package dan200.computercraft.core.apis.http;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import dan200.computercraft.core.CoreConfig;
|
import dan200.computercraft.core.CoreConfig;
|
||||||
import dan200.computercraft.core.apis.http.options.Action;
|
import dan200.computercraft.core.apis.http.options.Action;
|
||||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||||
@ -17,11 +18,16 @@ import io.netty.channel.socket.SocketChannel;
|
|||||||
import io.netty.handler.codec.DecoderException;
|
import io.netty.handler.codec.DecoderException;
|
||||||
import io.netty.handler.codec.TooLongFrameException;
|
import io.netty.handler.codec.TooLongFrameException;
|
||||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||||
|
import io.netty.handler.proxy.HttpProxyHandler;
|
||||||
|
import io.netty.handler.proxy.Socks4ProxyHandler;
|
||||||
|
import io.netty.handler.proxy.Socks5ProxyHandler;
|
||||||
import io.netty.handler.ssl.SslContext;
|
import io.netty.handler.ssl.SslContext;
|
||||||
import io.netty.handler.ssl.SslContextBuilder;
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
import io.netty.handler.timeout.ReadTimeoutException;
|
import io.netty.handler.timeout.ReadTimeoutException;
|
||||||
import io.netty.handler.traffic.AbstractTrafficShapingHandler;
|
import io.netty.handler.traffic.AbstractTrafficShapingHandler;
|
||||||
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
|
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -32,6 +38,7 @@ import java.net.InetSocketAddress;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Just a shared object for executing simple HTTP related tasks.
|
* Just a shared object for executing simple HTTP related tasks.
|
||||||
@ -57,8 +64,7 @@ public final class NetworkUtils {
|
|||||||
private static @Nullable SslContext sslContext;
|
private static @Nullable SslContext sslContext;
|
||||||
private static boolean triedSslContext = false;
|
private static boolean triedSslContext = false;
|
||||||
|
|
||||||
@Nullable
|
private static @Nullable SslContext makeSslContext() {
|
||||||
private static SslContext makeSslContext() {
|
|
||||||
if (triedSslContext) return sslContext;
|
if (triedSslContext) return sslContext;
|
||||||
synchronized (sslLock) {
|
synchronized (sslLock) {
|
||||||
if (triedSslContext) return sslContext;
|
if (triedSslContext) return sslContext;
|
||||||
@ -133,6 +139,66 @@ public final class NetworkUtils {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a proxy handler for a specific domain. Returns null if a proxy is not required for this HTTP rule, or
|
||||||
|
* throws if it is required but is not configured correctly.
|
||||||
|
* <p>
|
||||||
|
* Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
|
||||||
|
*
|
||||||
|
* @param options The options for the host to be proxied.
|
||||||
|
* @param timeout The timeout for this connection. Currently only used for establishing the SSL initialisation.
|
||||||
|
* @return A consumer that takes a {@link SocketChannel} and injects the proxy handler..
|
||||||
|
* @throws HTTPRequestException If a proxy is required but not configured correctly.
|
||||||
|
*/
|
||||||
|
public static @Nullable Consumer<SocketChannel> getProxyHandler(Options options, int timeout) throws HTTPRequestException {
|
||||||
|
if (!options.useProxy) return null;
|
||||||
|
|
||||||
|
var type = CoreConfig.httpProxyType;
|
||||||
|
var host = CoreConfig.httpProxyHost;
|
||||||
|
var port = CoreConfig.httpProxyPort;
|
||||||
|
var username = CoreConfig.httpProxyUsername;
|
||||||
|
var password = CoreConfig.httpProxyPassword;
|
||||||
|
|
||||||
|
if (Strings.isNullOrEmpty(host)) {
|
||||||
|
throw new HTTPRequestException("Proxy host not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyAddress = new InetSocketAddress(host, port);
|
||||||
|
if (proxyAddress.isUnresolved()) throw new HTTPRequestException("Unknown proxy host");
|
||||||
|
|
||||||
|
return switch (type) {
|
||||||
|
case HTTP -> ch -> ch.pipeline().addLast(new HttpProxyHandler(proxyAddress, username, password));
|
||||||
|
case HTTPS -> {
|
||||||
|
var sslContext = getSslContext();
|
||||||
|
yield ch -> {
|
||||||
|
var p = ch.pipeline();
|
||||||
|
// If we're using an HTTPS proxy, we need to add an SSL handler for the proxy too.
|
||||||
|
p.addLast(makeSslHandler(ch, sslContext, timeout, host, port));
|
||||||
|
p.addLast(new HttpProxyHandler(proxyAddress, username, password));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SOCKS4 -> ch -> ch.pipeline().addLast(new Socks4ProxyHandler(proxyAddress, username));
|
||||||
|
case SOCKS5 -> ch -> ch.pipeline().addLast(new Socks5ProxyHandler(proxyAddress, username, password));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an SSL handler for the remote host.
|
||||||
|
*
|
||||||
|
* @param ch The channel the handler will be added to.
|
||||||
|
* @param sslContext The SSL context, if present.
|
||||||
|
* @param timeout The timeout on this channel.
|
||||||
|
* @param peerHost The host to connect to.
|
||||||
|
* @param peerPort The port to connect to.
|
||||||
|
* @return The SSL handler.
|
||||||
|
* @see io.netty.handler.ssl.SslHandler
|
||||||
|
*/
|
||||||
|
private static SslHandler makeSslHandler(SocketChannel ch, @NotNull SslContext sslContext, int timeout, String peerHost, int peerPort) {
|
||||||
|
var handler = sslContext.newHandler(ch.alloc(), peerHost, peerPort);
|
||||||
|
if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout);
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up some basic properties of the channel. This adds a timeout, the traffic shaping handler, and the SSL
|
* Set up some basic properties of the channel. This adds a timeout, the traffic shaping handler, and the SSL
|
||||||
* handler.
|
* handler.
|
||||||
@ -141,19 +207,20 @@ public final class NetworkUtils {
|
|||||||
* @param uri The URI to connect to.
|
* @param uri The URI to connect to.
|
||||||
* @param socketAddress The address of the socket to connect to.
|
* @param socketAddress The address of the socket to connect to.
|
||||||
* @param sslContext The SSL context, if present.
|
* @param sslContext The SSL context, if present.
|
||||||
|
* @param proxy The proxy handler, if present.
|
||||||
* @param timeout The timeout on this channel.
|
* @param timeout The timeout on this channel.
|
||||||
* @see io.netty.channel.ChannelInitializer
|
* @see io.netty.channel.ChannelInitializer
|
||||||
*/
|
*/
|
||||||
public static void initChannel(SocketChannel ch, URI uri, InetSocketAddress socketAddress, @Nullable SslContext sslContext, int timeout) {
|
public static void initChannel(SocketChannel ch, URI uri, InetSocketAddress socketAddress, @Nullable SslContext sslContext, @Nullable Consumer<SocketChannel> proxy, int timeout) {
|
||||||
if (timeout > 0) ch.config().setConnectTimeoutMillis(timeout);
|
if (timeout > 0) ch.config().setConnectTimeoutMillis(timeout);
|
||||||
|
|
||||||
var p = ch.pipeline();
|
var p = ch.pipeline();
|
||||||
p.addLast(SHAPING_HANDLER);
|
p.addLast(SHAPING_HANDLER);
|
||||||
|
|
||||||
|
if (proxy != null) proxy.accept(ch);
|
||||||
|
|
||||||
if (sslContext != null) {
|
if (sslContext != null) {
|
||||||
var handler = sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort());
|
p.addLast(makeSslHandler(ch, sslContext, timeout, uri.getHost(), socketAddress.getPort()));
|
||||||
if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout);
|
|
||||||
p.addLast(handler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.apis.http.options;
|
package dan200.computercraft.core.apis.http.options;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ public enum Action {
|
|||||||
DENY;
|
DENY;
|
||||||
|
|
||||||
private final PartialOptions partial = new PartialOptions(
|
private final PartialOptions partial = new PartialOptions(
|
||||||
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty()
|
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), Optional.empty()
|
||||||
);
|
);
|
||||||
|
|
||||||
public PartialOptions toPartial() {
|
public PartialOptions toPartial() {
|
||||||
|
@ -13,11 +13,13 @@ public final class Options {
|
|||||||
public final long maxUpload;
|
public final long maxUpload;
|
||||||
public final long maxDownload;
|
public final long maxDownload;
|
||||||
public final int websocketMessage;
|
public final int websocketMessage;
|
||||||
|
public final boolean useProxy;
|
||||||
|
|
||||||
Options(Action action, long maxUpload, long maxDownload, int websocketMessage) {
|
Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.maxUpload = maxUpload;
|
this.maxUpload = maxUpload;
|
||||||
this.maxDownload = maxDownload;
|
this.maxDownload = maxDownload;
|
||||||
this.websocketMessage = websocketMessage;
|
this.websocketMessage = websocketMessage;
|
||||||
|
this.useProxy = useProxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,28 +7,31 @@ package dan200.computercraft.core.apis.http.options;
|
|||||||
import com.google.errorprone.annotations.Immutable;
|
import com.google.errorprone.annotations.Immutable;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class PartialOptions {
|
public final class PartialOptions {
|
||||||
public static final PartialOptions DEFAULT = new PartialOptions(
|
public static final PartialOptions DEFAULT = new PartialOptions(
|
||||||
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty()
|
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), Optional.empty()
|
||||||
);
|
);
|
||||||
|
|
||||||
private final @Nullable Action action;
|
private final @Nullable Action action;
|
||||||
private final OptionalLong maxUpload;
|
private final OptionalLong maxUpload;
|
||||||
private final OptionalLong maxDownload;
|
private final OptionalLong maxDownload;
|
||||||
private final OptionalInt websocketMessage;
|
private final OptionalInt websocketMessage;
|
||||||
|
private final Optional<Boolean> useProxy;
|
||||||
|
|
||||||
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
|
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
|
||||||
private @Nullable Options options;
|
private @Nullable Options options;
|
||||||
|
|
||||||
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage) {
|
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) {
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.maxUpload = maxUpload;
|
this.maxUpload = maxUpload;
|
||||||
this.maxDownload = maxDownload;
|
this.maxDownload = maxDownload;
|
||||||
this.websocketMessage = websocketMessage;
|
this.websocketMessage = websocketMessage;
|
||||||
|
this.useProxy = useProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
Options toOptions() {
|
Options toOptions() {
|
||||||
@ -38,7 +41,8 @@ public final class PartialOptions {
|
|||||||
action == null ? Action.DENY : action,
|
action == null ? Action.DENY : action,
|
||||||
maxUpload.orElse(AddressRule.MAX_UPLOAD),
|
maxUpload.orElse(AddressRule.MAX_UPLOAD),
|
||||||
maxDownload.orElse(AddressRule.MAX_DOWNLOAD),
|
maxDownload.orElse(AddressRule.MAX_DOWNLOAD),
|
||||||
websocketMessage.orElse(AddressRule.WEBSOCKET_MESSAGE)
|
websocketMessage.orElse(AddressRule.WEBSOCKET_MESSAGE),
|
||||||
|
useProxy.orElse(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +60,8 @@ public final class PartialOptions {
|
|||||||
action == null && other.action != null ? other.action : action,
|
action == null && other.action != null ? other.action : action,
|
||||||
maxUpload.isPresent() ? maxUpload : other.maxUpload,
|
maxUpload.isPresent() ? maxUpload : other.maxUpload,
|
||||||
maxDownload.isPresent() ? maxDownload : other.maxDownload,
|
maxDownload.isPresent() ? maxDownload : other.maxDownload,
|
||||||
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage
|
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage,
|
||||||
|
useProxy.isPresent() ? useProxy : other.useProxy
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.http.options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of proxy to use for HTTP requests.
|
||||||
|
*
|
||||||
|
* @see dan200.computercraft.core.apis.http.NetworkUtils#getProxyHandler(Options, int)
|
||||||
|
* @see dan200.computercraft.core.CoreConfig#httpProxyType
|
||||||
|
*/
|
||||||
|
public enum ProxyType {
|
||||||
|
HTTP,
|
||||||
|
HTTPS,
|
||||||
|
SOCKS4,
|
||||||
|
SOCKS5
|
||||||
|
}
|
@ -124,6 +124,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
|||||||
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
||||||
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||||
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||||
|
var proxy = NetworkUtils.getProxyHandler(options, timeout);
|
||||||
|
|
||||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||||
if (isClosed()) return;
|
if (isClosed()) return;
|
||||||
@ -145,7 +146,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
|||||||
.handler(new ChannelInitializer<SocketChannel>() {
|
.handler(new ChannelInitializer<SocketChannel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(SocketChannel ch) {
|
protected void initChannel(SocketChannel ch) {
|
||||||
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout);
|
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, proxy, timeout);
|
||||||
|
|
||||||
var p = ch.pipeline();
|
var p = ch.pipeline();
|
||||||
if (timeout > 0) p.addLast(new ReadTimeoutHandler(timeout, TimeUnit.MILLISECONDS));
|
if (timeout > 0) p.addLast(new ReadTimeoutHandler(timeout, TimeUnit.MILLISECONDS));
|
||||||
|
@ -117,6 +117,7 @@ public class Websocket extends Resource<Websocket> {
|
|||||||
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
||||||
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||||
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||||
|
var proxy = NetworkUtils.getProxyHandler(options, timeout);
|
||||||
|
|
||||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||||
if (isClosed()) return;
|
if (isClosed()) return;
|
||||||
@ -127,7 +128,7 @@ public class Websocket extends Resource<Websocket> {
|
|||||||
.handler(new ChannelInitializer<SocketChannel>() {
|
.handler(new ChannelInitializer<SocketChannel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(SocketChannel ch) {
|
protected void initChannel(SocketChannel ch) {
|
||||||
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout);
|
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, proxy, timeout);
|
||||||
|
|
||||||
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
||||||
var handshaker = new NoOriginWebSocketHandshaker(
|
var handshaker = new NoOriginWebSocketHandshaker(
|
||||||
|
@ -54,6 +54,8 @@ dependencies {
|
|||||||
include(libs.cobalt)
|
include(libs.cobalt)
|
||||||
include(libs.jzlib)
|
include(libs.jzlib)
|
||||||
include(libs.netty.http)
|
include(libs.netty.http)
|
||||||
|
include(libs.netty.socks)
|
||||||
|
include(libs.netty.proxy)
|
||||||
include(libs.nightConfig.core)
|
include(libs.nightConfig.core)
|
||||||
include(libs.nightConfig.toml)
|
include(libs.nightConfig.toml)
|
||||||
|
|
||||||
@ -65,6 +67,8 @@ dependencies {
|
|||||||
// in our POM, and this is the easiest way.
|
// in our POM, and this is the easiest way.
|
||||||
runtimeOnly(libs.cobalt)
|
runtimeOnly(libs.cobalt)
|
||||||
runtimeOnly(libs.netty.http)
|
runtimeOnly(libs.netty.http)
|
||||||
|
runtimeOnly(libs.netty.socks)
|
||||||
|
runtimeOnly(libs.netty.proxy)
|
||||||
|
|
||||||
annotationProcessorEverywhere(libs.autoService)
|
annotationProcessorEverywhere(libs.autoService)
|
||||||
|
|
||||||
|
@ -34,11 +34,11 @@ public class FabricConfigFile implements ConfigFile {
|
|||||||
|
|
||||||
private final ConfigSpec spec;
|
private final ConfigSpec spec;
|
||||||
private final Trie<String, Entry> entries;
|
private final Trie<String, Entry> entries;
|
||||||
private final Runnable onChange;
|
private final ConfigListener onChange;
|
||||||
|
|
||||||
private @Nullable CommentedFileConfig config;
|
private @Nullable CommentedFileConfig config;
|
||||||
|
|
||||||
public FabricConfigFile(ConfigSpec spec, Trie<String, Entry> entries, Runnable onChange) {
|
public FabricConfigFile(ConfigSpec spec, Trie<String, Entry> entries, ConfigListener onChange) {
|
||||||
this.spec = spec;
|
this.spec = spec;
|
||||||
this.entries = entries;
|
this.entries = entries;
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
@ -95,7 +95,7 @@ public class FabricConfigFile implements ConfigFile {
|
|||||||
LOG.warn("Incorrect key {} was corrected from {} to {}", String.join(".", entryPath), oldValue, newValue);
|
LOG.warn("Incorrect key {} was corrected from {} to {}", String.join(".", entryPath), oldValue, newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
onChange.run();
|
onChange.onConfigChanged(config.getNioPath());
|
||||||
|
|
||||||
return corrected > 0;
|
return corrected > 0;
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ public class FabricConfigFile implements ConfigFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConfigFile build(Runnable onChange) {
|
public ConfigFile build(ConfigListener onChange) {
|
||||||
return new FabricConfigFile(spec, entries, onChange);
|
return new FabricConfigFile(spec, entries, onChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,14 @@ dependencies {
|
|||||||
jarJar.ranged(this, "[${libs.versions.netty.get()},)")
|
jarJar.ranged(this, "[${libs.versions.netty.get()},)")
|
||||||
isTransitive = false
|
isTransitive = false
|
||||||
}
|
}
|
||||||
|
minecraftEmbed(libs.netty.socks) {
|
||||||
|
jarJar.ranged(this, "[${libs.versions.netty.get()},)")
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
minecraftEmbed(libs.netty.proxy) {
|
||||||
|
jarJar.ranged(this, "[${libs.versions.netty.get()},)")
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
|
||||||
testFixturesApi(libs.bundles.test)
|
testFixturesApi(libs.bundles.test)
|
||||||
testFixturesApi(libs.bundles.kotlin)
|
testFixturesApi(libs.bundles.kotlin)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package dan200.computercraft;
|
package dan200.computercraft;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.FileConfig;
|
||||||
import dan200.computercraft.api.ComputerCraftAPI;
|
import dan200.computercraft.api.ComputerCraftAPI;
|
||||||
import dan200.computercraft.api.ForgeComputerCraftAPI;
|
import dan200.computercraft.api.ForgeComputerCraftAPI;
|
||||||
import dan200.computercraft.api.detail.ForgeDetailRegistries;
|
import dan200.computercraft.api.detail.ForgeDetailRegistries;
|
||||||
@ -94,10 +95,12 @@ public final class ComputerCraft {
|
|||||||
private static void syncConfig(ModConfig config) {
|
private static void syncConfig(ModConfig config) {
|
||||||
if (!config.getModId().equals(ComputerCraftAPI.MOD_ID)) return;
|
if (!config.getModId().equals(ComputerCraftAPI.MOD_ID)) return;
|
||||||
|
|
||||||
|
var path = config.getConfigData() instanceof FileConfig fileConfig ? fileConfig.getNioPath() : null;
|
||||||
|
|
||||||
if (config.getType() == ModConfig.Type.SERVER && ((ForgeConfigFile) ConfigSpec.serverSpec).spec().isLoaded()) {
|
if (config.getType() == ModConfig.Type.SERVER && ((ForgeConfigFile) ConfigSpec.serverSpec).spec().isLoaded()) {
|
||||||
ConfigSpec.syncServer();
|
ConfigSpec.syncServer(path);
|
||||||
} else if (config.getType() == ModConfig.Type.CLIENT) {
|
} else if (config.getType() == ModConfig.Type.CLIENT) {
|
||||||
ConfigSpec.syncClient();
|
ConfigSpec.syncClient(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ public final class ForgeConfigFile implements ConfigFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConfigFile build(Runnable onChange) {
|
public ConfigFile build(ConfigListener onChange) {
|
||||||
var spec = builder.build();
|
var spec = builder.build();
|
||||||
entries.stream().forEach(x -> {
|
entries.stream().forEach(x -> {
|
||||||
if (x instanceof ValueImpl<?> value) value.owner = spec;
|
if (x instanceof ValueImpl<?> value) value.owner = spec;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user