From 677784dbe1c86b5d690c6993c999867fb2442b7d Mon Sep 17 00:00:00 2001 From: jaymie9019 Date: Fri, 14 Mar 2025 16:35:45 +0800 Subject: [PATCH 1/2] feat(login) support login with webLogonNonce and connection with proxy --- .../SampleWebLogonNonce.java | 205 +++++++++++++ .../SampleLoginWithProxy.java | 224 +++++++++++++++ .../networking/steam3/Socks5Connection.java | 271 ++++++++++++++++++ .../networking/steam3/WebSocketConnection.kt | 22 +- .../dragonbra/javasteam/steam/CMClient.java | 7 +- .../steam/handlers/steamuser/LogOnDetails.kt | 2 + .../steam/handlers/steamuser/SteamUser.kt | 20 +- .../ISteamConfigurationBuilder.kt | 9 + .../configuration/SteamConfiguration.kt | 4 + .../SteamConfigurationBuilder.kt | 13 + .../configuration/SteamConfigurationState.kt | 2 + 11 files changed, 774 insertions(+), 5 deletions(-) create mode 100644 javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java create mode 100644 javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java create mode 100644 src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java new file mode 100644 index 00000000..4d6745ad --- /dev/null +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java @@ -0,0 +1,205 @@ +package in.dragonbra.javasteamsamples._006_weblogonnonce; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import in.dragonbra.javasteam.enums.EResult; +import in.dragonbra.javasteam.networking.steam3.ProtocolTypes; +import in.dragonbra.javasteam.steam.authentication.*; +import in.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails; +import in.dragonbra.javasteam.steam.handlers.steamuser.SteamUser; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOffCallback; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOnCallback; +import in.dragonbra.javasteam.steam.steamclient.SteamClient; +import in.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackManager; +import in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback; +import in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback; +import in.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration; +import in.dragonbra.javasteam.util.NetHookNetworkListener; +import in.dragonbra.javasteam.util.log.DefaultLogListener; +import in.dragonbra.javasteam.util.log.LogManager; +import in.dragonbra.javasteamsamples._000_authentication.SampleLogonAuthentication; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.CancellationException; + +/** + * @author jaymie + * @since 2025-03-14 + */ +@SuppressWarnings("FieldCanBeLocal") +public class SampleWebLogonNonce implements Runnable { + + private SteamClient steamClient; + + private CallbackManager manager; + + private SteamUser steamUser; + + private boolean isRunning; + + private final String user; + + private final String webLogonNonce; + + private List subscriptions; + + + public SampleWebLogonNonce(String user, String webLogonNonce) { + this.user = user; + this.webLogonNonce = webLogonNonce; + } + + public static void main(String[] args) throws InterruptedException { + // you can view https://steamcommunity.com/chat/clientjstoken to get webLogonNonce, the property name is "token" + // webLoginNonce can only use one time, it's more safety for your customer + + if (args.length < 2) { + System.out.println("Sample1: No username and password specified!"); + return; + } + + LogManager.addListener(new DefaultLogListener()); + + new SampleLogonAuthentication(args[0], args[1]).run(); + Thread.sleep(1000000L); + } + + @Override + public void run() { + + // If any configuration needs to be set; such as connection protocol api key, etc., you can configure it like so. + var configuration = SteamConfiguration.create(config -> config.withProtocolTypes(ProtocolTypes.TCP)); + // create our steamclient instance with custom configuration. + steamClient = new SteamClient(configuration); + + // create our steamclient instance using default configuration +// steamClient = new SteamClient(); + + steamClient.setDebugNetworkListener(new NetHookNetworkListener()); + + // create the callback manager which will route callbacks to function calls + manager = new CallbackManager(steamClient); + + // get the steamuser handler, which is used for logging on after successfully connecting + steamUser = steamClient.getHandler(SteamUser.class); + + // The callbacks are a closeable, and to properly fix + // "'Closeable' used without 'try'-with-resources statement", they should be closed once done. + // Usually putting them in a list and close each of them once the client is finished is recommended. + subscriptions = new ArrayList<>(); + + // register a few callbacks we're interested in + // these are registered upon creation to a callback manager, which will then route the callbacks + // to the functions specified + subscriptions.add(manager.subscribe(ConnectedCallback.class, this::onConnected)); + subscriptions.add(manager.subscribe(DisconnectedCallback.class, this::onDisconnected)); + subscriptions.add(manager.subscribe(LoggedOnCallback.class, this::onLoggedOn)); + subscriptions.add(manager.subscribe(LoggedOffCallback.class, this::onLoggedOff)); + + isRunning = true; + + System.out.println("Connecting to steam..."); + + // initiate the connection + steamClient.connect(); + + // create our callback handling loop + while (isRunning) { + // in order for the callbacks to get routed, they need to be handled by the manager + manager.runWaitCallbacks(1000L); + } + + // Close the subscriptions when done. + System.out.println("Closing " + subscriptions.size() + " callbacks"); + for (var subscription : subscriptions) { + try { + subscription.close(); + } catch (IOException e) { + System.out.println("Couldn't close a callback."); + } + } + } + + @SuppressWarnings("DanglingJavadoc") + private void onConnected(ConnectedCallback callback) { + System.out.println("Connected to Steam! Logging in " + user + "..."); + + LogOnDetails details = new LogOnDetails(); + details.setUsername(user); + details.setWebLogonNonce(webLogonNonce); + + // Set LoginID to a non-zero value if you have another client connected using the same account, + // the same private ip, and same public ip. + details.setLoginID(149); + + steamUser.logOn(details); + } + + private void onDisconnected(DisconnectedCallback callback) { + System.out.println("Disconnected from Steam. User initialized: " + callback.isUserInitiated()); + + // If the disconnection was not user initiated, we will retry connecting to steam again after a short delay. + if (callback.isUserInitiated()) { + isRunning = false; + } else { + try { + Thread.sleep(2000L); + steamClient.connect(); + } catch (InterruptedException e) { + System.err.println("An Interrupted exception occurred. " + e.getMessage()); + } + } + } + + private void onLoggedOn(LoggedOnCallback callback) { + if (callback.getResult() != EResult.OK) { + System.out.println("Unable to logon to Steam: " + callback.getResult() + " / " + callback.getExtendedResult()); + + isRunning = false; + return; + } + + System.out.println("Successfully logged on!"); + + // at this point, we'd be able to perform actions on Steam + + // for this sample we'll just log off + steamUser.logOff(); + } + + private void onLoggedOff(LoggedOffCallback callback) { + System.out.println("Logged off of Steam: " + callback.getResult()); + + isRunning = false; + } + + + @SuppressWarnings("unused") + private void parseJsonWebToken(String token, String name) { + String[] tokenComponents = token.split("\\."); + + // Fix up base64url to normal base64 + String base64 = tokenComponents[1].replace('-', '+').replace('_', '/'); + + if (base64.length() % 4 != 0) { + base64 += new String(new char[4 - base64.length() % 4]).replace('\0', '='); + } + + byte[] payloadBytes = Base64.getDecoder().decode(base64); + + // Payload can be parsed as JSON, and then fields such expiration date, scope, etc can be accessed + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonElement payload = JsonParser.parseString(new String(payloadBytes)); + String formatted = gson.toJson(payload); + + // For brevity, we will simply output formatted json to console + System.out.println(name + ": " + formatted); + System.out.println(); + } +} diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java new file mode 100644 index 00000000..b6196607 --- /dev/null +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java @@ -0,0 +1,224 @@ +package in.dragonbra.javasteamsamples._007_loginwithproxy; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import in.dragonbra.javasteam.enums.EResult; +import in.dragonbra.javasteam.networking.steam3.ProtocolTypes; +import in.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails; +import in.dragonbra.javasteam.steam.handlers.steamuser.SteamUser; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOffCallback; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOnCallback; +import in.dragonbra.javasteam.steam.steamclient.SteamClient; +import in.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackManager; +import in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback; +import in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback; +import in.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration; +import in.dragonbra.javasteam.util.NetHookNetworkListener; +import in.dragonbra.javasteam.util.log.DefaultLogListener; +import in.dragonbra.javasteam.util.log.LogManager; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * @author jaymie + * @since 2025-03-14 + */ +@SuppressWarnings("FieldCanBeLocal") +public class SampleLoginWithProxy implements Runnable { + + private SteamClient steamClient; + + private CallbackManager manager; + + private SteamUser steamUser; + + private boolean isRunning; + + private final String user; + + private final String webLogonNonce; + + private List subscriptions; + + private Proxy proxy; + + + public void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + public SampleLoginWithProxy(String user, String webLogonNonce) { + this.user = user; + this.webLogonNonce = webLogonNonce; + } + + public static void main(String[] args) throws InterruptedException { + // you can view https://steamcommunity.com/chat/clientjstoken to get webLogonNonce, the property name is "token" + // webLoginNonce can only use one time, it's more safety for your customer + + if (args.length < 2) { + System.out.println("Sample1: No username and password specified!"); + return; + } + + LogManager.addListener(new DefaultLogListener()); + + SampleLoginWithProxy client = new SampleLoginWithProxy(args[0], args[1]); + // if you use http proxy, you can only use websocket protocol to connection, you need to modify the configuration + + Proxy proxy = new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress("127.0.0.1", 7890)); + +// Proxy proxy = new Proxy(Proxy.Type.HTTP, +// new InetSocketAddress("127.0.0.1", 7890)); + + client.setProxy(proxy); + client.run(); + + Thread.sleep(1000000L); + } + + @Override + public void run() { + + // If any configuration needs to be set; such as connection protocol api key, etc., you can configure it like so. + var configuration = SteamConfiguration.create(config -> { + config.withProtocolTypes(ProtocolTypes.TCP) + .withProxy(proxy); + }); + // create our steamclient instance with custom configuration. + steamClient = new SteamClient(configuration); + + // create our steamclient instance using default configuration +// steamClient = new SteamClient(); + + steamClient.setDebugNetworkListener(new NetHookNetworkListener()); + + // create the callback manager which will route callbacks to function calls + manager = new CallbackManager(steamClient); + + // get the steamuser handler, which is used for logging on after successfully connecting + steamUser = steamClient.getHandler(SteamUser.class); + + // The callbacks are a closeable, and to properly fix + // "'Closeable' used without 'try'-with-resources statement", they should be closed once done. + // Usually putting them in a list and close each of them once the client is finished is recommended. + subscriptions = new ArrayList<>(); + + // register a few callbacks we're interested in + // these are registered upon creation to a callback manager, which will then route the callbacks + // to the functions specified + subscriptions.add(manager.subscribe(ConnectedCallback.class, this::onConnected)); + subscriptions.add(manager.subscribe(DisconnectedCallback.class, this::onDisconnected)); + subscriptions.add(manager.subscribe(LoggedOnCallback.class, this::onLoggedOn)); + subscriptions.add(manager.subscribe(LoggedOffCallback.class, this::onLoggedOff)); + + isRunning = true; + + System.out.println("Connecting to steam..."); + + // initiate the connection + steamClient.connect(); + + // create our callback handling loop + while (isRunning) { + // in order for the callbacks to get routed, they need to be handled by the manager + manager.runWaitCallbacks(1000L); + } + + // Close the subscriptions when done. + System.out.println("Closing " + subscriptions.size() + " callbacks"); + for (var subscription : subscriptions) { + try { + subscription.close(); + } catch (IOException e) { + System.out.println("Couldn't close a callback."); + } + } + } + + @SuppressWarnings("DanglingJavadoc") + private void onConnected(ConnectedCallback callback) { + System.out.println("Connected to Steam! Logging in " + user + "..."); + + LogOnDetails details = new LogOnDetails(); + details.setUsername(user); + details.setWebLogonNonce(webLogonNonce); + + // Set LoginID to a non-zero value if you have another client connected using the same account, + // the same private ip, and same public ip. + details.setLoginID(149); + + steamUser.logOn(details); + } + + private void onDisconnected(DisconnectedCallback callback) { + System.out.println("Disconnected from Steam. User initialized: " + callback.isUserInitiated()); + + // If the disconnection was not user initiated, we will retry connecting to steam again after a short delay. + if (callback.isUserInitiated()) { + isRunning = false; + } else { + try { + Thread.sleep(2000L); + steamClient.connect(); + } catch (InterruptedException e) { + System.err.println("An Interrupted exception occurred. " + e.getMessage()); + } + } + } + + private void onLoggedOn(LoggedOnCallback callback) { + if (callback.getResult() != EResult.OK) { + System.out.println("Unable to logon to Steam: " + callback.getResult() + " / " + callback.getExtendedResult()); + + isRunning = false; + return; + } + + System.out.println("Successfully logged on!"); + + // at this point, we'd be able to perform actions on Steam + + // for this sample we'll just log off + steamUser.logOff(); + } + + private void onLoggedOff(LoggedOffCallback callback) { + System.out.println("Logged off of Steam: " + callback.getResult()); + + isRunning = false; + } + + + @SuppressWarnings("unused") + private void parseJsonWebToken(String token, String name) { + String[] tokenComponents = token.split("\\."); + + // Fix up base64url to normal base64 + String base64 = tokenComponents[1].replace('-', '+').replace('_', '/'); + + if (base64.length() % 4 != 0) { + base64 += new String(new char[4 - base64.length() % 4]).replace('\0', '='); + } + + byte[] payloadBytes = Base64.getDecoder().decode(base64); + + // Payload can be parsed as JSON, and then fields such expiration date, scope, etc can be accessed + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonElement payload = JsonParser.parseString(new String(payloadBytes)); + String formatted = gson.toJson(payload); + + // For brevity, we will simply output formatted json to console + System.out.println(name + ": " + formatted); + System.out.println(); + } +} diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java b/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java new file mode 100644 index 00000000..96c41a22 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java @@ -0,0 +1,271 @@ +package in.dragonbra.javasteam.networking.steam3; + +import in.dragonbra.javasteam.util.log.LogManager; +import in.dragonbra.javasteam.util.log.Logger; +import in.dragonbra.javasteam.util.stream.BinaryReader; +import in.dragonbra.javasteam.util.stream.BinaryWriter; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + + +/** + * @author cap_qin + */ +public class Socks5Connection extends Connection { + + private static final Logger logger = LogManager.getLogger(Socks5Connection.class); + + private static final int MAGIC = 0x31305456; // "VT01" + + private Socket socket; + + private InetSocketAddress currentEndPoint; + + private BinaryWriter netWriter; + + private BinaryReader netReader; + + @SuppressWarnings("FieldCanBeLocal") + private Thread netThread; + + private NetLoop netLoop; + + private final Object netLock = new Object(); + + private final Proxy proxy; + + public Socks5Connection(Proxy proxy) { + this.proxy = proxy; + } + + private void shutdown() { + try { + if (socket.isConnected()) { + socket.shutdownInput(); + socket.shutdownOutput(); + } + } catch (IOException e) { + logger.debug(e); + } + } + + private void connectionCompleted(boolean success) { + if (!success) { + logger.debug("Timed out while connecting to " + currentEndPoint); + release(false); + return; + } + + logger.debug("Connected to " + currentEndPoint); + + try { + synchronized (netLock) { + netReader = new BinaryReader(socket.getInputStream()); + netWriter = new BinaryWriter(socket.getOutputStream()); + + netLoop = new NetLoop(); + netThread = new Thread(netLoop, "Socks5Connection Thread"); + + currentEndPoint = new InetSocketAddress(socket.getInetAddress(), socket.getPort()); + } + + netThread.start(); + + onConnected(); + } catch (IOException e) { + logger.debug("Exception while setting up connection to " + currentEndPoint, e); + release(false); + } + } + + private void release(boolean userRequestedDisconnect) { + synchronized (netLock) { + if (netWriter != null) { + try { + netWriter.close(); + } catch (IOException ignored) { + } + netWriter = null; + } + + if (netReader != null) { + try { + netReader.close(); + } catch (IOException ignored) { + } + netReader = null; + } + + if (socket != null) { + try { + socket.close(); + } catch (IOException ignored) { + } + socket = null; + } + } + + onDisconnected(userRequestedDisconnect); + } + + @Override + public void connect(InetSocketAddress endPoint, int timeout) { + synchronized (netLock) { + currentEndPoint = endPoint; + try { + logger.debug(String.format("use proxy [%s:/%s] Connecting to %s...", proxy.type(), proxy.address(), currentEndPoint)); + Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxy.address())); + underlying.connect(endPoint,timeout); + socket = underlying; +// socket = factory.createSocket( +// underlying, +// ((InetSocketAddress) (proxy.address())).getAddress().getHostAddress(), +// ((InetSocketAddress) (proxy.address())).getPort(), +// true); +// socket.connect(endPoint, timeout); + + connectionCompleted(true); + } catch (Exception e) { + logger.debug("Socket exception while completing connection request to " + currentEndPoint, e); + connectionCompleted(false); + } + } + } + + /** + * Disconnects this instance. + * + * @param userInitiated + */ + @Override + public void disconnect(boolean userInitiated) { + synchronized (netLock) { + if (netLoop != null) { + netLoop.stop(userInitiated); + } + } + } + + private byte[] readPacket() throws IOException { + int packetLen = netReader.readInt(); + int packetMagic = netReader.readInt(); + + if (packetMagic != MAGIC) { + throw new IOException("Got a packet with invalid magic!"); + } + + return netReader.readBytes(packetLen); + } + + @Override + public void send(byte[] data) { + synchronized (netLock) { + if (socket == null) { + logger.debug("Attempting to send client data when not connected."); + return; + } + + try { + netWriter.writeInt(data.length); + netWriter.writeInt(MAGIC); + netWriter.write(data); + } catch (IOException e) { + logger.debug("Socket exception while writing data.", e); + + // looks like the only way to detect a closed connection is to try and write to it + // afaik read also throws an exception if the connection is open but there is nothing to read + if (netLoop != null) { + netLoop.stop(false); + } + } + } + } + + @Override + public InetAddress getLocalIP() { + synchronized (netLock) { + if (socket == null) { + return null; + } + + return socket.getLocalAddress(); + } + } + + @Override + public InetSocketAddress getCurrentEndPoint() { + return currentEndPoint; + } + + @Override + public ProtocolTypes getProtocolTypes() { + return ProtocolTypes.TCP; + } + + // this is now a steamkit meme + + /** + * Nets the loop. + */ + private class NetLoop implements Runnable { + private static final int POLL_MS = 100; + + private volatile boolean cancelRequested = false; + + private volatile boolean userRequested = false; + + void stop(boolean userRequested) { + this.userRequested = userRequested; + cancelRequested = true; + } + + @Override + public void run() { + while (!cancelRequested) { + try { + Thread.sleep(POLL_MS); + } catch (InterruptedException e) { + logger.debug("Thread interrupted", e); + } + + if (cancelRequested) { + break; + } + + boolean canRead; + + try { + canRead = netReader.available() > 0; + } catch (IOException e) { + logger.debug("Socket exception while polling", e); + break; + } + + if (!canRead) { + // nothing to read yet + continue; + } + + byte[] packData; + + try { + packData = readPacket(); + + onNetMsgReceived(new NetMsgEventArgs(packData, currentEndPoint)); + } catch (IOException e) { + logger.debug("Socket exception occurred while reading packet", e); + break; + } + } + + if (cancelRequested) { + shutdown(); + } + release(cancelRequested && userRequested); + } + } +} diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt index 0c6f0b01..8a61f8de 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt @@ -2,7 +2,9 @@ package `in`.dragonbra.javasteam.networking.steam3 import `in`.dragonbra.javasteam.util.log.LogManager import io.ktor.client.HttpClient +import io.ktor.client.engine.ProxyBuilder import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.type import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.pingInterval import io.ktor.client.plugins.websocket.webSocketSession @@ -24,11 +26,13 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.net.InetAddress import java.net.InetSocketAddress +import java.net.Proxy import kotlin.coroutines.CoroutineContext import kotlin.time.DurationUnit import kotlin.time.toDuration -class WebSocketConnection : +class WebSocketConnection(private val proxy: Proxy? = null) : + Connection(), CoroutineScope { @@ -46,19 +50,31 @@ class WebSocketConnection : private var lastFrameTime = System.currentTimeMillis() + override val coroutineContext: CoroutineContext = Dispatchers.IO + job override fun connect(endPoint: InetSocketAddress, timeout: Int) { launch { - logger.debug("Trying connection to ${endPoint.hostName}:${endPoint.port}") + if (proxy != null) { + logger.debug("Trying use proxy ${proxy.type}:/${proxy.address()} connection to ${endPoint.hostName}:${endPoint.port}") + } else { + logger.debug("Trying connection to ${endPoint.hostName}:${endPoint.port}") + } try { endpoint = endPoint - client = HttpClient(CIO) { install(WebSockets) { pingInterval = timeout.toDuration(DurationUnit.SECONDS) } + + // 如果配置了代理,可以在这里使用 + // 注意:实际应用中可能需要根据 Ktor HttpClient 的代理配置方式进行调整 + if (proxy != null) { + engine { + proxy = this@WebSocketConnection.proxy + } + } } val session = client?.webSocketSession { diff --git a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java index aa778935..97c9c44c 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java +++ b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.Proxy; import java.util.*; import java.util.zip.GZIPInputStream; @@ -293,9 +294,13 @@ protected void onClientDisconnected(boolean userInitiated) { } private Connection createConnection(EnumSet protocol) { + Proxy proxy = configuration.getProxy(); if (protocol.contains(ProtocolTypes.WEB_SOCKET)) { - return new WebSocketConnection(); + return new WebSocketConnection(proxy); } else if (protocol.contains(ProtocolTypes.TCP)) { + if (proxy != null && Proxy.Type.SOCKS == proxy.type()) { + return new EnvelopeEncryptedConnection(new Socks5Connection(proxy), getUniverse()); + } return new EnvelopeEncryptedConnection(new TcpConnection(), getUniverse()); } else if (protocol.contains(ProtocolTypes.UDP)) { return new EnvelopeEncryptedConnection(new UdpConnection(), getUniverse()); diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/LogOnDetails.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/LogOnDetails.kt index a45387ee..f332cd08 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/LogOnDetails.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/LogOnDetails.kt @@ -53,4 +53,6 @@ data class LogOnDetails( var chatMode: ChatMode = ChatMode.DEFAULT, var uiMode: EUIMode = EUIMode.Unknown, var isSteamDeck: Boolean = false, + + var webLogonNonce: String? = null, // you can use username + webLogonNonce to login . use https://steamcommunity.com/chat/clientjstoken to get the token ) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt index d14e7031..1677e745 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt @@ -5,6 +5,7 @@ import `in`.dragonbra.javasteam.base.ClientMsgProtobuf import `in`.dragonbra.javasteam.base.IPacketMsg import `in`.dragonbra.javasteam.enums.EAccountType import `in`.dragonbra.javasteam.enums.EMsg +import `in`.dragonbra.javasteam.enums.EOSType import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.enums.EUIMode import `in`.dragonbra.javasteam.generated.MsgClientLogon @@ -28,6 +29,7 @@ import `in`.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback import `in`.dragonbra.javasteam.types.SteamID import `in`.dragonbra.javasteam.util.HardwareUtils import `in`.dragonbra.javasteam.util.NetHelpers +import org.apache.commons.lang3.StringUtils /** * This handler handles all user log on/log off related actions and callbacks. @@ -56,7 +58,7 @@ class SteamUser : ClientMsgHandler() { * @param details The details to use for logging on. */ fun logOn(details: LogOnDetails) { - if (details.username.isEmpty() || (details.password.isNullOrEmpty() && details.accessToken.isNullOrEmpty())) { + if (details.username.isEmpty() || (details.password.isNullOrEmpty() && details.accessToken.isNullOrEmpty()) && details.webLogonNonce.isNullOrEmpty()) { throw IllegalArgumentException("LogOn requires a username and password or access token to be set in 'details'.") } @@ -69,6 +71,22 @@ class SteamUser : ClientMsgHandler() { val steamID = SteamID(details.accountID, details.accountInstance, client.universe, EAccountType.Individual) + // set the protocol version + logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol); + + if (details.webLogonNonce?.isNotBlank() == true) { + logon.protoHeader.setClientSessionid(0); + logon.protoHeader.setSteamid(steamID.convertToUInt64()); + + logon.body.setAccountName(details.username); + logon.body.setWebLogonNonce(details.webLogonNonce); + logon.body.setClientOsType(EOSType.Web.code()); + logon.body.setUiMode(4); + client.send(logon); + return + } + + if (details.loginID != null) { // TODO: (SK) Support IPv6 login ids? logon.body.obfuscatedPrivateIp = CMsgIPAddress.newBuilder().apply { diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/ISteamConfigurationBuilder.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/ISteamConfigurationBuilder.kt index d596aa2d..04b3054a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/ISteamConfigurationBuilder.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/ISteamConfigurationBuilder.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.networking.steam3.ProtocolTypes import `in`.dragonbra.javasteam.steam.contentdownloader.IManifestProvider import `in`.dragonbra.javasteam.steam.discovery.IServerListProvider import okhttp3.OkHttpClient +import java.net.Proxy import java.util.* /** @@ -117,4 +118,12 @@ interface ISteamConfigurationBuilder { * @return A builder with modified configuration. */ fun withWebAPIKey(webApiKey: String): ISteamConfigurationBuilder + + /** + * Configures this [SteamConfiguration] with a proxy to use when connecting to Steam. + * + * @param proxy The proxy to use when connecting to Steam. + * @return A builder with modified configuration. + */ + fun withProxy(proxy: Proxy): ISteamConfigurationBuilder } diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfiguration.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfiguration.kt index 775b70bf..374a3f93 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfiguration.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfiguration.kt @@ -10,6 +10,7 @@ import `in`.dragonbra.javasteam.steam.steamclient.SteamClient import `in`.dragonbra.javasteam.steam.webapi.WebAPI import `in`.dragonbra.javasteam.util.compat.Consumer import okhttp3.OkHttpClient +import java.net.Proxy import java.util.* /** @@ -90,6 +91,9 @@ class SteamConfiguration internal constructor(private val state: SteamConfigurat */ val serverList: SmartCMServerList = SmartCMServerList(this) + val proxy: Proxy? + get() = state.proxy + /** * Retrieves a handler capable of interacting with the specified interface on the Web API. * diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationBuilder.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationBuilder.kt index bd04cd55..0e1dd2e8 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationBuilder.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationBuilder.kt @@ -9,6 +9,7 @@ import `in`.dragonbra.javasteam.steam.discovery.IServerListProvider import `in`.dragonbra.javasteam.steam.discovery.MemoryServerListProvider import `in`.dragonbra.javasteam.steam.webapi.WebAPI import okhttp3.OkHttpClient +import java.net.Proxy import java.util.* /** @@ -86,6 +87,17 @@ class SteamConfigurationBuilder : ISteamConfigurationBuilder { return this } + /** + * Configures this [SteamConfiguration] with a proxy to use when connecting to Steam. + * + * @param proxy The proxy to use when connecting to Steam. + * @return A builder with modified configuration. + */ + override fun withProxy(proxy: Proxy): ISteamConfigurationBuilder { + state.proxy = proxy + return this + } + companion object { @JvmStatic fun createDefaultState(): SteamConfigurationState = SteamConfigurationState( @@ -106,6 +118,7 @@ class SteamConfigurationBuilder : ISteamConfigurationBuilder { webAPIBaseAddress = WebAPI.DEFAULT_BASE_ADDRESS, cellID = 0, webAPIKey = null, + proxy = null, ) } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt index e4810ab1..f41dd498 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.networking.steam3.ProtocolTypes import `in`.dragonbra.javasteam.steam.contentdownloader.IManifestProvider import `in`.dragonbra.javasteam.steam.discovery.IServerListProvider import okhttp3.OkHttpClient +import java.net.Proxy import java.util.EnumSet /** @@ -24,4 +25,5 @@ data class SteamConfigurationState( var universe: EUniverse, var webAPIBaseAddress: String, var webAPIKey: String?, + var proxy: Proxy? ) From 95e126f0e8b43efe124cdac7d6e7f93451f878f0 Mon Sep 17 00:00:00 2001 From: jaymie9019 Date: Tue, 18 Mar 2025 09:53:54 +0800 Subject: [PATCH 2/2] chore: modify as per the pr cr request --- .../SampleWebLogonNonce.java | 7 +------ .../SampleLoginWithProxy.java | 2 -- .../networking/steam3/Socks5Connection.java | 9 ++------- .../networking/steam3/WebSocketConnection.kt | 4 ---- .../steam/handlers/steamuser/SteamUser.kt | 19 ++++++++----------- .../configuration/SteamConfigurationState.kt | 2 +- 6 files changed, 12 insertions(+), 31 deletions(-) diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java index 4d6745ad..88c6ae2c 100644 --- a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_006_weblogonnonce/SampleWebLogonNonce.java @@ -6,7 +6,6 @@ import com.google.gson.JsonParser; import in.dragonbra.javasteam.enums.EResult; import in.dragonbra.javasteam.networking.steam3.ProtocolTypes; -import in.dragonbra.javasteam.steam.authentication.*; import in.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails; import in.dragonbra.javasteam.steam.handlers.steamuser.SteamUser; import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOffCallback; @@ -19,14 +18,12 @@ import in.dragonbra.javasteam.util.NetHookNetworkListener; import in.dragonbra.javasteam.util.log.DefaultLogListener; import in.dragonbra.javasteam.util.log.LogManager; -import in.dragonbra.javasteamsamples._000_authentication.SampleLogonAuthentication; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; -import java.util.concurrent.CancellationException; /** * @author jaymie @@ -58,7 +55,6 @@ public SampleWebLogonNonce(String user, String webLogonNonce) { public static void main(String[] args) throws InterruptedException { // you can view https://steamcommunity.com/chat/clientjstoken to get webLogonNonce, the property name is "token" // webLoginNonce can only use one time, it's more safety for your customer - if (args.length < 2) { System.out.println("Sample1: No username and password specified!"); return; @@ -66,8 +62,7 @@ public static void main(String[] args) throws InterruptedException { LogManager.addListener(new DefaultLogListener()); - new SampleLogonAuthentication(args[0], args[1]).run(); - Thread.sleep(1000000L); + new SampleWebLogonNonce(args[0], args[1]).run(); } @Override diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java index b6196607..28f8aca8 100644 --- a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_007_loginwithproxy/SampleLoginWithProxy.java @@ -82,8 +82,6 @@ public static void main(String[] args) throws InterruptedException { client.setProxy(proxy); client.run(); - - Thread.sleep(1000000L); } @Override diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java b/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java index 96c41a22..94ec98bb 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/Socks5Connection.java @@ -13,7 +13,8 @@ /** - * @author cap_qin + * @author jaymie + * @since 2025-03-14 */ public class Socks5Connection extends Connection { @@ -121,12 +122,6 @@ public void connect(InetSocketAddress endPoint, int timeout) { Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxy.address())); underlying.connect(endPoint,timeout); socket = underlying; -// socket = factory.createSocket( -// underlying, -// ((InetSocketAddress) (proxy.address())).getAddress().getHostAddress(), -// ((InetSocketAddress) (proxy.address())).getPort(), -// true); -// socket.connect(endPoint, timeout); connectionCompleted(true); } catch (Exception e) { diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt index 8a61f8de..f7c9a5e7 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt @@ -2,7 +2,6 @@ package `in`.dragonbra.javasteam.networking.steam3 import `in`.dragonbra.javasteam.util.log.LogManager import io.ktor.client.HttpClient -import io.ktor.client.engine.ProxyBuilder import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.type import io.ktor.client.plugins.websocket.WebSockets @@ -50,7 +49,6 @@ class WebSocketConnection(private val proxy: Proxy? = null) : private var lastFrameTime = System.currentTimeMillis() - override val coroutineContext: CoroutineContext = Dispatchers.IO + job override fun connect(endPoint: InetSocketAddress, timeout: Int) { @@ -68,8 +66,6 @@ class WebSocketConnection(private val proxy: Proxy? = null) : pingInterval = timeout.toDuration(DurationUnit.SECONDS) } - // 如果配置了代理,可以在这里使用 - // 注意:实际应用中可能需要根据 Ktor HttpClient 的代理配置方式进行调整 if (proxy != null) { engine { proxy = this@WebSocketConnection.proxy diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt index 1677e745..9b02116c 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/SteamUser.kt @@ -29,7 +29,6 @@ import `in`.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback import `in`.dragonbra.javasteam.types.SteamID import `in`.dragonbra.javasteam.util.HardwareUtils import `in`.dragonbra.javasteam.util.NetHelpers -import org.apache.commons.lang3.StringUtils /** * This handler handles all user log on/log off related actions and callbacks. @@ -72,21 +71,19 @@ class SteamUser : ClientMsgHandler() { val steamID = SteamID(details.accountID, details.accountInstance, client.universe, EAccountType.Individual) // set the protocol version - logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol); + logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol) if (details.webLogonNonce?.isNotBlank() == true) { - logon.protoHeader.setClientSessionid(0); - logon.protoHeader.setSteamid(steamID.convertToUInt64()); - - logon.body.setAccountName(details.username); - logon.body.setWebLogonNonce(details.webLogonNonce); - logon.body.setClientOsType(EOSType.Web.code()); - logon.body.setUiMode(4); - client.send(logon); + logon.protoHeader.setClientSessionid(0) + logon.protoHeader.setSteamid(steamID.convertToUInt64()) + + logon.body.setAccountName(details.username) + logon.body.setWebLogonNonce(details.webLogonNonce) + logon.body.setClientOsType(EOSType.Web.code()) + client.send(logon) return } - if (details.loginID != null) { // TODO: (SK) Support IPv6 login ids? logon.body.obfuscatedPrivateIp = CMsgIPAddress.newBuilder().apply { diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt index f41dd498..5c2073d5 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/configuration/SteamConfigurationState.kt @@ -25,5 +25,5 @@ data class SteamConfigurationState( var universe: EUniverse, var webAPIBaseAddress: String, var webAPIKey: String?, - var proxy: Proxy? + var proxy: Proxy?, )