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 8f648453..ef50c6a7 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.net.InetAddress import java.net.InetSocketAddress +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.CoroutineContext import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -46,6 +48,8 @@ class WebSocketConnection : private var lastFrameTime = System.currentTimeMillis() + private val isDisconnecting = AtomicBoolean(false) + override val coroutineContext: CoroutineContext = Dispatchers.IO + job override fun connect(endPoint: InetSocketAddress, timeout: Int) { @@ -53,6 +57,7 @@ class WebSocketConnection : logger.debug("Trying connection to ${endPoint.hostName}:${endPoint.port}") try { + isDisconnecting.set(false) endpoint = endPoint client = HttpClient(CIO) { @@ -90,6 +95,8 @@ class WebSocketConnection : is Frame.Text -> logger.debug("Received plain text ${frame.readText()}") } } + } catch (e: CancellationException) { + logger.debug("Frame listener was cancelled", e) } catch (e: Exception) { logger.error("An error occurred while receiving data", e) disconnect(false) @@ -106,7 +113,13 @@ class WebSocketConnection : } override fun disconnect(userInitiated: Boolean) { + if (!isDisconnecting.compareAndSet(false, true)) { + logger.error("Already disconnected or disconnecting") + return + } + logger.debug("Disconnect called: $userInitiated") + launch { try { session?.close() @@ -123,6 +136,10 @@ class WebSocketConnection : } override fun send(data: ByteArray) { + if (isDisconnecting.get()) { + return + } + launch { try { val frame = Frame.Binary(true, data) @@ -145,30 +162,34 @@ class WebSocketConnection : */ private fun startConnectionMonitoring() { launch { - while (isActive) { - if (client?.isActive == false || session?.isActive == false) { - logger.error("Client or Session is no longer active") - disconnect(userInitiated = false) - } - - val timeSinceLastFrame = System.currentTimeMillis() - lastFrameTime - - // logger.debug("Watchdog status: $timeSinceLastFrame") - when { - timeSinceLastFrame > 30000 -> { - logger.error("Watchdog: No response for 30 seconds. Disconnecting from steam") + try { + while (isActive && !isDisconnecting.get()) { + if (client?.isActive == false || session?.isActive == false) { + logger.error("Client or Session is no longer active") disconnect(userInitiated = false) - break } - timeSinceLastFrame > 25000 -> logger.debug("Watchdog: No response for 25 seconds") + val timeSinceLastFrame = System.currentTimeMillis() - lastFrameTime - timeSinceLastFrame > 20000 -> logger.debug("Watchdog: No response for 20 seconds") + // logger.debug("Watchdog status: $timeSinceLastFrame") + when { + timeSinceLastFrame > 30000 -> { + logger.error("Watchdog: No response for 30 seconds. Disconnecting from steam") + disconnect(userInitiated = false) + break + } - timeSinceLastFrame > 15000 -> logger.debug("Watchdog: No response for 15 seconds") - } + timeSinceLastFrame > 25000 -> logger.debug("Watchdog: No response for 25 seconds") - delay(5000) + timeSinceLastFrame > 20000 -> logger.debug("Watchdog: No response for 20 seconds") + + timeSinceLastFrame > 15000 -> logger.debug("Watchdog: No response for 15 seconds") + } + + delay(5000) + } + } catch (e: CancellationException) { + logger.debug("Watchdog Cancelled.", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java deleted file mode 100644 index 278e53c6..00000000 --- a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java +++ /dev/null @@ -1,648 +0,0 @@ -package in.dragonbra.javasteam.steam; - -import in.dragonbra.javasteam.base.*; -import in.dragonbra.javasteam.enums.EMsg; -import in.dragonbra.javasteam.enums.EResult; -import in.dragonbra.javasteam.enums.EUniverse; -import in.dragonbra.javasteam.generated.MsgClientLogon; -import in.dragonbra.javasteam.generated.MsgClientServerUnavailable; -import in.dragonbra.javasteam.networking.steam3.*; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesBase.CMsgMulti; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserver.CMsgClientSessionToken; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientHeartBeat; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientHello; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLoggedOff; -import in.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLogonResponse; -import in.dragonbra.javasteam.steam.discovery.ServerQuality; -import in.dragonbra.javasteam.steam.discovery.ServerRecord; -import in.dragonbra.javasteam.steam.discovery.SmartCMServerList; -import in.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration; -import in.dragonbra.javasteam.types.SteamID; -import in.dragonbra.javasteam.util.*; -import in.dragonbra.javasteam.util.event.EventArgs; -import in.dragonbra.javasteam.util.event.EventHandler; -import in.dragonbra.javasteam.util.event.ScheduledFunction; -import in.dragonbra.javasteam.util.log.LogManager; -import in.dragonbra.javasteam.util.log.Logger; -import in.dragonbra.javasteam.util.stream.BinaryReader; -import org.jetbrains.annotations.Nullable; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.EnumSet; -import java.util.zip.GZIPInputStream; - -/** - * This base client handles the underlying connection to a CM server. This class should not be use directly, but through - * the {@link in.dragonbra.javasteam.steam.steamclient.SteamClient SteamClient} class. - */ -public abstract class CMClient { - - private static final Logger logger = LogManager.getLogger(CMClient.class); - - private final SteamConfiguration configuration; - - @Nullable - private InetAddress publicIP; - - @Nullable - private String ipCountryCode; - - private boolean isConnected; - - private long sessionToken; - - @Nullable - private Integer cellID; - - @Nullable - private Integer sessionID; - - @Nullable - private SteamID steamID; - - private IDebugNetworkListener debugNetworkListener; - - private boolean expectDisconnection; - - // connection lock around the setup and tear down of the connection task - private final Object connectionLock = new Object(); - - @Nullable - private Connection connection; - - private final ScheduledFunction heartBeatFunc; - - private final EventHandler netMsgReceived = (sender, e) -> onClientMsgReceived(getPacketMsg(e.getData())); - - private final EventHandler connected = (sender, e) -> { - logger.debug("EventHandler `connected` called"); - - getServers().tryMark(connection.getCurrentEndPoint(), connection.getProtocolTypes(), ServerQuality.GOOD); - - isConnected = true; - - try { - onClientConnected(); - } catch (Exception ex) { - logger.error("Unhandled exception after connecting: ", ex); - disconnect(false); - } - }; - - private final EventHandler disconnected = new EventHandler<>() { - @Override - public void handleEvent(Object sender, DisconnectedEventArgs e) { - logger.debug("EventHandler `disconnected` called. User Initiated: " + e.isUserInitiated() + - ", Expected Disconnection: " + expectDisconnection); - - isConnected = false; - - if (!e.isUserInitiated() && !expectDisconnection) { - getServers().tryMark(connection.getCurrentEndPoint(), connection.getProtocolTypes(), ServerQuality.BAD); - } - - sessionID = null; - steamID = null; - - connection.getNetMsgReceived().removeEventHandler(netMsgReceived); - connection.getConnected().removeEventHandler(connected); - connection.getDisconnected().removeEventHandler(this); - connection = null; - - heartBeatFunc.stop(); - - onClientDisconnected(e.isUserInitiated() || expectDisconnection); - } - }; - - public CMClient(SteamConfiguration configuration) { - if (configuration == null) { - throw new IllegalArgumentException("configuration is null"); - } - - this.configuration = configuration; - - heartBeatFunc = new ScheduledFunction(() -> { - var heartbeat = new ClientMsgProtobuf( - CMsgClientHeartBeat.class, EMsg.ClientHeartBeat); - heartbeat.getBody().setSendReply(true); // Ping Pong - send(heartbeat); - }, 5000); - } - - /** - * Debugging only method: - * Do not use this directly. - */ - public void receiveTestPacketMsg(IPacketMsg packetMsg) { - onClientMsgReceived(packetMsg); - } - - /** - * Debugging only method: - * Do not use this directly. - */ - public void setIsConnected(boolean value) { - isConnected = value; - } - - /** - * Connects this client to a Steam3 server. This begins the process of connecting and encrypting the data channel - * between the client and the server. Results are returned asynchronously in a {@link in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback ConnectedCallback}. If the - * server that SteamKit attempts to connect to is down, a {@link in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback DisconnectedCallback} will be posted instead. - * SteamKit will not attempt to reconnect to Steam, you must handle this callback and call Connect again preferably - * after a short delay. SteamKit will randomly select a CM server from its internal list. - */ - public void connect() { - connect(null); - } - - /** - * Connects this client to a Steam3 server. This begins the process of connecting and encrypting the data channel - * between the client and the server. Results are returned asynchronously in a {@link in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback ConnectedCallback}. If the - * server that SteamKit attempts to connect to is down, a {@link in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback DisconnectedCallback} will be posted instead. - * SteamKit will not attempt to reconnect to Steam, you must handle this callback and call Connect again preferably - * after a short delay. - * - * @param cmServer The {@link ServerRecord} of the CM server to connect to. - */ - public void connect(ServerRecord cmServer) { - synchronized (connectionLock) { - try { - disconnect(true); - - assert connection == null; - - expectDisconnection = false; - - if (cmServer == null) { - cmServer = getServers().getNextServerCandidate(configuration.getProtocolTypes()); - } - - if (cmServer == null) { - logger.error("No CM servers available to connect to"); - onClientDisconnected(false); - return; - } - - connection = createConnection(cmServer.getProtocolTypes()); - connection.getNetMsgReceived().addEventHandler(netMsgReceived); - connection.getConnected().addEventHandler(connected); - connection.getDisconnected().addEventHandler(disconnected); - logger.debug(String.format("Connecting to %s with protocol %s, and with connection impl %s", - cmServer.getEndpoint(), cmServer.getProtocolTypes(), connection.getClass().getSimpleName())); - connection.connect(cmServer.getEndpoint()); - } catch (Exception e) { - logger.debug("Failed to connect to Steam network", e); - onClientDisconnected(false); - } - } - } - - /** - * Disconnects this client. - */ - public void disconnect() { - disconnect(true); - } - - private void disconnect(boolean userInitiated) { - synchronized (connectionLock) { - heartBeatFunc.stop(); - - if (connection != null) { - connection.disconnect(userInitiated); - } - } - } - - /** - * Sends the specified client message to the server. This method automatically assigns the correct SessionID and - * SteamID of the message. - * - * @param msg The client message to send. - */ - public void send(IClientMsg msg) { - if (msg == null) { - throw new IllegalArgumentException("A value for 'msg' must be supplied"); - } - - Integer _sessionID = this.sessionID; - - if (_sessionID != null) { - msg.setSessionID(_sessionID); - } - - SteamID _steamID = this.steamID; - - if (_steamID != null) { - msg.setSteamID(_steamID); - } - - try { - if (debugNetworkListener != null) { - debugNetworkListener.onOutgoingNetworkMessage(msg.getMsgType(), msg.serialize()); - } - } catch (Exception e) { - logger.debug("DebugNetworkListener threw an exception", e); - } - - // we'll swallow any network failures here because they will be thrown later - // on the network thread, and that will lead to a disconnect callback - // down the line - - if (connection != null) { - connection.send(msg.serialize()); - } - } - - protected boolean onClientMsgReceived(IPacketMsg packetMsg) { - if (packetMsg == null) { - logger.debug("Packet message failed to parse, shutting down connection"); - disconnect(false); - return false; - } - - // Multi message gets logged down the line after it's decompressed - if (packetMsg.getMsgType() != EMsg.Multi) { - try { - if (debugNetworkListener != null) { - debugNetworkListener.onIncomingNetworkMessage(packetMsg.getMsgType(), packetMsg.getData()); - } - } catch (Exception e) { - logger.debug("debugNetworkListener threw an exception", e); - } - } - - switch (packetMsg.getMsgType()) { - case Multi: - handleMulti(packetMsg); - break; - case ClientLogOnResponse: // we handle this to get the SteamID/SessionID and to set up heart beating - handleLogOnResponse(packetMsg); - break; - case ClientLoggedOff: // to stop heart beating when we get logged off - handleLoggedOff(packetMsg); - break; - case ClientServerUnavailable: - handleServerUnavailable(packetMsg); - break; - case ClientSessionToken: // am session token - handleSessionToken(packetMsg); - break; - } - - return true; - } - - /** - * Called when the client is securely isConnected to Steam3. - */ - protected void onClientConnected() { - ClientMsgProtobuf request = new ClientMsgProtobuf<>(CMsgClientHello.class, EMsg.ClientHello); - request.getBody().setProtocolVersion(MsgClientLogon.CurrentProtocol); - - send(request); - } - - /** - * Called when the client is physically disconnected from Steam3. - * - * @param userInitiated whether the disconnect was initialized by the client - */ - protected void onClientDisconnected(boolean userInitiated) { - } - - private Connection createConnection(EnumSet protocol) { - IConnectionFactory connectionFactory = configuration.getConnectionFactory(); - Connection connection = connectionFactory.createConnection(configuration, protocol); - if (connection == null) { - logger.error(String.format("Connection factory returned null connection for protocols %s", protocol)); - throw new IllegalArgumentException("Connection factory returned null connection."); - } - return connection; - } - - public static IPacketMsg getPacketMsg(byte[] data) { - if (data.length < 4) { - logger.debug("PacketMsg too small to contain a message, was only " + data.length + " bytes. Message: " + Strings.toHex(data)); - return null; - } - - int rawEMsg = 0; - try (var reader = new BinaryReader(new ByteArrayInputStream(data))) { - rawEMsg = reader.readInt(); - } catch (IOException e) { - logger.debug("Exception while getting EMsg code", e); - } - EMsg eMsg = MsgUtil.getMsg(rawEMsg); - - switch (eMsg) { - case ChannelEncryptRequest: - case ChannelEncryptResponse: - case ChannelEncryptResult: - try { - return new PacketMsg(eMsg, data); - } catch (IOException e) { - logger.debug("Exception deserializing emsg " + eMsg + " (" + MsgUtil.isProtoBuf(rawEMsg) + ").", e); - } - } - - try { - if (MsgUtil.isProtoBuf(rawEMsg)) { - // if the emsg is flagged, we're a proto message - return new PacketClientMsgProtobuf(eMsg, data); - } else { - return new PacketClientMsg(eMsg, data); - } - } catch (IOException e) { - logger.debug("Exception deserializing emsg " + eMsg + " (" + MsgUtil.isProtoBuf(rawEMsg) + ").", e); - return null; - } - } - - private void handleMulti(IPacketMsg packetMsg) { - if (!packetMsg.isProto()) { - logger.debug("HandleMulti got non-proto MsgMulti!!"); - return; - } - - ClientMsgProtobuf msgMulti = new ClientMsgProtobuf<>(CMsgMulti.class, packetMsg); - - byte[] payload = msgMulti.getBody().getMessageBody().toByteArray(); - - if (msgMulti.getBody().getSizeUnzipped() > 0) { - try (var gzin = new GZIPInputStream(new ByteArrayInputStream(payload)); - var baos = new ByteArrayOutputStream()) { - int res = 0; - byte[] buf = new byte[1024]; - while (res >= 0) { - res = gzin.read(buf, 0, buf.length); - if (res > 0) { - baos.write(buf, 0, res); - } - } - payload = baos.toByteArray(); - } catch (IOException e) { - logger.debug("HandleMulti encountered an exception when decompressing.", e); - return; - } - } - - try (var bais = new ByteArrayInputStream(payload); - var br = new BinaryReader(bais)) { - while (br.available() > 0) { - int subSize = br.readInt(); - byte[] subData = br.readBytes(subSize); - - if (!onClientMsgReceived(getPacketMsg(subData))) { - break; - } - } - } catch (IOException e) { - logger.error("error in handleMulti()", e); - } - } - - private void handleLogOnResponse(IPacketMsg packetMsg) { - if (!packetMsg.isProto()) { - // a non-proto ClientLogonResponse can come in as a result of connecting but never sending a ClientLogon - // in this case, it always fails, so we don't need to do anything special here - logger.debug("Got non-proto logon response, this is indicative of no logon attempt after connecting."); - return; - } - - ClientMsgProtobuf logonResp = new ClientMsgProtobuf<>(CMsgClientLogonResponse.class, packetMsg); - - EResult logonResponse = EResult.from(logonResp.getBody().getEresult()); - EResult extendedResponse = EResult.from(logonResp.getBody().getEresultExtended()); - logger.debug("handleLogOnResponse got response: " + logonResponse + ", extended: " + extendedResponse); - - // Note: Sometimes if you sign in too many times, steam may confuse "InvalidPassword" with "RateLimitExceeded" - - if (logonResponse == EResult.OK) { - sessionID = logonResp.getProtoHeader().getClientSessionid(); - steamID = new SteamID(logonResp.getProtoHeader().getSteamid()); - - cellID = logonResp.getBody().getCellId(); - publicIP = NetHelpers.getIPAddress(logonResp.getBody().getPublicIp()); - ipCountryCode = logonResp.getBody().getIpCountryCode(); - - // restart heartbeat - heartBeatFunc.stop(); - heartBeatFunc.setDelay(logonResp.getBody().getLegacyOutOfGameHeartbeatSeconds() * 1000L); - heartBeatFunc.start(); - } else if (logonResponse == EResult.TryAnotherCM || logonResponse == EResult.ServiceUnavailable) { - var connection = this.connection; - if (connection != null) { - getServers().tryMark(connection.getCurrentEndPoint(), connection.getProtocolTypes(), ServerQuality.BAD); - } else { - logger.error("Connection was null trying to mark endpoint bad."); - } - } - } - - private void handleLoggedOff(IPacketMsg packetMsg) { - sessionID = null; - steamID = null; - - cellID = null; - publicIP = null; - ipCountryCode = null; - - heartBeatFunc.stop(); - - if (packetMsg.isProto()) { - ClientMsgProtobuf logoffMsg = new ClientMsgProtobuf<>(CMsgClientLoggedOff.class, packetMsg); - EResult logoffResult = EResult.from(logoffMsg.getBody().getEresult()); - - logger.debug("handleLoggedOff got " + logoffResult); - - if (logoffResult == EResult.TryAnotherCM || logoffResult == EResult.ServiceUnavailable) { - var connection = this.connection; - if (connection != null) { - getServers().tryMark(connection.getCurrentEndPoint(), connection.getProtocolTypes(), ServerQuality.BAD); - } else { - logger.error("Connection was null trying to mark endpoint bad."); - } - } - } else { - logger.debug("handleLoggedOff got unexpected response: " + packetMsg.getMsgType()); - } - } - - private void handleServerUnavailable(IPacketMsg packetMsg) { - var msgServerUnavailable = new ClientMsg<>(MsgClientServerUnavailable.class, packetMsg); - - logger.debug("A server of type " + msgServerUnavailable.getBody().getEServerTypeUnavailable() + - "was not available for request: " + EMsg.from(msgServerUnavailable.getBody().getEMsgSent())); - - disconnect(false); - } - - private void handleSessionToken(IPacketMsg packetMsg) { - ClientMsgProtobuf sessToken = new ClientMsgProtobuf<>(CMsgClientSessionToken.class, packetMsg); - - sessionToken = sessToken.getBody().getToken(); - } - - public SteamConfiguration getConfiguration() { - return configuration; - } - - /** - * @return Bootstrap list of CM servers. - */ - public SmartCMServerList getServers() { - return configuration.getServerList(); - } - - /** - * Returns the local IP of this client. - * - * @return The local IP or null if no connection is available. - */ - public @Nullable InetAddress getLocalIP() { - var connection = this.connection; - if (connection == null) { - return null; - } - return connection.getLocalIP(); - } - - /** - * Returns the current endpoint this client is connected to. - * - * @return The current endpoint or null if no connection is available. - */ - public @Nullable InetSocketAddress getCurrentEndpoint() { - var connection = this.connection; - if (connection == null) { - return null; - } - return connection.getCurrentEndPoint(); - } - - /** - * Gets the public IP address of this client. This value is assigned after a logon attempt has succeeded. - * This value will be null if the client is logged off of Steam. - * - * @return The {@link InetSocketAddress} public ip - */ - public @Nullable InetAddress getPublicIP() { - return publicIP; - } - - /** - * Gets the country code of our public IP address according to Steam. This value is assigned after a logon attempt has succeeded. - * This value will be null if the client is logged off of Steam. - * - * @return the ip country code. - */ - public @Nullable String getIpCountryCode() { - return ipCountryCode; - } - - /** - * Gets the universe of this client. - * - * @return The universe. - */ - public EUniverse getUniverse() { - return configuration.getUniverse(); - } - - /** - * Gets a value indicating whether this instance is isConnected to the remote CM server. - * - * @return true if this instance is isConnected; otherwise, false. - */ - public boolean isConnected() { - return isConnected; - } - - /** - * Gets a value indicating whether isConnected and connection is not connected to the remote CM server. - * Inverse alternative to {@link CMClient#isConnected()} - * - * @return true is this instance is disconnected, otherwise, false. - */ - // > "since the client can technically not be connected but still have a connection" - public boolean isDisconnected() { - return !isConnected && connection == null; - } - - /** - * @return the session token assigned to this client from the AM. - */ - public long getSessionToken() { - return sessionToken; - } - - /** - * @return the Steam recommended Cell ID of this client. This value is assigned after a logon attempt has succeeded. - * This value will be null if the client is logged off of Steam. - */ - public @Nullable Integer getCellID() { - return cellID; - } - - /** - * Gets the session ID of this client. This value is assigned after a logon attempt has succeeded. - * This value will be null if the client is logged off of Steam. - * - * @return The session ID. - */ - public @Nullable Integer getSessionID() { - return sessionID; - } - - /** - * Gets the SteamID of this client. This value is assigned after a logon attempt has succeeded. - * This value will be null if the client is logged off of Steam. - * - * @return The SteamID. - */ - public @Nullable SteamID getSteamID() { - return steamID; - } - - /** - * Gets or sets the connection timeout used when connecting to the Steam server. - * - * @return The connection timeout. - */ - public long getConnectionTimeout() { - return configuration.getConnectionTimeout(); - } - - /** - * @return the network listening interface. Use this for debugging only. - * For your convenience, you can use {@link NetHookNetworkListener} class. - */ - public IDebugNetworkListener getDebugNetworkListener() { - return debugNetworkListener; - } - - /** - * Sets the network listening interface. Use this for debugging only. - * For your convenience, you can use {@link NetHookNetworkListener} class. - * - * @param debugNetworkListener the listener - */ - public void setDebugNetworkListener(IDebugNetworkListener debugNetworkListener) { - this.debugNetworkListener = debugNetworkListener; - } - - public boolean isExpectDisconnection() { - return expectDisconnection; - } - - public void setExpectDisconnection(boolean expectDisconnection) { - this.expectDisconnection = expectDisconnection; - } -} diff --git a/src/main/java/in/dragonbra/javasteam/steam/CMClient.kt b/src/main/java/in/dragonbra/javasteam/steam/CMClient.kt new file mode 100644 index 00000000..5e84e18f --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/CMClient.kt @@ -0,0 +1,599 @@ +package `in`.dragonbra.javasteam.steam + +import `in`.dragonbra.javasteam.base.ClientMsg +import `in`.dragonbra.javasteam.base.ClientMsgProtobuf +import `in`.dragonbra.javasteam.base.IClientMsg +import `in`.dragonbra.javasteam.base.IPacketMsg +import `in`.dragonbra.javasteam.base.PacketClientMsg +import `in`.dragonbra.javasteam.base.PacketClientMsgProtobuf +import `in`.dragonbra.javasteam.base.PacketMsg +import `in`.dragonbra.javasteam.enums.EMsg +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.enums.EUniverse +import `in`.dragonbra.javasteam.generated.MsgClientLogon +import `in`.dragonbra.javasteam.generated.MsgClientServerUnavailable +import `in`.dragonbra.javasteam.networking.steam3.Connection +import `in`.dragonbra.javasteam.networking.steam3.DisconnectedEventArgs +import `in`.dragonbra.javasteam.networking.steam3.IConnectionFactory +import `in`.dragonbra.javasteam.networking.steam3.NetMsgEventArgs +import `in`.dragonbra.javasteam.networking.steam3.ProtocolTypes +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesBase.CMsgMulti +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserver.CMsgClientSessionToken +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientHeartBeat +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientHello +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLoggedOff +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLogonResponse +import `in`.dragonbra.javasteam.steam.discovery.ServerQuality +import `in`.dragonbra.javasteam.steam.discovery.ServerRecord +import `in`.dragonbra.javasteam.steam.discovery.SmartCMServerList +import `in`.dragonbra.javasteam.steam.steamclient.SteamClient +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.types.SteamID +import `in`.dragonbra.javasteam.util.IDebugNetworkListener +import `in`.dragonbra.javasteam.util.MsgUtil +import `in`.dragonbra.javasteam.util.NetHelpers +import `in`.dragonbra.javasteam.util.NetHookNetworkListener +import `in`.dragonbra.javasteam.util.Strings +import `in`.dragonbra.javasteam.util.event.EventArgs +import `in`.dragonbra.javasteam.util.event.EventHandler +import `in`.dragonbra.javasteam.util.event.ScheduledFunction +import `in`.dragonbra.javasteam.util.log.LogManager +import `in`.dragonbra.javasteam.util.log.Logger +import `in`.dragonbra.javasteam.util.stream.BinaryReader +import okio.Buffer +import okio.Source +import okio.buffer +import okio.gzip +import java.io.ByteArrayInputStream +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.util.EnumSet + +/** + * This base client handles the underlying connection to a CM server. This class should not be use directly, + * but through the [SteamClient] class. + * + * @constructor Initializes a new instance of the [CMClient] class with a specific configuration. + * @param configuration The configuration to use for this client. + * @param identifier A specific identifier to be used to uniquely identify this instance. + * @throws IllegalArgumentException The identifier is an empty string + */ +@Suppress("unused") +abstract class CMClient +@Throws(IllegalArgumentException::class) +constructor( + val configuration: SteamConfiguration, + val identifier: String, +) { + companion object { + private val logger: Logger = LogManager.getLogger(CMClient::class.java) + + // TODO move to Utils once that is ported to Kotlin + fun Long.toSteamID(): SteamID = SteamID(this) + + @JvmStatic + fun getPacketMsg(data: ByteArray): IPacketMsg? { + if (data.size < 4) { + logger.debug( + "PacketMsg too small to contain a message, was only ${data.size} bytes. " + + "Message: ${Strings.toHex(data)}" + ) + return null + } + + var rawEMsg = 0 + BinaryReader(ByteArrayInputStream(data)).use { reader -> + rawEMsg = reader.readInt() + } + val eMsg: EMsg = MsgUtil.getMsg(rawEMsg) + + when (eMsg) { + EMsg.ChannelEncryptRequest, + EMsg.ChannelEncryptResponse, + EMsg.ChannelEncryptResult, + -> try { + return PacketMsg(eMsg, data) + } catch (e: IOException) { + logger.debug("Exception deserializing emsg $eMsg (${MsgUtil.isProtoBuf(rawEMsg)}).", e) + } + + else -> Unit + } + + try { + return if (MsgUtil.isProtoBuf(rawEMsg)) { + // if the emsg is flagged, we're a proto message + PacketClientMsgProtobuf(eMsg, data) + } else { + PacketClientMsg(eMsg, data) + } + } catch (e: IOException) { + logger.debug("Exception deserializing emsg $eMsg (${MsgUtil.isProtoBuf(rawEMsg)}).", e) + return null + } + } + } + + /** + * Bootstrap list of CM servers. + */ + val servers: SmartCMServerList + get() = configuration.serverList + + /** + * Returns the local IP of this client. + */ + val localIP: InetAddress? + get() = connection?.localIP + + /** + * Returns the current endpoint this client is connected to. + */ + val currentEndPoint: InetSocketAddress? + get() = connection?.currentEndPoint + + /** + * Gets the public IP address of this client. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + */ + var publicIP: InetAddress? = null + private set + + /** + * Gets the country code of our public IP address according to Steam. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + */ + var ipCountryCode: String? = null + private set + + /** + * Gets the universe of this client. + */ + val universe: EUniverse + get() = configuration.universe + + /** + * Gets a value indicating whether this instance is connected to the remote CM server. + * true if this instance is connected; otherwise, false. + */ + var isConnected: Boolean = false + private set + + /** + * Gets a value indicating whether isConnected and connection is not connected to the remote CM server. + * Inverse alternative to [CMClient.isConnected] + * @return **true** is this instance is disconnected, otherwise, **false**. + */ + // JavaSteam addition: "since the client can technically not be connected but still have a connection" + fun isDisconnected(): Boolean = !isConnected && connection == null + + /** + * Gets the session token assigned to this client from the AM. + */ + var sessionToken: Long = 0L + private set + + /** + * Gets the Steam recommended Cell ID of this client. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + */ + var cellID: Int? = null + private set + + /** + * Gets the session ID of this client. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + */ + var sessionID: Int? = null + private set + + /** + * Gets the SteamID of this client. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + */ + var steamID: SteamID? = null + private set + + /** + * Gets or sets the connection timeout used when connecting to the Steam server. + */ + val connectionTimeout: Long + get() = configuration.connectionTimeout + + /** + * Gets or sets the network listening interface. Use this for debugging only. + * For your convenience, you can use [NetHookNetworkListener] class. + */ + var debugNetworkListener: IDebugNetworkListener? = null + + /** + * + */ + var expectDisconnection: Boolean = false + + // connection lock around the setup and tear down of the connection task + private val connectionLock: Any = Any() + + @Volatile + private var connection: Connection? = null + + val heartBeatFunc: ScheduledFunction + + init { + if (identifier.isBlank()) { + throw IllegalArgumentException("identifier must not be empty") + } + + heartBeatFunc = ScheduledFunction({ + val heartbeat = ClientMsgProtobuf( + CMsgClientHeartBeat::class.java, + EMsg.ClientHeartBeat + ).apply { + body.sendReply = true // Ping Pong + } + send(heartbeat) + }, 5000) + } + + /** + * Connects this client to a Steam3 server. This begins the process of connecting and encrypting the data channel + * between the client and the server. Results are returned asynchronously in a [ConnectedCallback]. If the + * server that SteamKit attempts to connect to is down, a [DisconnectedCallback] will be posted instead. + * SteamKit will not attempt to reconnect to Steam, you must handle this callback and call Connect again preferably + * after a short delay. + * + * @param cmServer The [ServerRecord] of the CM server to connect to. + */ + @JvmOverloads + fun connect(cmServer: ServerRecord? = null) { + var cmServer = cmServer + synchronized(connectionLock) { + try { + disconnect(true) + + assert(connection == null) + + expectDisconnection = false + + if (cmServer == null) { + cmServer = servers.getNextServerCandidate(configuration.protocolTypes) + } + + if (cmServer == null) { + logger.error("No CM servers available to connect to") + onClientDisconnected(false) + return + } + + connection = createConnection(cmServer.protocolTypes) + connection!!.getNetMsgReceived().addEventHandler(netMsgReceived) + connection!!.getConnected().addEventHandler(connected) + connection!!.getDisconnected().addEventHandler(disconnected) + logger.debug( + "Connecting to ${cmServer.endpoint} with protocol ${cmServer.protocolTypes}, " + + "and with connection impl ${connection!!.javaClass.getSimpleName()}", + ) + connection!!.connect(cmServer.endpoint) + } catch (e: Exception) { + logger.debug("Failed to connect to Steam network", e) + onClientDisconnected(false) + } + } + } + + /** + * Disconnects this client. + */ + @JvmOverloads + fun disconnect(userInitiated: Boolean = true) { + synchronized(connectionLock) { + heartBeatFunc.stop() + connection?.disconnect(userInitiated) + } + } + + /** + * Sends the specified client message to the server. + * This method automatically assigns the correct [sessionID] and [SteamID] of the message. + * @param msg The client message to send. + */ + fun send(msg: IClientMsg) { + if (!isConnected) { + logger.error("Send(${msg.msgType}) was called while not connected to Steam.") + } + + sessionID?.let { msg.setSessionID(it) } + steamID?.let { msg.setSteamID(it) } + + try { + debugNetworkListener?.onOutgoingNetworkMessage(msg.getMsgType(), msg.serialize()) + } catch (e: Exception) { + logger.debug("DebugNetworkListener threw an exception", e) + } + + // we'll swallow any network failures here because they will be thrown later + // on the network thread, and that will lead to a disconnect callback + // down the line + connection?.send(msg.serialize()) + } + + // TODO: override fun logDebug() + + /** + * Called when a client message is received from the network. + * @param packetMsg The packet message. + */ + protected open fun onClientMsgReceived(packetMsg: IPacketMsg?): Boolean { + if (packetMsg == null) { + logger.debug("Packet message failed to parse, shutting down connection") + disconnect(userInitiated = false) + return false + } + + // Multi message gets logged down the line after it's decompressed + if (packetMsg.getMsgType() != EMsg.Multi) { + try { + debugNetworkListener?.onIncomingNetworkMessage(packetMsg.getMsgType(), packetMsg.getData()) + } catch (e: Exception) { + logger.debug("debugNetworkListener threw an exception", e) + } + } + + when (packetMsg.getMsgType()) { + EMsg.Multi -> handleMulti(packetMsg) + EMsg.ClientLogOnResponse -> handleLogOnResponse(packetMsg) // we handle this to get the SteamID/SessionID and to setup heartbeating + EMsg.ClientLoggedOff -> handleLoggedOff(packetMsg) // to stop heartbeating when we get logged off + EMsg.ClientServerUnavailable -> handleServerUnavailable(packetMsg) + EMsg.ClientSessionToken -> handleSessionToken(packetMsg) // am session token + else -> Unit + } + + return true + } + + /** + * Called when the client is securely isConnected to Steam3. + */ + protected open fun onClientConnected() { + val request = ClientMsgProtobuf( + CMsgClientHello::class.java, + EMsg.ClientHello + ).apply { + body.protocolVersion = MsgClientLogon.CurrentProtocol + } + + send(request) + } + + /** + * Called when the client is physically disconnected from Steam3. + * @param userInitiated whether the disconnect was initialized by the client + */ + protected open fun onClientDisconnected(userInitiated: Boolean) {} + + private fun createConnection(protocol: EnumSet): Connection { + val connectionFactory: IConnectionFactory = configuration.connectionFactory + val connection = connectionFactory.createConnection(configuration, protocol) + if (connection == null) { + logger.error("Connection factory returned null connection for protocols $protocol") + throw IllegalArgumentException("Connection factory returned null connection.") + } + return connection + } + + /** + * Debugging: Do not use this directly. + */ + fun receiveTestPacketMsg(packetMsg: IPacketMsg) { + onClientMsgReceived(packetMsg) + } + + /** + * Debugging: Do not use this directly. + */ + fun setIsConnected(value: Boolean) { + isConnected = value + } + + private val netMsgReceived = EventHandler { _, e -> + onClientMsgReceived(getPacketMsg(e.data)) + } + + private val connected = EventHandler { _, e -> + logger.debug("EventHandler `connected` called") + + if (connection == null) { + logger.error("No connection object after connecting.") + } + + if (connection?.currentEndPoint == null) { + logger.error("No connection endpoint after connecting - cannot update server list") + } + + servers.tryMark( + endPoint = connection?.getCurrentEndPoint(), + protocolTypes = connection?.getProtocolTypes(), + quality = ServerQuality.GOOD + ) + + isConnected = true + + try { + onClientConnected() + } catch (ex: Exception) { + logger.error("Unhandled exception after connecting: ", ex) + disconnect(userInitiated = false) + } + } + + private val disconnected = object : EventHandler { + override fun handleEvent( + sender: Any?, + e: DisconnectedEventArgs, + ) { + logger.debug( + "EventHandler `disconnected` called. User Initiated: ${e.isUserInitiated}, " + + "Expected Disconnection: $expectDisconnection" + ) + isConnected = false + + if (!e.isUserInitiated && !expectDisconnection) { + servers.tryMark( + endPoint = connection!!.currentEndPoint, + protocolTypes = connection!!.protocolTypes, + quality = ServerQuality.BAD + ) + } + + sessionID = null + steamID = null + + connection!!.getNetMsgReceived().removeEventHandler(netMsgReceived) + connection!!.getConnected().removeEventHandler(connected) + connection!!.getDisconnected().removeEventHandler(this) + connection = null // Why do we null here, but SK doesn't? + + heartBeatFunc.stop() + + onClientDisconnected(userInitiated = e.isUserInitiated || expectDisconnection) + } + } + + // region ClientMsg Handlers + + private fun handleMulti(packetMsg: IPacketMsg) { + if (!packetMsg.isProto()) { + logger.debug("HandleMulti got non-proto MsgMulti!!") + return + } + + val msgMulti = ClientMsgProtobuf(CMsgMulti::class.java, packetMsg) + + val payloadBuffer = Buffer().write(msgMulti.body.messageBody.toByteArray()).let { + if (msgMulti.body.sizeUnzipped > 0) { + (it as Source).gzip().buffer() + } else { + it + } + } + + do { + val packetSize = payloadBuffer.readIntLe() + val packetContent = payloadBuffer.readByteArray(packetSize.toLong()) + val packet = getPacketMsg(packetContent) + + if (!onClientMsgReceived(packet)) { + break + } + } while (!payloadBuffer.exhausted()) + + payloadBuffer.close() + } + + private fun handleLogOnResponse(packetMsg: IPacketMsg) { + if (!packetMsg.isProto()) { + // a non-proto ClientLogonResponse can come in as a result of connecting but never sending a ClientLogon + // in this case, it always fails, so we don't need to do anything special here + logger.debug("Got non-proto logon response, this is indicative of no logon attempt after connecting.") + return + } + + val logonResp = ClientMsgProtobuf( + CMsgClientLogonResponse::class.java, + packetMsg + ) + val logonResponse: EResult = EResult.from(logonResp.body.eresult) + val extendedResponse: EResult = EResult.from(logonResp.body.eresultExtended) + + logger.debug("handleLogOnResponse got response: $logonResponse, extended: $extendedResponse") + + // Note: Sometimes if you sign in too many times, steam may confuse "InvalidPassword" with "RateLimitExceeded" + if (logonResponse == EResult.OK) { + sessionID = logonResp.protoHeader.clientSessionid + steamID = logonResp.protoHeader.steamid.toSteamID() + + cellID = logonResp.body.cellId + publicIP = NetHelpers.getIPAddress(logonResp.body.publicIp) + ipCountryCode = logonResp.body.ipCountryCode + + val hbDelay = logonResp.body.legacyOutOfGameHeartbeatSeconds + + // restart heartbeat + heartBeatFunc.stop() + heartBeatFunc.delay = hbDelay * 1000L + heartBeatFunc.start() + } else if (logonResponse == EResult.TryAnotherCM || logonResponse == EResult.ServiceUnavailable) { + if (connection?.currentEndPoint != null) { + servers.tryMark( + endPoint = connection?.currentEndPoint, + protocolTypes = connection?.protocolTypes, + quality = ServerQuality.BAD + ) + } + } + } + + private fun handleLoggedOff(packetMsg: IPacketMsg) { + sessionID = null + steamID = null + + cellID = null + publicIP = null + ipCountryCode = null + + heartBeatFunc.stop() + + if (packetMsg.isProto()) { + val logoffMsg = ClientMsgProtobuf( + CMsgClientLoggedOff::class.java, + packetMsg + ) + val logoffResult = EResult.from(logoffMsg.body.eresult) + + logger.debug("handleLoggedOff got $logoffResult") + + if (logoffResult == EResult.TryAnotherCM || logoffResult == EResult.ServiceUnavailable) { + if (connection == null) { + logger.error("No connection object during ClientLoggedOff.") + } + + if (connection?.currentEndPoint == null) { + logger.error("No connection endpoint during ClientLoggedOff - cannot update server list status") + } + + servers.tryMark( + endPoint = connection?.currentEndPoint, + protocolTypes = connection?.protocolTypes, + quality = ServerQuality.BAD + ) + } + } else { + logger.debug("handleLoggedOff got unexpected response: ${packetMsg.getMsgType()}") + } + } + + private fun handleServerUnavailable(packetMsg: IPacketMsg) { + val msgServerUnavailable = ClientMsg(MsgClientServerUnavailable::class.java, packetMsg) + + logger.debug( + "A server of type ${msgServerUnavailable.body.eServerTypeUnavailable} " + + "was not available for request: ${EMsg.from(msgServerUnavailable.body.eMsgSent)}" + ) + + disconnect(userInitiated = false) + } + + private fun handleSessionToken(packetMsg: IPacketMsg) { + val sessToken = ClientMsgProtobuf( + CMsgClientSessionToken::class.java, + packetMsg + ) + + sessionToken = sessToken.body.token + } + + // endregion +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt index 93f65f69..5ace9369 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt @@ -26,9 +26,9 @@ abstract class ClientMsgHandler { * [DisconnectedCallback.isUserInitiated] property will be set to **true**. */ protected var isExpectDisconnection: Boolean - get() = client.isExpectDisconnection + get() = client.expectDisconnection set(expectDisconnection) { - client.isExpectDisconnection = expectDisconnection + client.expectDisconnection = expectDisconnection } /** diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt index a873f743..8d734e6f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt @@ -44,12 +44,15 @@ import java.util.concurrent.atomic.AtomicLong * * @constructor Initializes a new instance of the [SteamClient] class with a specific configuration. * @param configuration The configuration to use for this client. + * @param identifier A specific identifier to be used to uniquely identify this instance. + * @param defaultScope todo */ @Suppress("unused") class SteamClient @JvmOverloads constructor( - configuration: SteamConfiguration? = SteamConfiguration.createDefault(), + configuration: SteamConfiguration = SteamConfiguration.createDefault(), + identifier: String = UUID.randomUUID().toString().replace("-", ""), internal val defaultScope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), -) : CMClient(configuration) { +) : CMClient(configuration, identifier) { private val handlers = HashMap, ClientMsgHandler>(HANDLERS_COUNT) @@ -221,12 +224,14 @@ class SteamClient @JvmOverloads constructor( * Called when a client message is received from the network. * @param packetMsg The packet message. */ - override fun onClientMsgReceived(packetMsg: IPacketMsg): Boolean { + override fun onClientMsgReceived(packetMsg: IPacketMsg?): Boolean { // let the underlying CMClient handle this message first if (!super.onClientMsgReceived(packetMsg)) { return false } + requireNotNull(packetMsg) + // we want to handle some of the clientMsg's before we pass them along to registered handlers when (packetMsg.getMsgType()) { EMsg.JobHeartbeat -> handleJobHeartbeat(packetMsg) diff --git a/src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt b/src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt index 5f9ff54a..40a85ce3 100644 --- a/src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt +++ b/src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt @@ -42,7 +42,7 @@ fun InputStream.readNBytesCompat(len: Int): ByteArray { var n: Int do { - var buf = ByteArray(min(remaining, 8192)) + val buf = ByteArray(min(remaining, 8192)) var nread = 0 // read to EOF which may read more or less than buffer size @@ -82,7 +82,7 @@ fun InputStream.readNBytesCompat(len: Int): ByteArray { remaining = total bufs.forEach { b -> - var count = min(b.size, remaining) + val count = min(b.size, remaining) System.arraycopy(b, 0, result, offset, count) offset += count remaining -= count diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java deleted file mode 100644 index 3ed59ab0..00000000 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ /dev/null @@ -1,158 +0,0 @@ -package in.dragonbra.javasteam.util.stream; - -import in.dragonbra.javasteam.util.compat.ByteArrayOutputStreamCompat; - -import java.io.*; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -/** - * Basically DataInputStream, but the bytes are parsed in reverse order - */ -public class BinaryReader extends FilterInputStream { - - private final byte[] readBuffer = new byte[8]; - - private int position = 0; - - public BinaryReader(InputStream in) { - super(in); - } - - public int readInt() throws IOException { - int ch1 = in.read(); - int ch2 = in.read(); - int ch3 = in.read(); - int ch4 = in.read(); - position += 4; - if ((ch1 | ch2 | ch3 | ch4) < 0) { - throw new EOFException(); - } - return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1); - } - - public byte[] readBytes(int len) throws IOException { - if (len < 0) { - throw new IOException("negative length"); - } - - byte[] bytes = new byte[len]; - - for (int i = 0; i < bytes.length; i++) { - bytes[i] = readByte(); - } - - return bytes; - } - - public byte readByte() throws IOException { - int ch = in.read(); - if (ch < 0) { - throw new EOFException(); - } - position += 1; - return (byte) ch; - } - - public short readShort() throws IOException { - int ch1 = in.read(); - int ch2 = in.read(); - if ((ch1 | ch2) < 0) { - throw new EOFException(); - } - position += 2; - return (short) ((ch2 << 8) + ch1); - } - - public long readLong() throws IOException { - in.read(readBuffer, 0, 8); - position += 8; - return (((long) readBuffer[7] << 56) + - ((long) (readBuffer[6] & 255) << 48) + - ((long) (readBuffer[5] & 255) << 40) + - ((long) (readBuffer[4] & 255) << 32) + - ((long) (readBuffer[3] & 255) << 24) + - ((readBuffer[2] & 255) << 16) + - ((readBuffer[1] & 255) << 8) + - (readBuffer[0] & 255)); - } - - public char readChar() throws IOException { - int ch1 = in.read(); - if (ch1 < 0) { - throw new EOFException(); - } - position += 1; - return (char) ch1; - } - - public float readFloat() throws IOException { - return Float.intBitsToFloat(readInt()); - } - - public double readDouble() throws IOException { - return Double.longBitsToDouble(readLong()); - } - - public boolean readBoolean() throws IOException { - int ch = in.read(); - if (ch < 0) { - throw new EOFException(); - } - position += 1; - return ch != 0; - } - - public String readNullTermString() throws IOException { - return readNullTermString(StandardCharsets.UTF_8); - } - - public String readNullTermString(Charset charset) throws IOException { - if (charset == null) { - throw new IOException("charset is null"); - } - - if (charset.equals(StandardCharsets.UTF_8)) { - return readNullTermUtf8String(); - } - - ByteArrayOutputStream buffer = new ByteArrayOutputStream(0); - BinaryWriter bw = new BinaryWriter(buffer); - - while (true) { - char ch = readChar(); - - if (ch == 0) { - break; - } - - bw.writeChar(ch); - } - - byte[] bytes = buffer.toByteArray(); - position += bytes.length; - - return new String(bytes, charset); - } - - private String readNullTermUtf8String() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int b; - - while ((b = in.read()) != 0) { - if (b <= 0) { - break; - } - baos.write(b); - position++; - } - - position++; // Increment for the null terminator - - return ByteArrayOutputStreamCompat.toString(baos); - } - - public int getPosition() { - return position; - } -} diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.kt b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.kt new file mode 100644 index 00000000..e5baa7cc --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.kt @@ -0,0 +1,181 @@ +package `in`.dragonbra.javasteam.util.stream + +import `in`.dragonbra.javasteam.util.compat.ByteArrayOutputStreamCompat +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat +import java.io.ByteArrayOutputStream +import java.io.EOFException +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +/** + * Basically DataInputStream, but the bytes are parsed in reverse order + */ +class BinaryReader(inputStream: InputStream) : FilterInputStream(inputStream) { + + private val readBuffer = ByteArray(16) + + var position: Int = 0 + private set + + @Throws(IOException::class) + fun readInt(): Int { + val bytesRead = `in`.read(readBuffer, 0, 4) + + if (bytesRead != 4) { + throw EOFException() + } + + position += 4 + + return ((readBuffer[3].toInt() and 0xFF) shl 24) or + ((readBuffer[2].toInt() and 0xFF) shl 16) or + ((readBuffer[1].toInt() and 0xFF) shl 8) or + (readBuffer[0].toInt() and 0xFF) + } + + @Throws(IOException::class) + fun readBytes(len: Int): ByteArray { + if (len < 0) { + throw IOException("negative length") + } + + val bytes = `in`.readNBytesCompat(len) + + if (bytes.size != len) { + throw EOFException("Unexpected end of stream") + } + + position += len + + return bytes + } + + @Throws(IOException::class) + fun readByte(): Byte { + val ch = `in`.read() + + if (ch < 0) { + throw EOFException() + } + + position += 1 + + return ch.toByte() + } + + @Throws(IOException::class) + fun readShort(): Short { + val bytesRead = `in`.read(readBuffer, 0, 2) + if (bytesRead != 2) { + throw EOFException() + } + + position += 2 + + return (((readBuffer[1].toInt() and 0xFF) shl 8) or (readBuffer[0].toInt() and 0xFF)).toShort() + } + + @Throws(IOException::class) + fun readLong(): Long { + val bytesRead = `in`.read(readBuffer, 0, 8) + + if (bytesRead != 8) { + throw EOFException() + } + + position += 8 + + return ( + (readBuffer[7].toLong() shl 56) + + ((readBuffer[6].toInt() and 255).toLong() shl 48) + + ((readBuffer[5].toInt() and 255).toLong() shl 40) + + ((readBuffer[4].toInt() and 255).toLong() shl 32) + + ((readBuffer[3].toInt() and 255).toLong() shl 24) + + ((readBuffer[2].toInt() and 255) shl 16) + + ((readBuffer[1].toInt() and 255) shl 8) + + (readBuffer[0].toInt() and 255) + ) + } + + @Throws(IOException::class) + fun readChar(): Char { + val ch1 = `in`.read() + + if (ch1 < 0) { + throw EOFException() + } + + position += 1 + + return ch1.toChar() + } + + @Throws(IOException::class) + fun readFloat(): Float = Float.fromBits(readInt()) + + @Throws(IOException::class) + fun readDouble(): Double = Double.fromBits(readLong()) + + @Throws(IOException::class) + fun readBoolean(): Boolean { + val ch = `in`.read() + + if (ch < 0) { + throw EOFException() + } + + position += 1 + + return ch != 0 + } + + @JvmOverloads + @Throws(IOException::class) + fun readNullTermString(charset: Charset = StandardCharsets.UTF_8): String { + if (charset == StandardCharsets.UTF_8) { + return readNullTermUtf8String() + } + + val buffer = ByteArrayOutputStream(0) + val bw = BinaryWriter(buffer) + + while (true) { + val ch = readChar() + + if (ch.code == 0) { + break + } + + bw.writeChar(ch) + } + + val bytes = buffer.toByteArray() + + position += bytes.size + + return String(bytes, charset) + } + + @Throws(IOException::class) + private fun readNullTermUtf8String(): String { + val baos = ByteArrayOutputStream() + + while (true) { + val b = `in`.read() + + if (b <= 0) { + break + } + + baos.write(b) + position++ + } + + position++ // Increment for the null terminator + + return ByteArrayOutputStreamCompat.toString(baos) + } +} diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.java deleted file mode 100644 index c4d56a32..00000000 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.java +++ /dev/null @@ -1,61 +0,0 @@ -package in.dragonbra.javasteam.util.stream; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Basically DataOutputStream, but the bytes are parsed in reverse order - */ -public class BinaryWriter extends FilterOutputStream { - - private final byte[] writeBuffer = new byte[8]; - - public BinaryWriter(OutputStream out) { - super(out); - } - - public void writeInt(int v) throws IOException { - out.write(v & 0xFF); - out.write((v >>> 8) & 0xFF); - out.write((v >>> 16) & 0xFF); - out.write((v >>> 24) & 0xFF); - } - - public void writeShort(short v) throws IOException { - out.write(v & 0xFF); - out.write((v >>> 8) & 0xFF); - } - - public void writeLong(long v) throws IOException { - writeBuffer[7] = (byte) (v >>> 56); - writeBuffer[6] = (byte) (v >>> 48); - writeBuffer[5] = (byte) (v >>> 40); - writeBuffer[4] = (byte) (v >>> 32); - writeBuffer[3] = (byte) (v >>> 24); - writeBuffer[2] = (byte) (v >>> 16); - writeBuffer[1] = (byte) (v >>> 8); - writeBuffer[0] = (byte) v; - out.write(writeBuffer, 0, 8); - } - - public void writeFloat(float v) throws IOException { - writeInt(Float.floatToIntBits(v)); - } - - public void writeDouble(double v) throws IOException { - writeLong(Double.doubleToLongBits(v)); - } - - public void writeBoolean(boolean v) throws IOException { - out.write(v ? 1 : 0); - } - - public void writeByte(byte v) throws IOException { - out.write(v); - } - - public void writeChar(char v) throws IOException { - out.write(v); - } -} diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.kt b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.kt new file mode 100644 index 00000000..b28af24d --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryWriter.kt @@ -0,0 +1,64 @@ +package `in`.dragonbra.javasteam.util.stream + +import java.io.FilterOutputStream +import java.io.IOException +import java.io.OutputStream +/** + * Basically DataOutputStream, but the bytes are parsed in reverse order + */ +class BinaryWriter(out: OutputStream) : FilterOutputStream(out) { + + private val writeBuffer = ByteArray(8) + + @Throws(IOException::class) + fun writeInt(v: Int) { + out.write(v and 0xFF) + out.write((v shr 8) and 0xFF) + out.write((v shr 16) and 0xFF) + out.write((v shr 24) and 0xFF) + } + + @Throws(IOException::class) + fun writeShort(v: Short) { + out.write(v.toInt() and 0xFF) + out.write((v.toInt() shr 8) and 0xFF) + } + + @Throws(IOException::class) + fun writeLong(v: Long) { + writeBuffer[7] = (v shr 56).toByte() + writeBuffer[6] = (v shr 48).toByte() + writeBuffer[5] = (v shr 40).toByte() + writeBuffer[4] = (v shr 32).toByte() + writeBuffer[3] = (v shr 24).toByte() + writeBuffer[2] = (v shr 16).toByte() + writeBuffer[1] = (v shr 8).toByte() + writeBuffer[0] = v.toByte() + out.write(writeBuffer, 0, 8) + } + + @Throws(IOException::class) + fun writeFloat(v: Float) { + writeInt(v.toBits()) + } + + @Throws(IOException::class) + fun writeDouble(v: Double) { + writeLong(v.toBits()) + } + + @Throws(IOException::class) + fun writeBoolean(v: Boolean) { + out.write(if (v) 1 else 0) + } + + @Throws(IOException::class) + fun writeByte(v: Byte) { + out.write(v.toInt()) + } + + @Throws(IOException::class) + fun writeChar(v: Char) { + out.write(v.code) + } +} diff --git a/src/test/java/in/dragonbra/javasteam/steam/DummyClient.java b/src/test/java/in/dragonbra/javasteam/steam/DummyClient.java index 6efccce8..79f1e85e 100644 --- a/src/test/java/in/dragonbra/javasteam/steam/DummyClient.java +++ b/src/test/java/in/dragonbra/javasteam/steam/DummyClient.java @@ -9,7 +9,7 @@ */ public class DummyClient extends CMClient { public DummyClient() { - super(SteamConfiguration.createDefault()); + super(SteamConfiguration.createDefault(), "DummyClient"); } public void dummyDisconnect() {