mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-21 17:07:39 +00:00
Add support for proxying HTTP requests (#1461)
This commit is contained in:
@@ -21,6 +21,8 @@ dependencies {
|
||||
implementation(libs.guava)
|
||||
implementation(libs.jzlib)
|
||||
implementation(libs.netty.http)
|
||||
implementation(libs.netty.socks)
|
||||
implementation(libs.netty.proxy)
|
||||
implementation(libs.slf4j)
|
||||
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.AddressRule;
|
||||
import dan200.computercraft.core.apis.http.options.ProxyType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
@@ -34,4 +35,9 @@ public final class CoreConfig {
|
||||
public static int httpMaxWebsockets = 4;
|
||||
public static int httpDownloadBandwidth = 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;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
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.TooLongFrameException;
|
||||
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.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
import io.netty.handler.traffic.AbstractTrafficShapingHandler;
|
||||
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -32,6 +38,7 @@ import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 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 boolean triedSslContext = false;
|
||||
|
||||
@Nullable
|
||||
private static SslContext makeSslContext() {
|
||||
private static @Nullable SslContext makeSslContext() {
|
||||
if (triedSslContext) return sslContext;
|
||||
synchronized (sslLock) {
|
||||
if (triedSslContext) return sslContext;
|
||||
@@ -133,6 +139,66 @@ public final class NetworkUtils {
|
||||
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
|
||||
* handler.
|
||||
@@ -141,19 +207,20 @@ public final class NetworkUtils {
|
||||
* @param uri The URI to connect to.
|
||||
* @param socketAddress The address of the socket to connect to.
|
||||
* @param sslContext The SSL context, if present.
|
||||
* @param proxy The proxy handler, if present.
|
||||
* @param timeout The timeout on this channel.
|
||||
* @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);
|
||||
|
||||
var p = ch.pipeline();
|
||||
p.addLast(SHAPING_HANDLER);
|
||||
|
||||
if (proxy != null) proxy.accept(ch);
|
||||
|
||||
if (sslContext != null) {
|
||||
var handler = sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort());
|
||||
if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout);
|
||||
p.addLast(handler);
|
||||
p.addLast(makeSslHandler(ch, sslContext, timeout, uri.getHost(), socketAddress.getPort()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
@@ -12,7 +13,7 @@ public enum Action {
|
||||
DENY;
|
||||
|
||||
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() {
|
||||
|
@@ -13,11 +13,13 @@ public final class Options {
|
||||
public final long maxUpload;
|
||||
public final long maxDownload;
|
||||
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.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.websocketMessage = websocketMessage;
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
}
|
||||
|
@@ -7,28 +7,31 @@ package dan200.computercraft.core.apis.http.options;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
@Immutable
|
||||
public final class 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 OptionalLong maxUpload;
|
||||
private final OptionalLong maxDownload;
|
||||
private final OptionalInt websocketMessage;
|
||||
private final Optional<Boolean> useProxy;
|
||||
|
||||
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
|
||||
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.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.websocketMessage = websocketMessage;
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
|
||||
Options toOptions() {
|
||||
@@ -38,7 +41,8 @@ public final class PartialOptions {
|
||||
action == null ? Action.DENY : action,
|
||||
maxUpload.orElse(AddressRule.MAX_UPLOAD),
|
||||
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,
|
||||
maxUpload.isPresent() ? maxUpload : other.maxUpload,
|
||||
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 options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||
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.
|
||||
if (isClosed()) return;
|
||||
@@ -145,7 +146,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, timeout);
|
||||
NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, proxy, timeout);
|
||||
|
||||
var p = ch.pipeline();
|
||||
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 options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||
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.
|
||||
if (isClosed()) return;
|
||||
@@ -127,7 +128,7 @@ public class Websocket extends Resource<Websocket> {
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
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 handshaker = new NoOriginWebSocketHandshaker(
|
||||
|
Reference in New Issue
Block a user