diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_020_friends/SampleFriends.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_020_friends/SampleFriends.java index 7b9b2e74..136e827f 100644 --- a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_020_friends/SampleFriends.java +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_020_friends/SampleFriends.java @@ -122,7 +122,16 @@ private void onConnected(ConnectedCallback callback) { private void onDisconnected(DisconnectedCallback callback) { System.out.println("Disconnected from Steam"); - isRunning = false; + 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) { diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_022_pics/SamplePics.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_022_pics/SamplePics.java new file mode 100644 index 00000000..8a544e02 --- /dev/null +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_022_pics/SamplePics.java @@ -0,0 +1,226 @@ +package in.dragonbra.javasteamsamples._022_pics; + +import in.dragonbra.javasteam.enums.EResult; +import in.dragonbra.javasteam.steam.authentication.*; +import in.dragonbra.javasteam.steam.handlers.steamapps.PICSRequest; +import in.dragonbra.javasteam.steam.handlers.steamapps.SteamApps; +import in.dragonbra.javasteam.steam.handlers.steamapps.callback.PICSChangesCallback; +import in.dragonbra.javasteam.steam.handlers.steamapps.callback.PICSProductInfoCallback; +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.types.KeyValue; +import in.dragonbra.javasteam.util.log.DefaultLogListener; +import in.dragonbra.javasteam.util.log.LogManager; + +import java.util.concurrent.CancellationException; + +@SuppressWarnings("FieldCanBeLocal") +public class SamplePics implements Runnable { + + private SteamClient steamClient; + + private CallbackManager manager; + + private SteamApps steamApps; + + private SteamUser steamUser; + + private boolean isRunning; + + private final String user; + + private final String pass; + + private String previouslyStoredGuardData; // For the sake of this sample, we do not persist guard data + + public SamplePics(String user, String pass) { + this.user = user; + this.pass = pass; + } + + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Sample1: No username and password specified!"); + return; + } + + LogManager.addListener(new DefaultLogListener()); + + new SamplePics(args[0], args[1]).run(); + } + + @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.WEB_SOCKET); + // }); + // // create our steamclient instance with custom configuration. + // steamClient = new SteamClient(configuration); + + // create our steamclient instance using default configuration + steamClient = new SteamClient(); + + // 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); + + // get the steamuser handler, which is used for interacting with apps and packages. + steamApps = steamClient.getHandler(SteamApps.class); + + // 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 + manager.subscribe(ConnectedCallback.class, this::onConnected); + manager.subscribe(DisconnectedCallback.class, this::onDisconnected); + + manager.subscribe(LoggedOnCallback.class, this::onLoggedOn); + manager.subscribe(LoggedOffCallback.class, this::onLoggedOff); + + manager.subscribe(PICSChangesCallback.class, this::onPicsChanges); + manager.subscribe(PICSProductInfoCallback.class, this::onPicsProduct); + + 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); + } + } + + @SuppressWarnings("DanglingJavadoc") + private void onConnected(ConnectedCallback callback) { + System.out.println("Connected to Steam! Logging in " + user + "..."); + + var shouldRememberPassword = false; + + AuthSessionDetails authDetails = new AuthSessionDetails(); + authDetails.username = user; + authDetails.password = pass; + authDetails.persistentSession = shouldRememberPassword; + + // See NewGuardData comment below. + authDetails.guardData = previouslyStoredGuardData; + + /** + * {@link UserConsoleAuthenticator} is the default authenticator implementation provided by JavaSteam + * for ease of use which blocks the thread and asks for user input to enter the code. + * However, if you require special handling (e.g. you have the TOTP secret and can generate codes on the fly), + * you can implement your own {@link IAuthenticator}. + */ + authDetails.authenticator = new UserConsoleAuthenticator(); + + try { + // Begin authenticating via credentials. + var authSession = steamClient.getAuthentication().beginAuthSessionViaCredentials(authDetails); + + // Note: This is blocking, it would be up to you to make it non-blocking for Java. + // Note: Kotlin uses should use ".pollingWaitForResult()" as its a suspending function. + AuthPollResult pollResponse = authSession.pollingWaitForResultCompat().get(); + + if (pollResponse.getNewGuardData() != null) { + // When using certain two factor methods (such as email 2fa), guard data may be provided by Steam + // for use in future authentication sessions to avoid triggering 2FA again (this works similarly to the old sentry file system). + // Do note that this guard data is also a JWT token and has an expiration date. + previouslyStoredGuardData = pollResponse.getNewGuardData(); + } + + // Logon to Steam with the access token we have received + // Note that we are using RefreshToken for logging on here + LogOnDetails details = new LogOnDetails(); + details.setUsername(pollResponse.getAccountName()); + details.setAccessToken(pollResponse.getRefreshToken()); + + // 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); + } catch (Exception e) { + // List a couple of exceptions that could be important to handle. + if (e instanceof AuthenticationException) { + System.err.println("An Authentication error has occurred. " + e.getMessage()); + } else if (e instanceof CancellationException) { + System.err.println("An Cancellation exception was raised. Usually means a timeout occurred. " + e.getMessage()); + } else { + System.err.println("An error occurred:" + e.getMessage()); + } + + steamUser.logOff(); + } + } + + 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 + steamApps.picsGetProductInfo(new PICSRequest(553850), null); // We'll use HellDivers 2 since it has "tm" in the title + + // steamUser.logOff(); + } + + private void onLoggedOff(LoggedOffCallback callback) { + System.out.println("Logged off of Steam: " + callback.getResult()); + + isRunning = false; + } + + private void onPicsChanges(PICSChangesCallback callback) { + System.out.println("TODO"); + } + + private void onPicsProduct(PICSProductInfoCallback callback) { + System.out.println("[PICSProductInfoCallback] apps: "); + for (var app : callback.getApps().entrySet()) { + printKeyValue(app.getValue().getKeyValues(), 1); + } + } + + private static void printKeyValue(KeyValue keyvalue, int depth) { + if (keyvalue.getChildren().isEmpty()) + System.out.println("[PICSProductInfoCallback] " + " ".repeat(depth * 4) + " " + keyvalue.getName() + ": " + keyvalue.getValue()); + else { + System.out.println("[PICSProductInfoCallback] " + " ".repeat(depth * 4) + " " + keyvalue.getName() + ":"); + for (KeyValue child : keyvalue.getChildren()) + printKeyValue(child, depth + 1); + } + } +} diff --git a/src/main/java/in/dragonbra/javasteam/base/AbstractMsgBase.java b/src/main/java/in/dragonbra/javasteam/base/AbstractMsgBase.java index b17a7528..69a8c957 100644 --- a/src/main/java/in/dragonbra/javasteam/base/AbstractMsgBase.java +++ b/src/main/java/in/dragonbra/javasteam/base/AbstractMsgBase.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * This class provides a payload backing to client messages. @@ -107,7 +108,7 @@ public double readDouble() throws IOException { } public void writeString(String data) throws IOException { - writeString(data, Charset.defaultCharset()); + writeString(data, StandardCharsets.UTF_8); } public void writeString(String data, Charset charset) throws IOException { @@ -123,7 +124,7 @@ public void writeString(String data, Charset charset) throws IOException { } public void writeNullTermString(String data) throws IOException { - writeNullTermString(data, Charset.defaultCharset()); + writeNullTermString(data, StandardCharsets.UTF_8); } public void writeNullTermString(String data, Charset charset) throws IOException { @@ -132,7 +133,7 @@ public void writeNullTermString(String data, Charset charset) throws IOException } public String readNullTermString() throws IOException { - return readNullTermString(Charset.defaultCharset()); + return readNullTermString(StandardCharsets.UTF_8); } public String readNullTermString(Charset charset) throws IOException { diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/TcpConnection.java b/src/main/java/in/dragonbra/javasteam/networking/steam3/TcpConnection.java index e2a5fe8c..05fd3a1d 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/TcpConnection.java +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/TcpConnection.java @@ -1,5 +1,6 @@ package in.dragonbra.javasteam.networking.steam3; +import in.dragonbra.javasteam.util.NetHelpers; import in.dragonbra.javasteam.util.log.LogManager; import in.dragonbra.javasteam.util.log.Logger; import in.dragonbra.javasteam.util.stream.BinaryReader; @@ -173,7 +174,12 @@ public InetAddress getLocalIP() { return null; } - return socket.getLocalAddress(); + try { + return NetHelpers.getLocalIP(socket); + } catch (Exception e) { + logger.debug("Socket exception trying to read bound IP: ", e); + return null; + } } } diff --git a/src/main/java/in/dragonbra/javasteam/networking/steam3/UdpConnection.java b/src/main/java/in/dragonbra/javasteam/networking/steam3/UdpConnection.java index c1ed7138..fbdb1698 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/UdpConnection.java +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/UdpConnection.java @@ -3,6 +3,7 @@ import in.dragonbra.javasteam.enums.EUdpPacketType; import in.dragonbra.javasteam.generated.ChallengeData; import in.dragonbra.javasteam.generated.ConnectData; +import in.dragonbra.javasteam.util.NetHelpers; import in.dragonbra.javasteam.util.log.LogManager; import in.dragonbra.javasteam.util.log.Logger; import in.dragonbra.javasteam.util.stream.MemoryStream; @@ -161,7 +162,7 @@ public void send(byte[] data) { @Override public InetAddress getLocalIP() { - return sock.getLocalAddress(); + return NetHelpers.getLocalIP(sock); } @Override 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 37929ef6..9dce1967 100644 --- a/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt +++ b/src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketConnection.kt @@ -54,8 +54,7 @@ class WebSocketConnection : } } - // TODO get local ip? SK uses `IPAddress.None` here. - override fun getLocalIP(): InetAddress? = null + override fun getLocalIP(): InetAddress? = InetAddress.getByAddress(byteArrayOf(0, 0, 0, 0)) override fun getCurrentEndPoint(): InetSocketAddress? = socketEndPoint @@ -88,7 +87,10 @@ class WebSocketConnection : } override fun onClosing(code: Int, reason: String) { - logger.debug("Closing connection") + logger.debug("Closing connection: $code, reason: ${reason.ifEmpty { "No reason given" }}") + // Steam can close a connection if there is nothing else it wants to send. + // For example: AccountLoginDeniedNeedTwoFactor, InvalidPassword, etc. + disconnectCore(code == 1000) } override fun onError(t: Throwable) { diff --git a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java index e7ca2707..4005701e 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java +++ b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java @@ -386,6 +386,9 @@ private void handleLogOnResponse(IPacketMsg packetMsg) { ClientMsgProtobuf logonResp = new ClientMsgProtobuf<>(CMsgClientLogonResponse.class, packetMsg); EResult logonResponse = EResult.from(logonResp.getBody().getEresult()); + logger.debug("handleLogOnResponse got response: " + logonResponse); + + // Note: Sometimes if you sign in too many times, steam may confuse "InvalidPassword" with "RateLimitExceeded" if (logonResponse == EResult.OK) { sessionID = logonResp.getProtoHeader().getClientSessionid(); @@ -414,9 +417,13 @@ private void handleLoggedOff(IPacketMsg packetMsg) { 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) { getServers().tryMark(connection.getCurrentEndPoint(), connection.getProtocolTypes(), ServerQuality.BAD); } + } else { + logger.debug("handleLoggedOff got unexpected response: " + packetMsg.getMsgType()); } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/AccessTokenGenerateResult.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/AccessTokenGenerateResult.kt index 0ff3beb0..b1a81bda 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/AccessTokenGenerateResult.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/AccessTokenGenerateResult.kt @@ -1,14 +1,12 @@ package `in`.dragonbra.javasteam.steam.authentication -import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.CAuthentication_AccessToken_GenerateForApp_Response import `in`.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails /** * Represents access token generation result. */ -class AccessTokenGenerateResult( - response: SteammessagesAuthSteamclient.CAuthentication_AccessToken_GenerateForApp_Response.Builder, -) { +class AccessTokenGenerateResult(response: CAuthentication_AccessToken_GenerateForApp_Response.Builder) { /** * New refresh token. diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSession.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSession.kt index 22f5e490..f0523f0a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSession.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSession.kt @@ -2,7 +2,10 @@ package `in`.dragonbra.javasteam.steam.authentication import com.google.protobuf.ByteString import `in`.dragonbra.javasteam.enums.EResult -import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.CAuthentication_AllowedConfirmation +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.CAuthentication_PollAuthSessionStatus_Request +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.CAuthentication_PollAuthSessionStatus_Response +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.EAuthSessionGuardType import `in`.dragonbra.javasteam.rpc.service.Authentication import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -11,21 +14,13 @@ import kotlinx.coroutines.future.await import kotlinx.coroutines.future.future import java.util.concurrent.CompletableFuture -//region Aliases -typealias AllowedConfirmations = SteammessagesAuthSteamclient.CAuthentication_AllowedConfirmation -typealias AuthSessionStatusRequest = SteammessagesAuthSteamclient.CAuthentication_PollAuthSessionStatus_Request -typealias AuthSessionStatusResponse = SteammessagesAuthSteamclient.CAuthentication_PollAuthSessionStatus_Response -typealias AuthSessionStatusResponseBuilder = SteammessagesAuthSteamclient.CAuthentication_PollAuthSessionStatus_Response.Builder -typealias SessionGuardType = SteammessagesAuthSteamclient.EAuthSessionGuardType -//endregion - /** * Represents an authentication session which can be used to finish the authentication and get access tokens. * * @param authentication Unified messages class for Authentication related messages, see [Authentication]. * @param authenticator Authenticator object which will be used to handle 2-factor authentication if necessary. - * @param clientId Unique identifier of requestor, also used for routing, portion of QR code. - * @param requestId Unique request ID to be presented by requestor at poll time. + * @param clientID Unique identifier of requestor, also used for routing, portion of QR code. + * @param requestID Unique request ID to be presented by requestor at poll time. * @param allowedConfirmations Confirmation types that will be able to confirm the request. * @param pollingInterval Refresh interval with which requestor should call PollAuthSessionStatus. */ @@ -33,9 +28,9 @@ typealias SessionGuardType = SteammessagesAuthSteamclient.EAuthSessionGuardType open class AuthSession( val authentication: SteamAuthentication, val authenticator: IAuthenticator?, - var clientId: Long, // Should be 'private set' - val requestId: ByteArray, - var allowedConfirmations: List, + var clientID: Long, // Should be 'private set' + val requestID: ByteArray, + var allowedConfirmations: List, val pollingInterval: Float, ) { @@ -67,14 +62,14 @@ open class AuthSession( var preferredConfirmation = allowedConfirmations.firstOrNull() ?: throw AuthenticationException("There are no allowed confirmations") - if (preferredConfirmation.confirmationType == SessionGuardType.k_EAuthSessionGuardType_Unknown) { + if (preferredConfirmation.confirmationType == EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown) { throw AuthenticationException("There are no allowed confirmations") } // If an authenticator is provided and the device confirmation is available, allow consumers to choose whether they want to // simply poll until confirmation is accepted, or whether they want to fall back to the next preferred confirmation type. authenticator?.let { auth -> - if (preferredConfirmation.confirmationType == SessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation) { + if (preferredConfirmation.confirmationType == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation) { val prefersToPollForConfirmation = auth.acceptDeviceConfirmation().await() if (!prefersToPollForConfirmation) { @@ -92,14 +87,15 @@ open class AuthSession( var pollLoop = false when (preferredConfirmation.confirmationType) { - SessionGuardType.k_EAuthSessionGuardType_None -> Unit // // No steam guard - SessionGuardType.k_EAuthSessionGuardType_EmailCode, - SessionGuardType.k_EAuthSessionGuardType_DeviceCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_None -> Unit // // No steam guard + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode, -> { // 2-factor code from the authenticator app or sent to an email handleCodeAuth(preferredConfirmation) } - SessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation -> { + + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation -> { // This is a prompt that appears in the Steam mobile app pollLoop = true } @@ -118,7 +114,7 @@ open class AuthSession( } @Throws(AuthenticationException::class) - private suspend fun handleCodeAuth(preferredConfirmation: AllowedConfirmations) { + private suspend fun handleCodeAuth(preferredConfirmation: CAuthentication_AllowedConfirmation) { val credentialsAuthSession = this as? CredentialsAuthSession ?: throw AuthenticationException( "Got ${preferredConfirmation.confirmationType} confirmation type in a session " + @@ -132,8 +128,8 @@ open class AuthSession( } val expectedInvalidCodeResult = when (preferredConfirmation.confirmationType) { - SessionGuardType.k_EAuthSessionGuardType_EmailCode -> EResult.InvalidLoginAuthCode - SessionGuardType.k_EAuthSessionGuardType_DeviceCode -> EResult.TwoFactorCodeMismatch + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode -> EResult.InvalidLoginAuthCode + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode -> EResult.TwoFactorCodeMismatch else -> throw AuthenticationException("\'${preferredConfirmation.confirmationType}\' not implemented") } @@ -143,13 +139,15 @@ open class AuthSession( while (waitingForValidCode) { try { val task = when (preferredConfirmation.confirmationType) { - SessionGuardType.k_EAuthSessionGuardType_EmailCode -> { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode -> { val msg = preferredConfirmation.associatedMessage authenticator.getEmailCode(msg, previousCodeWasIncorrect).await() } - SessionGuardType.k_EAuthSessionGuardType_DeviceCode -> { + + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode -> { authenticator.getDeviceCode(previousCodeWasIncorrect).await() } + else -> throw AuthenticationException() } @@ -183,9 +181,10 @@ open class AuthSession( */ @Throws(AuthenticationException::class) fun pollAuthSessionStatus(): AuthPollResult? { - val request = AuthSessionStatusRequest.newBuilder() - request.clientId = clientId - request.requestId = ByteString.copyFrom(requestId) + val request = CAuthentication_PollAuthSessionStatus_Request.newBuilder().apply { + clientId = clientID + requestId = ByteString.copyFrom(requestID) + } val message = authentication.authenticationService.pollAuthSessionStatus(request.build()).runBlock() @@ -194,11 +193,11 @@ open class AuthSession( throw AuthenticationException("Failed to poll status", message.result) } - val response: AuthSessionStatusResponseBuilder = - message.getDeserializedResponse(AuthSessionStatusResponse::class.java) + val response: CAuthentication_PollAuthSessionStatus_Response.Builder = + message.getDeserializedResponse(CAuthentication_PollAuthSessionStatus_Response::class.java) if (response.newClientId > 0) { - clientId = response.newClientId + clientID = response.newClientId } handlePollAuthSessionStatusResponse(response) @@ -210,9 +209,9 @@ open class AuthSession( return null } - internal open fun handlePollAuthSessionStatusResponse(response: AuthSessionStatusResponseBuilder) { + internal open fun handlePollAuthSessionStatusResponse(response: CAuthentication_PollAuthSessionStatus_Response.Builder) { if (response.newClientId != 0L) { - clientId = response.newClientId + clientID = response.newClientId } } @@ -221,15 +220,15 @@ open class AuthSession( * @param confirmations the list of confirmations * @return a sorted list of confirmations */ - private fun sortConfirmations(confirmations: List): List { + private fun sortConfirmations(confirmations: List): List { val preferredConfirmationTypes = arrayOf( - SessionGuardType.k_EAuthSessionGuardType_None, - SessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation, - SessionGuardType.k_EAuthSessionGuardType_DeviceCode, - SessionGuardType.k_EAuthSessionGuardType_EmailCode, - SessionGuardType.k_EAuthSessionGuardType_EmailConfirmation, - SessionGuardType.k_EAuthSessionGuardType_MachineToken, - SessionGuardType.k_EAuthSessionGuardType_Unknown + EAuthSessionGuardType.k_EAuthSessionGuardType_None, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken, + EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown ) val sortOrder = preferredConfirmationTypes.withIndex().associate { (index, value) -> value to index } diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSessionDetails.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSessionDetails.kt index 19773942..8c27cac6 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSessionDetails.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/AuthSessionDetails.kt @@ -1,7 +1,7 @@ package `in`.dragonbra.javasteam.steam.authentication import `in`.dragonbra.javasteam.enums.EOSType -import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesAuthSteamclient.EAuthTokenPlatformType import `in`.dragonbra.javasteam.util.Utils /** @@ -29,8 +29,7 @@ class AuthSessionDetails { * Gets or sets the platform type that the login will be performed for. */ @JvmField - var platformType: SteammessagesAuthSteamclient.EAuthTokenPlatformType = - SteammessagesAuthSteamclient.EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient + var platformType: EAuthTokenPlatformType = EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient /** * Gets or Sets the client operating system type. diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/CredentialsAuthSession.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/CredentialsAuthSession.kt index 0b53875a..f89b3cc8 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/CredentialsAuthSession.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/CredentialsAuthSession.kt @@ -17,8 +17,8 @@ class CredentialsAuthSession( ) : AuthSession( authentication = authentication, authenticator = authenticator, - clientId = response.clientId, - requestId = response.requestId.toByteArray(), + clientID = response.clientId, + requestID = response.requestId.toByteArray(), allowedConfirmations = response.allowedConfirmationsList, pollingInterval = response.interval ) { @@ -35,7 +35,7 @@ class CredentialsAuthSession( @Throws(AuthenticationException::class) fun sendSteamGuardCode(code: String?, codeType: EAuthSessionGuardType?) { val request = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request.newBuilder().apply { - this.clientId = this@CredentialsAuthSession.clientId // Could rename this to clientID instead. + this.clientId = clientID this.steamid = steamID.convertToUInt64() this.code = code this.codeType = codeType diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/QrAuthSession.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/QrAuthSession.kt index 4d584179..8010eafc 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/QrAuthSession.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/QrAuthSession.kt @@ -13,8 +13,8 @@ class QrAuthSession( ) : AuthSession( authentication = authentication, authenticator = authenticator, - clientId = response.clientId, - requestId = response.requestId.toByteArray(), + clientID = response.clientId, + requestID = response.requestId.toByteArray(), allowedConfirmations = response.allowedConfirmationsList, pollingInterval = response.interval ) { diff --git a/src/main/java/in/dragonbra/javasteam/steam/authentication/SteamAuthentication.kt b/src/main/java/in/dragonbra/javasteam/steam/authentication/SteamAuthentication.kt index e86320be..066fed01 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/authentication/SteamAuthentication.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/authentication/SteamAuthentication.kt @@ -53,7 +53,7 @@ class SteamAuthentication(private val steamClient: SteamClient) { * @throws AuthenticationException if getting the public key failed. */ @Throws(AuthenticationException::class) - private fun getPasswordRSAPublicKey(accountName: String?): CAuthentication_GetPasswordRSAPublicKey_Response.Builder { + private fun getPasswordRSAPublicKey(accountName: String): CAuthentication_GetPasswordRSAPublicKey_Response.Builder { val request = CAuthentication_GetPasswordRSAPublicKey_Request.newBuilder().apply { this.accountName = accountName } @@ -145,11 +145,7 @@ class SteamAuthentication(private val steamClient: SteamClient) { * @return [CredentialsAuthSession] */ @Throws(AuthenticationException::class) - fun beginAuthSessionViaCredentials(authSessionDetails: AuthSessionDetails?): CredentialsAuthSession { - if (authSessionDetails == null) { - throw IllegalArgumentException("authSessionDetails is null") - } - + fun beginAuthSessionViaCredentials(authSessionDetails: AuthSessionDetails): CredentialsAuthSession { if (authSessionDetails.username.isNullOrEmpty() || authSessionDetails.password.isNullOrEmpty()) { throw IllegalArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in authSessionDetails." @@ -161,7 +157,7 @@ class SteamAuthentication(private val steamClient: SteamClient) { } // Encrypt the password - val passwordRSAPublicKey = getPasswordRSAPublicKey(authSessionDetails.username) + val passwordRSAPublicKey = getPasswordRSAPublicKey(authSessionDetails.username!!) val publicModulus = BigInteger(passwordRSAPublicKey.publickeyMod, 16) val publicExponent = BigInteger(passwordRSAPublicKey.publickeyExp, 16) 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 5b4d68e5..93f65f69 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/ClientMsgHandler.kt @@ -27,7 +27,7 @@ abstract class ClientMsgHandler { */ protected var isExpectDisconnection: Boolean get() = client.isExpectDisconnection - protected set(expectDisconnection) { + set(expectDisconnection) { client.isExpectDisconnection = expectDisconnection } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/PICSProductInfo.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/PICSProductInfo.kt index 53e3696f..42287a37 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/PICSProductInfo.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/PICSProductInfo.kt @@ -4,13 +4,12 @@ import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverA import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.KeyValue import `in`.dragonbra.javasteam.util.Strings +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.BinaryReader import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.io.ByteArrayInputStream import java.io.IOException import java.net.URI -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets import java.util.* /** @@ -19,6 +18,10 @@ import java.util.* @Suppress("MemberVisibilityCanBePrivate", "unused") class PICSProductInfo : CallbackMsg { + companion object { + private val logger = LogManager.getLogger(PICSProductInfo::class.java) + } + /** * Gets the ID of the app or package. */ @@ -89,16 +92,12 @@ class PICSProductInfo : CallbackMsg { if (appInfo.hasBuffer() && !appInfo.buffer.isEmpty) { try { - // get the buffer as a string using the jvm's default charset. - // note: IDK why, but we have to encode this using the default charset - val bufferString = appInfo.buffer.toString(Charset.defaultCharset()) - // get the buffer as a byte array using utf-8 as a supported charset - val byteBuffer = bufferString.toByteArray(StandardCharsets.UTF_8) // we don't want to read the trailing null byte - val ms = MemoryStream(byteBuffer, 0, byteBuffer.size - 1) - keyValues.readAsText(ms) + MemoryStream(appInfo.buffer.toByteArray(), 0, appInfo.buffer.size() - 1).use { ms -> + keyValues.readAsText(ms) + } } catch (e: IOException) { - throw IllegalArgumentException("failed to read buffer", e) + logger.error("failed to read buffer", e) } } @@ -118,15 +117,16 @@ class PICSProductInfo : CallbackMsg { if (packageInfo.hasBuffer()) { // we don't want to read the trailing null byte try { + // TODO memory stream like SK? BinaryReader(ByteArrayInputStream(packageInfo.buffer.toByteArray())).use { br -> // steamclient checks this value == 1 before it attempts to read the KV from the buffer // see: CPackageInfo::UpdateFromBuffer(CSHA const&,uint,CUtlBuffer &) - // todo: we've apparently ignored this with zero ill effects, but perhaps we want to respect it? + // todo: (SK) we've apparently ignored this with zero ill effects, but perhaps we want to respect it? br.readInt() keyValues.tryReadAsBinary(br) } } catch (e: IOException) { - throw IllegalArgumentException("failed to read buffer", e) + logger.error("failed to read buffer", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/SteamApps.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/SteamApps.kt index 87c804ca..d3b15272 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/SteamApps.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/SteamApps.kt @@ -51,9 +51,9 @@ class SteamApps : ClientMsgHandler() { CMsgClientGetAppOwnershipTicket::class.java, EMsg.ClientGetAppOwnershipTicket ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() - body.setAppId(appId) + body.appId = appId } client.send(request) @@ -74,10 +74,10 @@ class SteamApps : ClientMsgHandler() { CMsgClientGetDepotDecryptionKey::class.java, EMsg.ClientGetDepotDecryptionKey ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() - body.setDepotId(depotId) - body.setAppId(appId) + body.depotId = depotId + body.appId = appId } client.send(request) @@ -95,8 +95,8 @@ class SteamApps : ClientMsgHandler() { */ @JvmOverloads fun picsGetAccessTokens(app: Int? = null, `package`: Int? = null): AsyncJobSingle { - val apps = mutableListOf().apply { app?.let { add(it) } } - val packages = mutableListOf().apply { `package`?.let { add(it) } } + val apps = listOfNotNull(app) + val packages = listOfNotNull(`package`) return picsGetAccessTokens(apps, packages) } @@ -114,7 +114,7 @@ class SteamApps : ClientMsgHandler() { CMsgClientPICSAccessTokenRequest::class.java, EMsg.ClientPICSAccessTokenRequest ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() body.addAllAppids(appIds) body.addAllPackageids(packageIds) @@ -144,11 +144,11 @@ class SteamApps : ClientMsgHandler() { CMsgClientPICSChangesSinceRequest::class.java, EMsg.ClientPICSChangesSinceRequest ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() - body.setSinceChangeNumber(lastChangeNumber) - body.setSendAppInfoChanges(sendAppChangeList) - body.setSendPackageInfoChanges(sendPackageChangelist) + body.sinceChangeNumber = lastChangeNumber + body.sendAppInfoChanges = sendAppChangeList + body.sendPackageInfoChanges = sendPackageChangelist } client.send(request) @@ -171,8 +171,8 @@ class SteamApps : ClientMsgHandler() { `package`: PICSRequest? = null, metaDataOnly: Boolean = false, ): AsyncJobMultiple { - val apps = mutableListOf().apply { app?.let { add(it) } } - val packages = mutableListOf().apply { `package`?.let { add(it) } } + val apps = listOfNotNull(app) + val packages = listOfNotNull(`package`) return picsGetProductInfo(apps, packages, metaDataOnly) } @@ -196,28 +196,28 @@ class SteamApps : ClientMsgHandler() { CMsgClientPICSProductInfoRequest::class.java, EMsg.ClientPICSProductInfoRequest ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() apps.forEach { appRequest -> - val appInfo = CMsgClientPICSProductInfoRequest.AppInfo.newBuilder() - - appInfo.setAccessToken(appRequest.accessToken) - appInfo.setAppid(appRequest.id) - appInfo.setOnlyPublicObsolete(false) + val appInfo = CMsgClientPICSProductInfoRequest.AppInfo.newBuilder().apply { + accessToken = appRequest.accessToken + appid = appRequest.id + onlyPublicObsolete = false + } body.addApps(appInfo) } packages.forEach { packageRequest -> - val packageInfo = CMsgClientPICSProductInfoRequest.PackageInfo.newBuilder() - - packageInfo.setAccessToken(packageRequest.accessToken) - packageInfo.setPackageid(packageRequest.id) + val packageInfo = CMsgClientPICSProductInfoRequest.PackageInfo.newBuilder().apply { + accessToken = packageRequest.accessToken + packageid = packageRequest.id + } body.addPackages(packageInfo) } - body.setMetaDataOnly(metaDataOnly) + body.metaDataOnly = metaDataOnly } client.send(request) @@ -239,11 +239,11 @@ class SteamApps : ClientMsgHandler() { CMsgClientGetCDNAuthToken::class.java, EMsg.ClientGetCDNAuthToken ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() - body.setAppId(app) - body.setDepotId(depot) - body.setHostName(hostName) + body.appId = app + body.depotId = depot + body.hostName = hostName } client.send(request) @@ -272,7 +272,7 @@ class SteamApps : ClientMsgHandler() { CMsgClientRequestFreeLicense::class.java, EMsg.ClientRequestFreeLicense ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() body.addAllAppids(apps) } @@ -295,10 +295,10 @@ class SteamApps : ClientMsgHandler() { CMsgClientCheckAppBetaPassword::class.java, EMsg.ClientCheckAppBetaPassword ).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = client.getNextJobID() - body.setAppId(app) - body.setBetapassword(password) + body.appId = app + body.betapassword = password } client.send(request) @@ -314,7 +314,7 @@ class SteamApps : ClientMsgHandler() { */ fun getLegacyGameKey(appId: Int): AsyncJobSingle { val request = ClientMsg(MsgClientGetLegacyGameKey::class.java).apply { - setSourceJobID(client.getNextJobID()) + sourceJobID = (client.getNextJobID()) body.appId = appId } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/GuestPassListCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/GuestPassListCallback.kt index 26733181..0fcfec5f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/GuestPassListCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/GuestPassListCallback.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.generated.MsgClientUpdateGuestPassesList import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.KeyValue +import `in`.dragonbra.javasteam.util.log.LogManager import java.io.IOException /** @@ -13,6 +14,10 @@ import java.io.IOException */ class GuestPassListCallback(packetMsg: IPacketMsg) : CallbackMsg() { + companion object { + private val logger = LogManager.getLogger(GuestPassListCallback::class.java) + } + /** * Gets the result of the operation. */ @@ -49,7 +54,7 @@ class GuestPassListCallback(packetMsg: IPacketMsg) : CallbackMsg() { tempList.add(kv) } } catch (e: IOException) { - throw IllegalArgumentException("failed to read guest passes", e) + logger.error("failed to read guest passes", e) } guestPasses = tempList.toList() diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/LegacyGameKeyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/LegacyGameKeyCallback.kt index e3ea1a01..dfd44a26 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/LegacyGameKeyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/LegacyGameKeyCallback.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.generated.MsgClientGetLegacyGameKeyResponse import `in`.dragonbra.javasteam.steam.handlers.steamapps.SteamApps import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import java.nio.charset.StandardCharsets /** * This callback is received in response to calling [SteamApps.getLegacyGameKey]. @@ -38,7 +39,7 @@ class LegacyGameKeyCallback(packetMsg: IPacketMsg) : CallbackMsg() { if (msg.length > 0) { val length: Int = msg.length - 1 val payload = keyResponse.payload.toByteArray() - key = String(payload, 0, length) + key = String(payload, 0, length, StandardCharsets.US_ASCII) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PICSProductInfoCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PICSProductInfoCallback.kt index cf888a84..ab92651f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PICSProductInfoCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PICSProductInfoCallback.kt @@ -54,8 +54,9 @@ class PICSProductInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { isMetaDataOnly = msg.metaDataOnly isResponsePending = msg.responsePending - unknownPackages = Collections.unmodifiableList(msg.unknownPackageidsList) - unknownApps = Collections.unmodifiableList(msg.unknownAppidsList) + + unknownPackages = msg.unknownPackageidsList + unknownApps = msg.unknownAppidsList apps = msg.appsList.associate { it.appid to PICSProductInfo(msg, it) } packages = msg.packagesList.associate { it.packageid to PICSProductInfo(it) } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PurchaseResponseCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PurchaseResponseCallback.kt index 1b95344a..3390a66c 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PurchaseResponseCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/PurchaseResponseCallback.kt @@ -7,8 +7,8 @@ import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserver2.CMsgClientPurchaseResponse import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.KeyValue +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.MemoryStream -import java.io.IOException /** * This callback is received in a response to activating a Steam key. @@ -16,6 +16,10 @@ import java.io.IOException @Suppress("MemberVisibilityCanBePrivate") class PurchaseResponseCallback(packetMsg: IPacketMsg) : CallbackMsg() { + companion object { + private val logger = LogManager.getLogger(PurchaseResponseCallback::class.java) + } + /** * Gets Result of the operation */ @@ -47,8 +51,8 @@ class PurchaseResponseCallback(packetMsg: IPacketMsg) : CallbackMsg() { try { val ms = MemoryStream(msg.purchaseReceiptInfo.toByteArray()) purchaseReceiptInfo.tryReadAsBinary(ms) - } catch (exception: IOException) { - throw IllegalArgumentException("input stream is null") + } catch (e: Exception) { + logger.error("Failed to read purchase receipt info", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/VACStatusCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/VACStatusCallback.kt index 349ff59d..7ee500c0 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/VACStatusCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamapps/callback/VACStatusCallback.kt @@ -4,9 +4,9 @@ import `in`.dragonbra.javasteam.base.ClientMsg import `in`.dragonbra.javasteam.base.IPacketMsg import `in`.dragonbra.javasteam.generated.MsgClientVACBanStatus import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.BinaryReader import java.io.ByteArrayInputStream -import java.io.IOException import java.util.* /** @@ -14,6 +14,10 @@ import java.util.* */ class VACStatusCallback(packetMsg: IPacketMsg) : CallbackMsg() { + companion object { + private val logger = LogManager.getLogger(VACStatusCallback::class.java) + } + /** * Gets a list of VAC banned apps the client is banned from. */ @@ -31,8 +35,8 @@ class VACStatusCallback(packetMsg: IPacketMsg) : CallbackMsg() { tempList.add(br.readInt()) } } - } catch (e: IOException) { - throw IllegalArgumentException("failed to read bans", e) + } catch (e: Exception) { + logger.error("failed to read bans", e) } bannedApps = tempList.toList() diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamcloud/SteamCloud.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamcloud/SteamCloud.kt index 4e8efa19..7f672918 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamcloud/SteamCloud.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamcloud/SteamCloud.kt @@ -30,10 +30,11 @@ class SteamCloud : ClientMsgHandler() { val request = ClientMsgProtobuf( CMsgClientUFSGetUGCDetails::class.java, EMsg.ClientUFSGetUGCDetails - ) - request.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - request.body.setHcontent(ugcId.value) + body.hcontent = ugcId.value + } client.send(request) @@ -52,11 +53,12 @@ class SteamCloud : ClientMsgHandler() { val request = ClientMsgProtobuf( CMsgClientUFSGetSingleFileInfo::class.java, EMsg.ClientUFSGetSingleFileInfo - ) - request.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - request.body.setAppId(appId) - request.body.setFileName(filename) + body.appId = appId + body.fileName = filename + } client.send(request) @@ -75,11 +77,12 @@ class SteamCloud : ClientMsgHandler() { val request = ClientMsgProtobuf( CMsgClientUFSShareFile::class.java, EMsg.ClientUFSShareFile - ) - request.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - request.body.setAppId(appId) - request.body.setFileName(filename) + body.appId = appId + body.fileName = filename + } client.send(request) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/SteamFriends.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/SteamFriends.kt index 0bd5080c..43318c97 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/SteamFriends.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/SteamFriends.kt @@ -152,8 +152,8 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChangeStatus::class.java, EMsg.ClientChangeStatus ).apply { - body.setPersonaState(cache.localUser.personaState.code()) - body.setPlayerName(name) + body.personaState = cache.localUser.personaState.code() + body.playerName = name }.also(client::send) } @@ -176,8 +176,8 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChangeStatus::class.java, EMsg.ClientChangeStatus ).apply { - body.setPersonaState(state.code()) - body.setPersonaSetByUser(true) + body.personaState = state.code() + body.personaSetByUser = true }.also(client::send) } @@ -190,8 +190,8 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChangeStatus::class.java, EMsg.ClientChangeStatus ).apply { - body.setPersonaSetByUser(true) - body.setPersonaStateFlags(0) + body.personaSetByUser = true + body.personaStateFlags = 0 }.also(client::send) } @@ -212,8 +212,8 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChangeStatus::class.java, EMsg.ClientChangeStatus ).apply { - body.setPersonaSetByUser(true) - body.setPersonaStateFlags(flag.code()) + body.personaSetByUser = true + body.personaStateFlags = flag.code() }.also(client::send) } @@ -349,9 +349,9 @@ class SteamFriends : ClientMsgHandler() { CMsgClientFriendMsg::class.java, EMsg.ClientFriendMsg ).apply { - body.setSteamid(target.convertToUInt64()) - body.setChatEntryType(type.code()) - body.setMessage(ByteString.copyFrom(message, StandardCharsets.UTF_8)) + body.steamid = target.convertToUInt64() + body.chatEntryType = type.code() + body.message = ByteString.copyFrom(message, StandardCharsets.UTF_8) }.also(client::send) } @@ -365,7 +365,7 @@ class SteamFriends : ClientMsgHandler() { CMsgClientAddFriend::class.java, EMsg.ClientAddFriend ).apply { - body.setAccountnameOrEmailToAdd(accountNameOrEmail) + body.accountnameOrEmailToAdd = accountNameOrEmail }.also(client::send) } @@ -379,7 +379,7 @@ class SteamFriends : ClientMsgHandler() { CMsgClientAddFriend::class.java, EMsg.ClientAddFriend ).apply { - body.setSteamidToAdd(steamID.convertToUInt64()) + body.steamidToAdd = steamID.convertToUInt64() }.also(client::send) } @@ -393,7 +393,7 @@ class SteamFriends : ClientMsgHandler() { CMsgClientRemoveFriend::class.java, EMsg.ClientRemoveFriend ).apply { - body.setFriendid(steamID.convertToUInt64()) + body.friendid = steamID.convertToUInt64() }.also(client::send) } @@ -469,11 +469,11 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChatInvite::class.java, EMsg.ClientChatInvite ).apply { - body.setSteamIdChat(chatID.convertToUInt64()) - body.setSteamIdInvited(steamIdUser.convertToUInt64()) + body.steamIdChat = chatID.convertToUInt64() + body.steamIdInvited = steamIdUser.convertToUInt64() // steamclient also sends the steamid of the user that did the invitation // we'll mimic that behavior - body.setSteamIdPatron(client.steamID.convertToUInt64()) + body.steamIdPatron = client.steamID.convertToUInt64() }.also(client::send) } @@ -548,7 +548,7 @@ class SteamFriends : ClientMsgHandler() { EMsg.ClientRequestFriendData ).apply { body.addAllFriends(steamIdList.map { it.convertToUInt64() }) - body.setPersonaStateRequested(info) + body.personaStateRequested = info }.also(client::send) } @@ -574,12 +574,13 @@ class SteamFriends : ClientMsgHandler() { */ @JvmOverloads fun ignoreFriend(steamID: SteamID, setIgnore: Boolean = true): AsyncJobSingle { - val ignore = ClientMsg(MsgClientSetIgnoreFriend::class.java) - ignore.setSourceJobID(client.getNextJobID()) + val ignore = ClientMsg(MsgClientSetIgnoreFriend::class.java).apply { + sourceJobID = client.getNextJobID() - ignore.body.mySteamId = client.steamID - ignore.body.ignore = if (setIgnore) 1.toByte() else 0.toByte() - ignore.body.steamIdFriend = steamID + body.mySteamId = client.steamID + body.ignore = if (setIgnore) 1.toByte() else 0.toByte() + body.steamIdFriend = steamID + } client.send(ignore) @@ -597,10 +598,11 @@ class SteamFriends : ClientMsgHandler() { val request = ClientMsgProtobuf( CMsgClientFriendProfileInfo::class.java, EMsg.ClientFriendProfileInfo - ) - request.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - request.body.setSteamidFriend(steamID.convertToUInt64()) + body.steamidFriend = steamID.convertToUInt64() + } client.send(request) @@ -618,7 +620,7 @@ class SteamFriends : ClientMsgHandler() { CMsgClientChatGetFriendMessageHistory::class.java, EMsg.ClientChatGetFriendMessageHistory ).apply { - body.setSteamid(steamID.convertToUInt64()) + body.steamid = steamID.convertToUInt64() }.also(client::send) } @@ -643,16 +645,16 @@ class SteamFriends : ClientMsgHandler() { * @return The Job ID of the request. This can be used to find the appropriate [NicknameCallback]. */ fun setFriendNickname(friendID: SteamID, nickname: String): JobID { + val jobID: JobID = client.getNextJobID() val request = ClientMsgProtobuf( CMsgClientSetPlayerNickname::class.java, EMsg.AMClientSetPlayerNickname - ) - val jobID: JobID = client.getNextJobID() - - request.setSourceJobID(jobID) + ).apply { + sourceJobID = jobID - request.body.setSteamid(friendID.convertToUInt64()) - request.body.setNickname(nickname) + body.steamid = friendID.convertToUInt64() + body.nickname = nickname + } client.send(request) @@ -676,21 +678,21 @@ class SteamFriends : ClientMsgHandler() { * @return The Job ID of the request. This can be used to find the appropriate [AliasHistoryCallback]. */ fun requestAliasHistory(steamIDs: List): JobID { + val jobID: JobID = client.getNextJobID() val request = ClientMsgProtobuf( CMsgClientAMGetPersonaNameHistory::class.java, EMsg.ClientAMGetPersonaNameHistory - ) - val jobID: JobID = client.getNextJobID() - - request.setSourceJobID(jobID) + ).apply { + sourceJobID = jobID - request.body.addAllIds( - steamIDs.map { - CMsgClientAMGetPersonaNameHistory.IdInstance.newBuilder().setSteamid(it.convertToUInt64()).build() - } - ) + body.addAllIds( + steamIDs.map { + CMsgClientAMGetPersonaNameHistory.IdInstance.newBuilder().setSteamid(it.convertToUInt64()).build() + } + ) - request.body.setIdCount(request.body.idsCount) + body.idCount = body.idsCount + } client.send(request) @@ -716,8 +718,8 @@ class SteamFriends : ClientMsgHandler() { val callback = getCallback(packetMsg) // Ignore messages that we don't have a handler function for - callback?.let { - client.postCallback(it) + if (callback != null) { + client.postCallback(callback) return } @@ -775,7 +777,7 @@ class SteamFriends : ClientMsgHandler() { logger.debug("Unknown item in handlePersonaState(): $friendID") } - // todo: cache other details/account types? + // todo: (SK) cache other details/account types? } perState.body.friendsList.forEach { friend -> diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatEnterCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatEnterCallback.kt index 5ae865db..c38e0abe 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatEnterCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatEnterCallback.kt @@ -8,11 +8,8 @@ import `in`.dragonbra.javasteam.generated.MsgClientChatEnter import `in`.dragonbra.javasteam.steam.handlers.steamfriends.ChatMemberInfo import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.SteamID +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.BinaryReader -import java.io.ByteArrayInputStream -import java.io.IOException -import java.nio.charset.StandardCharsets -import java.util.* /** * This callback is fired in response to attempting to join a chat. @@ -20,6 +17,10 @@ import java.util.* @Suppress("MemberVisibilityCanBePrivate") class ChatEnterCallback(packetMsg: IPacketMsg) : CallbackMsg() { + companion object { + private val logger = LogManager.getLogger(ChatEnterCallback::class.java) + } + /** * Gets the [SteamID] of the chat room. */ @@ -90,19 +91,18 @@ class ChatEnterCallback(packetMsg: IPacketMsg) : CallbackMsg() { numChatMembers = msg.numMembers - val bais = ByteArrayInputStream(chatEnter.payload.toByteArray()) - + val ms = chatEnter.payload try { - BinaryReader(bais).use { br -> + BinaryReader(ms).use { br -> // steamclient always attempts to read the chat room name, regardless of the enter response - chatRoomName = br.readNullTermString(StandardCharsets.UTF_8) + chatRoomName = br.readNullTermString() if (enterResponse != EChatRoomEnterResponse.Success) { // the rest of the payload depends on a successful chat enter return@use } - val memberList: MutableList = ArrayList() + val memberList: MutableList = mutableListOf() for (i in 0 until numChatMembers) { val memberInfo = ChatMemberInfo() @@ -110,9 +110,11 @@ class ChatEnterCallback(packetMsg: IPacketMsg) : CallbackMsg() { memberList.add(memberInfo) } - chatMembers = Collections.unmodifiableList(memberList) + + chatMembers = memberList.toList() } - } catch (ignored: IOException) { + } catch (e: Exception) { + logger.error("Failed to read chat enter info.", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt index bf8e64ac..4fab55d4 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt @@ -8,8 +8,9 @@ import `in`.dragonbra.javasteam.generated.MsgClientChatMemberInfo import `in`.dragonbra.javasteam.steam.handlers.steamfriends.ChatMemberInfo import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.SteamID +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.BinaryReader -import java.io.ByteArrayInputStream +import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.io.IOException import java.util.* @@ -19,6 +20,10 @@ import java.util.* @Suppress("MemberVisibilityCanBePrivate") class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { + companion object { + private val logger = LogManager.getLogger(ChatMemberInfoCallback::class.java) + } + /** * Gets SteamId of the chat room. */ @@ -42,8 +47,8 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { type = msg.type when (type) { - EChatInfoType.StateChange -> stateChangeInfo = StateChangeDetails(membInfo.payload.toByteArray()) - // todo: handle more types + EChatInfoType.StateChange -> stateChangeInfo = StateChangeDetails(membInfo.payload) + // todo: (SK) handle more types // based off disassembly // - for InfoUpdate, a ChatMemberInfo object is present // - for MemberLimitChange, looks like an ignored uint64 (probably steamid) followed @@ -55,7 +60,7 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { /** * Represents state change information. */ - class StateChangeDetails(data: ByteArray?) { + class StateChangeDetails(ms: MemoryStream) { /** * Gets the [SteamID] of the chatter that was acted on. */ @@ -79,7 +84,7 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { init { try { - BinaryReader(ByteArrayInputStream(data)).use { br -> + BinaryReader(ms).use { br -> chatterActedOn = SteamID(br.readLong()) stateChange = EChatMemberStateChange.from(br.readInt()) chatterActedBy = SteamID(br.readLong()) @@ -88,7 +93,8 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { memberInfo!!.readFromStream(br) } } - } catch (ignored: IOException) { + } catch (e: IOException) { + logger.error("Failed to read chat member info", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatRoomInfoCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatRoomInfoCallback.kt index 659a4a2a..8e1e24f4 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatRoomInfoCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatRoomInfoCallback.kt @@ -29,6 +29,6 @@ class ChatRoomInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { chatRoomID = msg.steamIdChat type = msg.type - // todo: handle inner payload based on the type similar to ChatMemberInfoCallback + // todo: (SK) handle inner payload based on the type similar to ChatMemberInfoCallback } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgamecoordinator/SteamGameCoordinator.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgamecoordinator/SteamGameCoordinator.kt index 7d7f3c02..a80b0b8c 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgamecoordinator/SteamGameCoordinator.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgamecoordinator/SteamGameCoordinator.kt @@ -22,13 +22,13 @@ class SteamGameCoordinator : ClientMsgHandler() { * @param appId The app id of the game coordinator to send to. */ fun send(msg: IClientGCMsg, appId: Int) { - val clientMsg = ClientMsgProtobuf(CMsgGCClient::class.java, EMsg.ClientToGC) + val clientMsg = ClientMsgProtobuf(CMsgGCClient::class.java, EMsg.ClientToGC).apply { + protoHeader.routingAppid = appId + body.msgtype = MsgUtil.makeGCMsg(msg.getMsgType(), msg.isProto()) + body.appid = appId - clientMsg.protoHeader.setRoutingAppid(appId) - clientMsg.body.setMsgtype(MsgUtil.makeGCMsg(msg.getMsgType(), msg.isProto())) - clientMsg.body.setAppid(appId) - - clientMsg.body.setPayload(ByteString.copyFrom(msg.serialize())) + body.payload = ByteString.copyFrom(msg.serialize()) + } client.send(clientMsg) } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgameserver/SteamGameServer.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgameserver/SteamGameServer.kt index 820ba37e..af829db7 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgameserver/SteamGameServer.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamgameserver/SteamGameServer.kt @@ -8,6 +8,7 @@ import `in`.dragonbra.javasteam.enums.EMsg import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.enums.EServerFlags import `in`.dragonbra.javasteam.generated.MsgClientLogon +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesBase.CMsgIPAddress import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverGameservers.CMsgGSServerType import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLogOff import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLogin.CMsgClientLogon @@ -48,19 +49,19 @@ class SteamGameServer : ClientMsgHandler() { val gsId = SteamID(0, 0, client.universe, EAccountType.GameServer) - logon.protoHeader.setClientSessionid(0) - logon.protoHeader.setSteamid(gsId.convertToUInt64()) + logon.protoHeader.clientSessionid = 0 + logon.protoHeader.steamid = gsId.convertToUInt64() - val localIp: Int = NetHelpers.getIPAddress(client.localIP) - logon.body.setDeprecatedObfustucatedPrivateIp(localIp xor MsgClientLogon.ObfuscationMask) // TODO: Using deprecated method. + val localIp: CMsgIPAddress = NetHelpers.getMsgIPAddress(client.localIP) + logon.body.obfuscatedPrivateIp = NetHelpers.obfuscatePrivateIP(localIp) - logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol) + logon.body.protocolVersion = MsgClientLogon.CurrentProtocol - logon.body.setClientOsType(Utils.getOSType().code()) - logon.body.setGameServerAppId(details.appID) - logon.body.setMachineId(ByteString.copyFrom(HardwareUtils.getMachineID())) + logon.body.clientOsType = Utils.getOSType().code() + logon.body.gameServerAppId = details.appID + logon.body.machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) - logon.body.setGameServerToken(details.token) + logon.body.gameServerToken = details.token client.send(logon) } @@ -83,17 +84,17 @@ class SteamGameServer : ClientMsgHandler() { val gsId = SteamID(0, 0, client.universe, EAccountType.AnonGameServer) - logon.protoHeader.setClientSessionid(0) - logon.protoHeader.setSteamid(gsId.convertToUInt64()) + logon.protoHeader.clientSessionid = 0 + logon.protoHeader.steamid = gsId.convertToUInt64() - val localIp: Int = NetHelpers.getIPAddress(client.localIP) - logon.body.setDeprecatedObfustucatedPrivateIp(localIp xor MsgClientLogon.ObfuscationMask) // TODO: Using deprecated method. + val localIp: CMsgIPAddress = NetHelpers.getMsgIPAddress(client.localIP) + logon.body.obfuscatedPrivateIp = NetHelpers.obfuscatePrivateIP(localIp) - logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol) + logon.body.protocolVersion = MsgClientLogon.CurrentProtocol - logon.body.setClientOsType(Utils.getOSType().code()) - logon.body.setGameServerAppId(appId) - logon.body.setMachineId(ByteString.copyFrom(HardwareUtils.getMachineID())) + logon.body.clientOsType = Utils.getOSType().code() + logon.body.gameServerAppId = appId + logon.body.machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) client.send(logon) } @@ -120,16 +121,20 @@ class SteamGameServer : ClientMsgHandler() { fun sendStatus(details: StatusDetails) { require(!(details.address != null && details.address is Inet6Address)) { "Only IPv4 addresses are supported." } - val status = ClientMsgProtobuf(CMsgGSServerType::class.java, EMsg.GSServerType) - status.body.setAppIdServed(details.appID) - status.body.setFlags(EServerFlags.code(details.serverFlags)) - status.body.setGameDir(details.gameDirectory) - status.body.setGamePort(details.port) - status.body.setGameQueryPort(details.queryPort) - status.body.setGameVersion(details.version) + val status = ClientMsgProtobuf( + CMsgGSServerType::class.java, + EMsg.GSServerType + ).apply { + body.appIdServed = details.appID + body.flags = EServerFlags.code(details.serverFlags) + body.gameDir = details.gameDirectory + body.gamePort = details.port + body.gameQueryPort = details.queryPort + body.gameVersion = details.version + } - details.address?.let { - status.body.setDeprecatedGameIpAddress(NetHelpers.getIPAddress(it)) // TODO: Using deprecated method. + if (details.address != null) { + status.body.deprecatedGameIpAddress = NetHelpers.getIPAddress(details.address!!) } client.send(status) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/QueryDetails.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/QueryDetails.kt index 0b0fab07..81e877b2 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/QueryDetails.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/QueryDetails.kt @@ -7,7 +7,7 @@ import java.net.InetAddress * Details used when performing a server list query. * * @param appID Gets or sets the AppID used when querying servers. - * @param filter Gets or sets the filter used for querying the master server. Check https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol for details on how the filter is structured. + * @param filter Gets or sets the filter used for querying the master server. Check [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) for details on how the filter is structured. * @param region Gets or sets the region that servers will be returned from. * @param geoLocatedIP Gets or sets the IP address that will be GeoIP located. This is done to return servers closer to this location. * @param maxServers Gets or sets the maximum number of servers to return. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/SteamMasterServer.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/SteamMasterServer.kt index 127147ed..4da2451b 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/SteamMasterServer.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammasterserver/SteamMasterServer.kt @@ -26,19 +26,20 @@ class SteamMasterServer : ClientMsgHandler() { val query = ClientMsgProtobuf( CMsgClientGMSServerQuery::class.java, EMsg.ClientGMSServerQuery - ) - query.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - query.body.setAppId(details.appID) + body.appId = details.appID - details.geoLocatedIP?.let { - query.body.setGeoLocationIp(NetHelpers.getIPAddress(it)) - } + if (details.geoLocatedIP != null) { + body.geoLocationIp = NetHelpers.getIPAddress(details.geoLocatedIP!!) + } - query.body.setFilterText(details.filter) - query.body.setRegionCode(details.region.code().toInt()) + body.filterText = details.filter + body.regionCode = details.region.code().toInt() - query.body.setMaxServers(details.maxServers) + body.maxServers = details.maxServers + } client.send(query) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnetworking/SteamNetworking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnetworking/SteamNetworking.kt index 68ecca70..8bfabbb4 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnetworking/SteamNetworking.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnetworking/SteamNetworking.kt @@ -28,11 +28,12 @@ class SteamNetworking : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgClientNetworkingCertRequest::class.java, EMsg.ClientNetworkingCertRequest - ) - msg.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - msg.body.setAppId(appId) - msg.body.setKeyData(ByteString.copyFrom(publicKey)) + body.appId = appId + body.keyData = ByteString.copyFrom(publicKey) + } client.send(msg) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/CommentNotificationsCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/CommentNotificationsCallback.kt index d4be5642..86179827 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/CommentNotificationsCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/CommentNotificationsCallback.kt @@ -30,9 +30,10 @@ class CommentNotificationsCallback(packetMsg: IPacketMsg) : CallbackMsg() { CMsgClientCommentNotifications::class.java, packetMsg ) + val msg = resp.body - commentCount = resp.body.countNewComments - commentOwnerCount = resp.body.countNewCommentsOwner - commentSubscriptionsCount = resp.body.countNewCommentsSubscriptions + commentCount = msg.countNewComments + commentOwnerCount = msg.countNewCommentsOwner + commentSubscriptionsCount = msg.countNewCommentsSubscriptions } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/ItemAnnouncementsCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/ItemAnnouncementsCallback.kt index 792da30b..356b53b4 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/ItemAnnouncementsCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/ItemAnnouncementsCallback.kt @@ -20,7 +20,8 @@ class ItemAnnouncementsCallback(packetMsg: IPacketMsg) : CallbackMsg() { CMsgClientItemAnnouncements::class.java, packetMsg ) + val msg = resp.body - count = resp.body.countNewItems + count = msg.countNewItems } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/OfflineMessageNotificationCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/OfflineMessageNotificationCallback.kt index abab8494..bc67b903 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/OfflineMessageNotificationCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/OfflineMessageNotificationCallback.kt @@ -27,10 +27,9 @@ class OfflineMessageNotificationCallback(packetMsg: IPacketMsg) : CallbackMsg() CMsgClientOfflineMessageNotification::class.java, packetMsg ) + val msg = resp.body - messageCount = resp.body.offlineMessages - friendsWithOfflineMessages = resp.body.friendsWithOfflineMessagesList.map { - SteamID(it.toLong()) - } + messageCount = msg.offlineMessages + friendsWithOfflineMessages = msg.friendsWithOfflineMessagesList.map { SteamID(it.toLong()) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/UserNotificationsCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/UserNotificationsCallback.kt index c89b85de..1af62493 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/UserNotificationsCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamnotifications/callback/UserNotificationsCallback.kt @@ -21,7 +21,8 @@ class UserNotificationsCallback(packetMsg: IPacketMsg) : CallbackMsg() { CMsgClientUserNotifications::class.java, packetMsg ) + val msg = resp.body - notifications = resp.body.notificationsList.map(::Notification) + notifications = msg.notificationsList.map(::Notification) } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamscreenshots/SteamScreenshots.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamscreenshots/SteamScreenshots.kt index df6e3283..c49e0506 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamscreenshots/SteamScreenshots.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamscreenshots/SteamScreenshots.kt @@ -27,20 +27,21 @@ class SteamScreenshots : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgClientUCMAddScreenshot::class.java, EMsg.ClientUCMAddScreenshot - ) - msg.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - details.gameID?.let { - msg.body.setAppid(it.appID) - } + if (details.gameID != null) { + body.appid = details.gameID!!.appID + } - msg.body.setCaption(details.caption) - msg.body.setFilename(details.ufsImageFilePath) - msg.body.setPermissions(details.privacy.code()) - msg.body.setThumbname(details.usfThumbnailFilePath) - msg.body.setWidth(details.width) - msg.body.setHeight(details.height) - msg.body.setRtime32Created((details.creationTime.time / 1000L).toInt()) + body.caption = details.caption + body.filename = details.ufsImageFilePath + body.permissions = details.privacy.code() + body.thumbname = details.usfThumbnailFilePath + body.width = details.width + body.height = details.height + body.rtime32Created = (details.creationTime.time / 1000L).toInt() + } client.send(msg) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/SteamUnifiedMessages.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/SteamUnifiedMessages.kt index 33ed1390..5397e45d 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/SteamUnifiedMessages.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/SteamUnifiedMessages.kt @@ -57,8 +57,8 @@ class SteamUnifiedMessages : ClientMsgHandler() { } ClientMsgProtobuf(message.javaClass, eMsg).apply { - setSourceJobID(jobID) - header.proto.setTargetJobName(rpcName) + sourceJobID = jobID + header.proto.targetJobName = rpcName body!!.mergeFrom(message) }.also(client::send) @@ -83,7 +83,7 @@ class SteamUnifiedMessages : ClientMsgHandler() { } ClientMsgProtobuf(message.javaClass, eMsg).apply { - header.proto.setTargetJobName(rpcName) + header.proto.targetJobName = rpcName body!!.mergeFrom(message) }.also(client::send) } @@ -98,7 +98,7 @@ class SteamUnifiedMessages : ClientMsgHandler() { private fun handleServiceMethod(packetMsg: IPacketMsg) { require(packetMsg is PacketClientMsgProtobuf) { "Packet message is expected to be protobuf." } - val jobName = packetMsg.header.proto.getTargetJobName() + val jobName = packetMsg.header.proto.targetJobName if (jobName.isNullOrEmpty()) { logger.debug("Job name is null or empty") @@ -130,7 +130,7 @@ class SteamUnifiedMessages : ClientMsgHandler() { client.postCallback(ServiceMethodNotification(argumentType, packetMsg)) } - } catch (e: ClassNotFoundException) { + } catch (_: ClassNotFoundException) { // The RPC service implementation was not implemented. // Either the .proto is missing, or the service was not converted to an interface yet. logger.debug("Service Method: $serviceName, was not found") diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/callback/ServiceMethodNotification.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/callback/ServiceMethodNotification.kt index 5d70a96e..d26e8ceb 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/callback/ServiceMethodNotification.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamunifiedmessages/callback/ServiceMethodNotification.kt @@ -54,7 +54,7 @@ class ServiceMethodNotification(messageType: Class, packetM init { // Note: JobID will be -1 - methodName = clientMsg.header.proto.getTargetJobName() + methodName = clientMsg.header.proto.targetJobName body = clientMsg.body.build() } } 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 125ec7ad..e43ea22f 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 @@ -69,61 +69,67 @@ class SteamUser : ClientMsgHandler() { val steamID = SteamID(details.accountID, details.accountInstance, client.universe, EAccountType.Individual) if (details.loginID != null) { - // TODO: Support IPv6 login ids? - CMsgIPAddress.newBuilder().apply { + // TODO: (SK) Support IPv6 login ids? + logon.body.obfuscatedPrivateIp = CMsgIPAddress.newBuilder().apply { v4 = details.loginID!! - }.build().also(logon.body::setObfuscatedPrivateIp) + }.build() } else { - CMsgIPAddress.newBuilder().apply { - client.localIP?.let { localIp -> - v4 = NetHelpers.getIPAddress(localIp) xor MsgClientLogon.ObfuscationMask - } - }.build().also(logon.body::setObfuscatedPrivateIp) + client.localIP?.let { localIP -> + val msgIpAddr = NetHelpers.getMsgIPAddress(localIP) + logon.body.obfuscatedPrivateIp = NetHelpers.obfuscatePrivateIP(msgIpAddr) + } } // Legacy field, Steam client still sets it if (logon.body.obfuscatedPrivateIp.hasV4()) { - logon.body.setDeprecatedObfustucatedPrivateIp(logon.body.obfuscatedPrivateIp.getV4()) + logon.body.deprecatedObfustucatedPrivateIp = logon.body.obfuscatedPrivateIp.v4 } - logon.protoHeader.setClientSessionid(0) - logon.protoHeader.setSteamid(steamID.convertToUInt64()) + logon.protoHeader.clientSessionid = 0 + logon.protoHeader.steamid = steamID.convertToUInt64() - logon.body.setAccountName(details.username) - details.password?.let { logon.body.setPassword(it) } - logon.body.setShouldRememberPassword(details.shouldRememberPassword) + logon.body.accountName = details.username + if (details.password != null) { + logon.body.password = details.password + } + logon.body.shouldRememberPassword = details.shouldRememberPassword - logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol) - logon.body.setClientOsType(details.clientOSType.code()) - logon.body.setClientLanguage(details.clientLanguage) - logon.body.setCellId(details.cellID ?: client.configuration.cellID) + logon.body.protocolVersion = MsgClientLogon.CurrentProtocol + logon.body.clientOsType = details.clientOSType.code() + logon.body.clientLanguage = details.clientLanguage + logon.body.cellId = details.cellID ?: client.configuration.cellID - logon.body.setSteam2TicketRequest(details.requestSteam2Ticket) + logon.body.steam2TicketRequest = details.requestSteam2Ticket // we're now using the latest steamclient package version, this is required to get a proper sentry file for steam guard - logon.body.setClientPackageVersion(1771) // todo: determine if this is still required - logon.body.setSupportsRateLimitResponse(true) - logon.body.setMachineName(details.machineName) - val machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) - logon.body.setMachineId(machineId) + logon.body.clientPackageVersion = 1771 // todo: (SK) determine if this is still required + logon.body.supportsRateLimitResponse = true + logon.body.machineName = details.machineName + logon.body.machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) if (details.chatMode != ChatMode.DEFAULT) { - logon.body.setChatMode(details.chatMode.mode) + logon.body.chatMode = details.chatMode.mode } if (details.uiMode != EUIMode.Unknown) { - logon.body.setUiMode(details.uiMode.code()) + logon.body.uiMode = details.uiMode.code() } if (details.isSteamDeck) { - logon.body.setIsSteamDeck(true) + logon.body.isSteamDeck = true } // steam guard - details.authCode?.let(logon.body::setAuthCode) - details.twoFactorCode?.let(logon.body::setTwoFactorCode) + if (details.authCode != null) { + logon.body.authCode = details.authCode + } + if (details.twoFactorCode != null) { + logon.body.twoFactorCode = details.twoFactorCode + } - details.accessToken?.let(logon.body::setAccessToken) + if (details.accessToken != null) { + logon.body.accessToken = details.accessToken + } client.send(logon) } @@ -146,16 +152,15 @@ class SteamUser : ClientMsgHandler() { val auId = SteamID(0, 0, client.universe, EAccountType.AnonUser) - logon.protoHeader.setClientSessionid(0) - logon.protoHeader.setSteamid(auId.convertToUInt64()) + logon.protoHeader.clientSessionid = 0 + logon.protoHeader.steamid = auId.convertToUInt64() - logon.body.setProtocolVersion(MsgClientLogon.CurrentProtocol) - logon.body.setClientOsType(details.clientOSType.code()) - logon.body.setClientLanguage(details.clientLanguage) - logon.body.setCellId(details.cellID ?: client.configuration.cellID) + logon.body.protocolVersion = MsgClientLogon.CurrentProtocol + logon.body.clientOsType = details.clientOSType.code() + logon.body.clientLanguage = details.clientLanguage + logon.body.cellId = details.cellID ?: client.configuration.cellID - val machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) - logon.body.setMachineId(machineId) + logon.body.machineId = ByteString.copyFrom(HardwareUtils.getMachineID()) client.send(logon) } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/LoggedOnCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/LoggedOnCallback.kt index 5912fbb7..a2c40331 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/LoggedOnCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/LoggedOnCallback.kt @@ -14,6 +14,7 @@ import `in`.dragonbra.javasteam.steam.handlers.steamuser.SteamUser import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.SteamID import `in`.dragonbra.javasteam.util.NetHelpers +import `in`.dragonbra.javasteam.util.log.LogManager import java.net.InetAddress import java.util.* @@ -23,6 +24,10 @@ import java.util.* @Suppress("unused", "MemberVisibilityCanBePrivate") class LoggedOnCallback : CallbackMsg { + companion object { + private val logger = LogManager.getLogger(LoggedOnCallback::class.java) + } + /** * Gets the result of the logon. */ @@ -127,7 +132,7 @@ class LoggedOnCallback : CallbackMsg { constructor(packetMsg: IPacketMsg) { if (!packetMsg.isProto) { - handleNonProtoLogin(packetMsg) + handleNonProtoLogon(packetMsg) return } @@ -143,7 +148,7 @@ class LoggedOnCallback : CallbackMsg { outOfGameSecsPerHeartbeat = resp.legacyOutOfGameHeartbeatSeconds inGameSecsPerHeartbeat = resp.heartbeatSeconds - publicIP = NetHelpers.getIPAddress(resp.publicIp.v4) // Has ipV6 support, but still using ipV4 + publicIP = NetHelpers.getIPAddress(resp.publicIp) serverTime = Date(resp.rtime32ServerTime * 1000L) @@ -165,11 +170,11 @@ class LoggedOnCallback : CallbackMsg { numLoginFailuresToMigrate = resp.countLoginfailuresToMigrate numDisconnectsToMigrate = resp.countDisconnectsToMigrate - resp.parentalSettings?.let { + if (resp.parentalSettings != null) { try { - parentalSettings = ParentalSettings.parseFrom(it) + parentalSettings = ParentalSettings.parseFrom(resp.parentalSettings) } catch (e: InvalidProtocolBufferException) { - e.printStackTrace() + logger.error("Failed to parse parental settings", e) } } } @@ -178,7 +183,7 @@ class LoggedOnCallback : CallbackMsg { this.result = result } - private fun handleNonProtoLogin(packetMsg: IPacketMsg) { + private fun handleNonProtoLogon(packetMsg: IPacketMsg) { val loginResp = ClientMsg(MsgClientLogOnResponse::class.java, packetMsg) val resp = loginResp.body diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/MarketingMessageCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/MarketingMessageCallback.kt index e4956152..b1d8e5c0 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/MarketingMessageCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuser/callback/MarketingMessageCallback.kt @@ -9,8 +9,7 @@ import `in`.dragonbra.javasteam.types.GlobalID import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.log.Logger import `in`.dragonbra.javasteam.util.stream.BinaryReader -import java.io.ByteArrayInputStream -import java.io.IOException +import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.nio.charset.StandardCharsets import java.util.* @@ -35,10 +34,10 @@ class MarketingMessageCallback(packetMsg: IPacketMsg) : CallbackMsg() { updateTime = Date(body.marketingMessageUpdateTime * 1000L) - val msgList: MutableList = ArrayList() + val msgList: MutableList = mutableListOf() try { - BinaryReader(ByteArrayInputStream(marketingMessage.payload.toByteArray())).use { br -> + BinaryReader(marketingMessage.payload).use { br -> for (i in 0 until body.count) { val dataLen = br.readInt() - 4 // total length includes the 4 byte length val messageData = br.readBytes(dataLen) @@ -46,8 +45,8 @@ class MarketingMessageCallback(packetMsg: IPacketMsg) : CallbackMsg() { msgList.add(Message(messageData)) } } - } catch (e: IOException) { - logger.debug(e) + } catch (e: Exception) { + logger.error("Failed to get marketing messages", e) } messages = msgList.toList() @@ -78,13 +77,14 @@ class MarketingMessageCallback(packetMsg: IPacketMsg) : CallbackMsg() { init { try { - BinaryReader(ByteArrayInputStream(data)).use { br -> + val ms = MemoryStream(data) + BinaryReader(ms).use { br -> id = GlobalID(br.readLong()) url = br.readNullTermString(StandardCharsets.UTF_8) flags = EMarketingMessageFlags.from(br.readInt()) } - } catch (e: IOException) { - logger.debug(e) + } catch (e: Exception) { + logger.error("Failed to parse marketing messages", e) } } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/LeaderboardEntry.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/LeaderboardEntry.kt index 117d353b..d431105a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/LeaderboardEntry.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/LeaderboardEntry.kt @@ -3,6 +3,7 @@ package `in`.dragonbra.javasteam.steam.handlers.steamuserstats import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLbs.CMsgClientLBSGetLBEntriesResponse import `in`.dragonbra.javasteam.types.SteamID import `in`.dragonbra.javasteam.types.UGCHandle +import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.stream.BinaryReader import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.io.IOException @@ -13,6 +14,10 @@ import java.io.IOException @Suppress("unused") class LeaderboardEntry(entry: CMsgClientLBSGetLBEntriesResponse.Entry) { + companion object { + private val logger = LogManager.getLogger(LeaderboardEntry::class.java) + } + /** * Gets the [SteamID] for this entry. */ @@ -39,7 +44,7 @@ class LeaderboardEntry(entry: CMsgClientLBSGetLBEntriesResponse.Entry) { val details: List init { - val entryDetails = mutableListOf() + val entryDetails: MutableList = mutableListOf() if (entry.details != null) { val ms = MemoryStream(entry.details.toByteArray()) @@ -50,7 +55,7 @@ class LeaderboardEntry(entry: CMsgClientLBSGetLBEntriesResponse.Entry) { entryDetails.add(br.readInt()) } } catch (e: IOException) { - throw IllegalArgumentException("failed to read details", e) + logger.error("failed to read details", e) } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt index a7ee1d3b..5666a59c 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt @@ -32,10 +32,11 @@ class SteamUserStats : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgDPGetNumberOfCurrentPlayers::class.java, EMsg.ClientGetNumberOfCurrentPlayersDP - ) - msg.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - msg.body.setAppid(appId) + body.appid = appId + } client.send(msg) @@ -55,15 +56,16 @@ class SteamUserStats : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgClientLBSFindOrCreateLB::class.java, EMsg.ClientLBSFindOrCreateLB - ) - msg.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = (client.getNextJobID()) - // routing_appid has to be set correctly to receive a response - msg.protoHeader.setRoutingAppid(appId) + // routing_appid has to be set correctly to receive a response + protoHeader.routingAppid = appId - msg.body.setAppId(appId) - msg.body.setLeaderboardName(name) - msg.body.setCreateIfNotFound(false) + body.appId = appId + body.leaderboardName = name + body.createIfNotFound = false + } client.send(msg) @@ -90,17 +92,18 @@ class SteamUserStats : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgClientLBSFindOrCreateLB::class.java, EMsg.ClientLBSFindOrCreateLB - ) - msg.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - // routing_appid has to be set correctly to receive a response - msg.protoHeader.setRoutingAppid(appId) + // routing_appid has to be set correctly to receive a response + protoHeader.routingAppid = appId - msg.body.setAppId(appId) - msg.body.setLeaderboardName(name) - msg.body.setLeaderboardDisplayType(displayType.code()) - msg.body.setLeaderboardSortMethod(sortMethod.code()) - msg.body.setCreateIfNotFound(true) + body.appId = appId + body.leaderboardName = name + body.leaderboardDisplayType = displayType.code() + body.leaderboardSortMethod = sortMethod.code() + body.createIfNotFound = true + } client.send(msg) @@ -129,14 +132,15 @@ class SteamUserStats : ClientMsgHandler() { val msg = ClientMsgProtobuf( CMsgClientLBSGetLBEntries::class.java, EMsg.ClientLBSGetLBEntries - ) - msg.setSourceJobID(client.getNextJobID()) - - msg.body.setAppId(appId) - msg.body.setLeaderboardId(id) - msg.body.setLeaderboardDataRequest(dataRequest.code()) - msg.body.setRangeStart(rangeStart) - msg.body.setRangeEnd(rangeEnd) + ).apply { + sourceJobID = client.getNextJobID() + + body.appId = appId + body.leaderboardId = id + body.leaderboardDataRequest = dataRequest.code() + body.rangeStart = rangeStart + body.rangeEnd = rangeEnd + } client.send(msg) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamworkshop/SteamWorkshop.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamworkshop/SteamWorkshop.kt index 1e811983..d9bd8034 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamworkshop/SteamWorkshop.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamworkshop/SteamWorkshop.kt @@ -26,12 +26,13 @@ class SteamWorkshop : ClientMsgHandler() { val enumRequest = ClientMsgProtobuf( CMsgClientUCMEnumeratePublishedFilesByUserAction::class.java, EMsg.ClientUCMEnumeratePublishedFilesByUserAction - ) - enumRequest.setSourceJobID(client.getNextJobID()) + ).apply { + sourceJobID = client.getNextJobID() - enumRequest.body.setAction(details.userAction.code()) - enumRequest.body.setAppId(details.appID) - enumRequest.body.setStartIndex(details.startIndex) + body.action = details.userAction.code() + body.appId = details.appID + body.startIndex = details.startIndex + } client.send(enumRequest) 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 3f2e75b1..60c6bdbc 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt @@ -189,7 +189,7 @@ class SteamClient @JvmOverloads constructor( boxID = 0L processID = 0L sequentialCount = currentJobId.incrementAndGet() - setStartTime(processStartTime) + startTime = processStartTime } fun startJob(job: AsyncJob) { diff --git a/src/main/java/in/dragonbra/javasteam/types/AsyncJobMultiple.kt b/src/main/java/in/dragonbra/javasteam/types/AsyncJobMultiple.kt index 4c4c8ed4..ef4aafd8 100644 --- a/src/main/java/in/dragonbra/javasteam/types/AsyncJobMultiple.kt +++ b/src/main/java/in/dragonbra/javasteam/types/AsyncJobMultiple.kt @@ -49,7 +49,7 @@ class AsyncJobMultiple( } override fun setFailed(dueToRemoteFailure: Boolean) { - if (results.size == 0) { + if (results.isEmpty()) { // if we have zero callbacks in our result set, we cancel this task if (dueToRemoteFailure) { // if we're canceling with a remote failure, post a job failure exception diff --git a/src/main/java/in/dragonbra/javasteam/types/KVTextReader.java b/src/main/java/in/dragonbra/javasteam/types/KVTextReader.java deleted file mode 100644 index 05e0deaf..00000000 --- a/src/main/java/in/dragonbra/javasteam/types/KVTextReader.java +++ /dev/null @@ -1,220 +0,0 @@ -package in.dragonbra.javasteam.types; - -import in.dragonbra.javasteam.util.Passable; -import in.dragonbra.javasteam.util.Strings; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PushbackInputStream; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; - -/** - * @author lngtr - * @since 2018-02-26 - */ -public class KVTextReader extends PushbackInputStream { - - public static final Map ESCAPED_MAPPING; - - private final StringBuilder sb = new StringBuilder(128); - - static { - Map escapedMapping = new TreeMap<>(); - - escapedMapping.put('n', '\n'); - escapedMapping.put('r', '\r'); - escapedMapping.put('t', '\t'); - escapedMapping.put('\\', '\\'); - - ESCAPED_MAPPING = Collections.unmodifiableMap(escapedMapping); - } - - KVTextReader(KeyValue kv, InputStream is) throws IOException { - super(is); - Passable wasQuoted = new Passable<>(false); - Passable wasConditional = new Passable<>(false); - - KeyValue currentKey = kv; - - do { - String s = readToken(wasQuoted, wasConditional); - - if (Strings.isNullOrEmpty(s)) { - break; - } - - if (currentKey == null) { - currentKey = new KeyValue(s); - } else { - currentKey.setName(s); - } - - s = readToken(wasQuoted, wasConditional); - - if (wasConditional.getValue()) { - // Now get the '{' - s = readToken(wasQuoted, wasConditional); - } - - if (s.startsWith("{") && !wasQuoted.getValue()) { - // header is valid so load the file - currentKey.recursiveLoadFromBuffer(this); - } else { - throw new IllegalStateException("LoadFromBuffer: missing {"); - } - - currentKey = null; - } while (!endOfStream()); - } - - private void eatWhiteSpace() throws IOException { - while (!endOfStream()) { - if (!Character.isWhitespace((char) peek())) { - break; - } - - read(); - } - } - - private boolean eatCPPComment() throws IOException { - if (!endOfStream()) { - char next = (char) peek(); - - if (next == '/') { - readLine(); - return true; - /* - * As came up in parsing the Dota 2 units.txt file, the reference (Valve) implementation - * of the KV format considers a single forward slash to be sufficient to comment out the - * entirety of a line. While they still _tend_ to use two, it's not required, and likely - * is just done out of habit. - */ - } - - return false; - } - return false; - } - - private void readLine() throws IOException { - char c; - do { - c = (char) read(); - } while (c != '\n' && !endOfStream()); - } - - private byte peek() throws IOException { - int p = read(); - if (p >= 0) { - unread(p); - } - return (byte) p; - } - - public String readToken(Passable wasQuoted, Passable wasConditional) throws IOException { - wasQuoted.setValue(false); - wasConditional.setValue(false); - - while (true) { - eatWhiteSpace(); - - if (endOfStream()) { - return null; - } - - if (!eatCPPComment()) { - break; - } - } - - if (endOfStream()) { - return null; - } - - char next = (char) peek(); - if (next == '"') { - wasQuoted.setValue(true); - - // " - read(); - - sb.setLength(0); - while (!endOfStream()) { - if (peek() == '\\') { - read(); - - char escapedChar = (char) read(); - - Character replacedChar = ESCAPED_MAPPING.get(escapedChar); - if (replacedChar == null) { - replacedChar = escapedChar; - } - - sb.append((char) replacedChar); - - continue; - } - - if (peek() == '"') { - break; - } - - sb.append((char) read()); - } - - // " - read(); - - return sb.toString(); - } - - if (next == '{' || next == '}') { - read(); - return String.valueOf(next); - } - - boolean bConditionalStart = false; - int count = 0; - sb.setLength(0); - - while (!endOfStream()) { - next = (char) peek(); - - if (next == '"' || next == '{' || next == '}') { - break; - } - - if (next == '[') { - bConditionalStart = true; - } - - if (next == ']' && bConditionalStart) { - wasConditional.setValue(true); - } - - if (Character.isWhitespace(next)) { - break; - } - - if (count < 1023) { - sb.append(next); - } else { - throw new IOException("ReadToken overflow"); - } - - read(); - } - return sb.toString(); - } - - private boolean endOfStream() { - try { - return peek() == -1; - } catch (IOException e) { - return true; - } - } -} diff --git a/src/main/java/in/dragonbra/javasteam/types/KVTextReader.kt b/src/main/java/in/dragonbra/javasteam/types/KVTextReader.kt new file mode 100644 index 00000000..b56db0bb --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/types/KVTextReader.kt @@ -0,0 +1,238 @@ +package `in`.dragonbra.javasteam.types + +import `in`.dragonbra.javasteam.util.Passable +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.lang.IllegalStateException +import java.lang.StringBuilder +import java.nio.charset.StandardCharsets + +/** + * @author lngtr + * @since 2018-02-26 + */ +class KVTextReader +@Throws(IllegalStateException::class, IOException::class) +internal constructor( + kv: KeyValue, + inputStream: InputStream, +) : InputStreamReader(inputStream, StandardCharsets.UTF_8) { + + companion object { + @JvmField + val ESCAPED_MAPPING = mapOf( + '\\' to '\\', + 'n' to '\n', + 'r' to '\r', + 't' to '\t', + // todo: (SK) any others? + ) + } + + private val sb = StringBuilder(128) + + private var peekedChar: Int? = null + + // Mimics C# StreamReader 'Peek()' + val peek: Int + get() { + if (peekedChar == null) { + peekedChar = read() + } + return peekedChar ?: -1 + } + + // Mimics C# StreamReader 'EndOfStream' + val endOfStream: Boolean + get() { + return try { + peek == -1 + } catch (_: IOException) { + true + } + } + + init { + val wasQuoted = Passable(false) + val wasConditional = Passable(false) + + var currentKey: KeyValue? = kv + + do { + var s = readToken(wasQuoted, wasConditional) + + if (s.isNullOrEmpty()) { + break + } + + if (currentKey == null) { + currentKey = KeyValue(s) + } else { + currentKey.name = s + } + + s = readToken(wasQuoted, wasConditional) + + if (wasConditional.value == true) { + // Now get the '{' + s = readToken(wasQuoted, wasConditional) + } + + if (s != null && s.startsWith("{") && wasQuoted.value == false) { + // header is valid so load the file + currentKey.recursiveLoadFromBuffer(this) + } else { + throw IllegalStateException("LoadFromBuffer: missing {") + } + + currentKey = null + } while (!endOfStream) + } + + // override read() to peek the char. + override fun read(): Int { + if (peekedChar != null) { + val result = peekedChar!! + peekedChar = null + return result + } + return super.read() + } + + @Throws(IOException::class) + private fun eatWhiteSpace() { + while (!endOfStream) { + if (!peek.toChar().isWhitespace()) { + break + } + + read() + } + } + + @Throws(IOException::class) + private fun eatCPPComment(): Boolean { + if (!endOfStream) { + val next = peek.toChar() + + if (next == '/') { + readLine() + return true + /* + * As came up in parsing the Dota 2 units.txt file, the reference (Valve) implementation + * of the KV format considers a single forward slash to be sufficient to comment out the + * entirety of a line. While they still _tend_ to use two, it's not required, and likely + * is just done out of habit. + */ + } + + return false + } + + return false + } + + @Throws(IOException::class) + private fun readLine() { + var c: Char + do { + c = read().toChar() + } while (c != '\n' && !endOfStream) + } + + @Throws(IOException::class) + fun readToken(wasQuoted: Passable, wasConditional: Passable): String? { + wasQuoted.value = false + wasConditional.value = false + + while (true) { + eatWhiteSpace() + + if (endOfStream) { + return null + } + + if (!eatCPPComment()) { + break + } + } + + if (endOfStream) { + return null + } + + var next = peek.toChar() + if (next == '"') { + wasQuoted.value = true + + // " + read() + + sb.clear() + while (!endOfStream) { + if (peek.toChar() == '\\') { + read() + + val escapedChar = read().toChar() + var replacedChar = ESCAPED_MAPPING[escapedChar] ?: escapedChar + + sb.append(replacedChar) + + continue + } + + if (peek.toChar() == '"') { + break + } + + sb.append(read().toChar()) + } + + // " + read() + + return sb.toString() + } + + if (next == '{' || next == '}') { + read() + return next.toString() + } + + var bConditionalStart = false + val count = 0 + sb.clear() + while (!endOfStream) { + next = peek.toChar() + + if (next == '"' || next == '{' || next == '}') { + break + } + + if (next == '[') { + bConditionalStart = true + } + + if (next == ']' && bConditionalStart) { + wasConditional.value = true + } + + if (next.isWhitespace()) { + break + } + + // count isn't used anymore, but still defined in SK. + @Suppress("KotlinConstantConditions") + if (count < 1023) { + sb.append(next) + } else { + throw IOException("ReadToken overflow") + } + + read() + } + + return sb.toString() + } +} diff --git a/src/main/java/in/dragonbra/javasteam/types/KeyValue.java b/src/main/java/in/dragonbra/javasteam/types/KeyValue.java index 91709bf0..d5f10dbc 100644 --- a/src/main/java/in/dragonbra/javasteam/types/KeyValue.java +++ b/src/main/java/in/dragonbra/javasteam/types/KeyValue.java @@ -442,6 +442,7 @@ private static KeyValue loadFromFile(String path, boolean asBinary) { return null; } + // TODO charsets? try (FileInputStream fis = new FileInputStream(file)) { // Massage the incoming file to be encoded as UTF-8. String fisString = IOUtils.toString(fis, Charset.defaultCharset()); @@ -605,9 +606,10 @@ private static void writeString(OutputStream os, String str, boolean quote) thro * * @param is The input {@link InputStream} to read from. * @return true if the read was successful; otherwise, false. - * @throws IOException exception while reading from the stream + * @throws IOException exception while reading from the stream + * @throws IllegalArgumentException exception while reading from the stream */ - public boolean tryReadAsBinary(InputStream is) throws IOException { + public boolean tryReadAsBinary(InputStream is) throws IllegalArgumentException, IOException { if (is == null) { throw new IllegalArgumentException("input stream is null"); } diff --git a/src/main/java/in/dragonbra/javasteam/util/EResultDeserializer.java b/src/main/java/in/dragonbra/javasteam/util/EResultDeserializer.java deleted file mode 100644 index d8ea59df..00000000 --- a/src/main/java/in/dragonbra/javasteam/util/EResultDeserializer.java +++ /dev/null @@ -1,22 +0,0 @@ -package in.dragonbra.javasteam.util; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import in.dragonbra.javasteam.enums.EResult; - -import java.lang.reflect.Type; - -/** - * @author lngtr - * @since 2018-02-20 - */ -public class EResultDeserializer implements JsonDeserializer { - - @Override - public EResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - int code = json.getAsInt(); - return EResult.from(code); - } -} diff --git a/src/main/java/in/dragonbra/javasteam/util/NetHelpers.java b/src/main/java/in/dragonbra/javasteam/util/NetHelpers.java deleted file mode 100644 index 01b00e27..00000000 --- a/src/main/java/in/dragonbra/javasteam/util/NetHelpers.java +++ /dev/null @@ -1,56 +0,0 @@ -package in.dragonbra.javasteam.util; - -import org.apache.commons.validator.routines.InetAddressValidator; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; - -/** - * @author lngtr - * @since 2018-02-22 - */ -public class NetHelpers { - - public static InetAddress getIPAddress(int ipAddr) { - ByteBuffer b = ByteBuffer.allocate(4); - b.putInt(ipAddr); - - byte[] result = b.array(); - - try { - return InetAddress.getByAddress(result); - } catch (UnknownHostException e) { - return null; - } - } - - public static int getIPAddress(InetAddress ip) { - final ByteBuffer buff = ByteBuffer.wrap(ip.getAddress()); - return (int) (buff.getInt() & 0xFFFFFFFFL); - } - - - public static InetSocketAddress tryParseIPEndPoint(String address) { - if (address == null) { - return null; - } - - String[] split = address.split(":"); - - if (!InetAddressValidator.getInstance().isValidInet4Address(split[0])) { - return null; - } - - try { - if (split.length > 1) { - return new InetSocketAddress(split[0], Integer.parseInt(split[1])); - } - } catch (IllegalArgumentException exception) { - // no-op - } - - return null; - } -} diff --git a/src/main/java/in/dragonbra/javasteam/util/NetHelpers.kt b/src/main/java/in/dragonbra/javasteam/util/NetHelpers.kt new file mode 100644 index 00000000..cc1e502c --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/NetHelpers.kt @@ -0,0 +1,125 @@ +package `in`.dragonbra.javasteam.util + +import com.google.protobuf.ByteString +import `in`.dragonbra.javasteam.generated.MsgClientLogon +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesBase.CMsgIPAddress +import org.apache.commons.validator.routines.InetAddressValidator +import java.lang.IllegalArgumentException +import java.net.DatagramSocket +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.nio.ByteBuffer + +/** + * @author lngtr + * @since 2018-02-22 + */ +object NetHelpers { + + @JvmStatic + @Throws(IllegalArgumentException::class) + fun getIPAddress(ipAddr: InetAddress): Int { + require(ipAddr.address.size == 4) { "only works with IPv4 addresses." } + + return ByteBuffer.wrap(ipAddr.address).int // and 0xFFFFFFFFL.toInt() + } + + @JvmStatic + @Throws(Exception::class) + fun getLocalIP(socket: Socket): InetAddress? = getLocalIP(socket.localAddress) + + @JvmStatic + @Throws(IllegalArgumentException::class) + fun getLocalIP(datagramSocket: DatagramSocket): InetAddress? = getLocalIP(datagramSocket.localAddress) + + @JvmStatic + fun getLocalIP(endpoint: InetAddress?): InetAddress? { + if (endpoint == null || endpoint.address == InetAddress.getByName("0.0.0.0")) { + return null + } + + return endpoint + } + + @JvmStatic + fun getIPAddress(ipAddr: Int): InetAddress { + val result = byteArrayOf( + ((ipAddr shr 24) and 0xFF).toByte(), + ((ipAddr shr 16) and 0xFF).toByte(), + ((ipAddr shr 8) and 0xFF).toByte(), + (ipAddr and 0xFF).toByte() + ) + return InetAddress.getByAddress(result) + } + + @JvmStatic + fun getIPAddress(ipAddr: CMsgIPAddress): InetAddress { + return if (ipAddr.hasV6()) { + InetAddress.getByAddress(ipAddr.v6.toByteArray()) + } else { + getIPAddress(ipAddr.v4) + } + } + + @JvmStatic + fun getMsgIPAddress(ipAddr: InetAddress): CMsgIPAddress { + val msgIpAddress = CMsgIPAddress.newBuilder() + + if (ipAddr is Inet6Address) { + msgIpAddress.v6 = ByteString.copyFrom(ipAddr.address) + } else { + msgIpAddress.v4 = getIPAddress(ipAddr) + } + + return msgIpAddress.build() + } + + @JvmStatic + fun obfuscatePrivateIP(msgIpAddress: CMsgIPAddress): CMsgIPAddress { + val localIp = msgIpAddress.toBuilder() + + if (localIp.hasV6()) { + // TODO v6 validation + val v6Bytes = msgIpAddress.v6.toByteArray() + for (i in 0..15 step 4) { + v6Bytes[i] = (v6Bytes[i].toInt() xor 0x0D).toByte() + v6Bytes[i + 1] = (v6Bytes[i + 1].toInt() xor 0xF0).toByte() + v6Bytes[i + 2] = (v6Bytes[i + 2].toInt() xor 0xAD).toByte() + v6Bytes[i + 3] = (v6Bytes[i + 3].toInt() xor 0xBA).toByte() + } + localIp.v6 = ByteString.copyFrom(v6Bytes) + } else { + localIp.v4 = msgIpAddress.v4 xor MsgClientLogon.ObfuscationMask + } + + return localIp.build() + } + + @JvmStatic + fun tryParseIPEndPoint(stringValue: String): InetSocketAddress? { + try { + val split = stringValue.lastIndexOf(':') + if (split == -1) { + return null + } + + var ip = stringValue.substring(0, split) + val port = stringValue.substring(split + 1).toInt() + + if (ip.startsWith("[") && ip.endsWith("]")) { + ip = ip.substring(1, ip.length - 1) // Remove the brackets + } + + val validator = InetAddressValidator.getInstance() + return if (validator.isValidInet4Address(ip) || validator.isValidInet6Address(ip)) { + InetSocketAddress(ip, port) + } else { + null + } + } catch (_: Exception) { + return null + } + } +} diff --git a/src/main/java/in/dragonbra/javasteam/util/Passable.java b/src/main/java/in/dragonbra/javasteam/util/Passable.java deleted file mode 100644 index 9931e930..00000000 --- a/src/main/java/in/dragonbra/javasteam/util/Passable.java +++ /dev/null @@ -1,22 +0,0 @@ -package in.dragonbra.javasteam.util; - -public class Passable { - - private T value; - - public Passable() { - this(null); - } - - public Passable(T value) { - this.value = value; - } - - public T getValue() { - return value; - } - - public void setValue(T value) { - this.value = value; - } -} diff --git a/src/main/java/in/dragonbra/javasteam/util/Passable.kt b/src/main/java/in/dragonbra/javasteam/util/Passable.kt new file mode 100644 index 00000000..54469cfd --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/Passable.kt @@ -0,0 +1,3 @@ +package `in`.dragonbra.javasteam.util + +class Passable @JvmOverloads constructor(var value: T? = null) diff --git a/src/main/java/in/dragonbra/javasteam/util/Utils.java b/src/main/java/in/dragonbra/javasteam/util/Utils.java index 6b0c1ed6..3906ba41 100644 --- a/src/main/java/in/dragonbra/javasteam/util/Utils.java +++ b/src/main/java/in/dragonbra/javasteam/util/Utils.java @@ -58,7 +58,8 @@ public class Utils { OSX_OS_MAP.put(SystemUtils.IS_OS_MAC_OSX_BIG_SUR, EOSType.MacOS11); OSX_OS_MAP.put(SystemUtils.IS_OS_MAC_OSX_MONTEREY, EOSType.MacOS12); OSX_OS_MAP.put(SystemUtils.IS_OS_MAC_OSX_VENTURA, EOSType.MacOS13); - OSX_OS_MAP.put(checkOS("Mac OS X", "14"), EOSType.MacOS14); + OSX_OS_MAP.put(SystemUtils.IS_OS_MAC_OSX_SONOMA, EOSType.MacOS14); + OSX_OS_MAP.put(checkOS("Mac OS X", "15"), EOSType.MacOS15); LINUX_OS_MAP.put("2.2", EOSType.Linux22); LINUX_OS_MAP.put("2.4", EOSType.Linux24); diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java index 1421c54c..960561e1 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java @@ -2,6 +2,7 @@ import java.io.*; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * Basically DataInputStream, but the bytes are parsed in reverse order @@ -99,8 +100,9 @@ public boolean readBoolean() throws IOException { position += 1; return ch != 0; } + public String readNullTermString() throws IOException { - return readNullTermString(Charset.defaultCharset()); + return readNullTermString(StandardCharsets.UTF_8); } public String readNullTermString(Charset charset) throws IOException { @@ -108,6 +110,10 @@ public String readNullTermString(Charset charset) throws IOException { throw new IOException("charset is null"); } + if (charset.equals(StandardCharsets.UTF_8)) { + return readNullTermUtf8String(); + } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(0); BinaryWriter bw = new BinaryWriter(buffer); @@ -126,7 +132,24 @@ public String readNullTermString(Charset charset) throws IOException { 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 baos.toString(StandardCharsets.UTF_8); + } + public int getPosition() { return position; } -} \ No newline at end of file +} diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java b/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java index 395f9f6d..f3c687d5 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java @@ -1,5 +1,7 @@ package in.dragonbra.javasteam.util.stream; +import com.google.protobuf.ByteString; + import java.io.Closeable; import java.io.InputStream; import java.io.OutputStream; @@ -46,6 +48,17 @@ public MemoryStream(int capacity) { this.bufferVisible = true; } + /** + * Initializes a new non-resizable instance of the MemoryStream class based on the + * specified byte array. + * + * @param byteString The sequence of bytes from which to create the current stream. + */ + public MemoryStream(ByteString byteString) { + this(byteString.toByteArray(), true); + } + + /** * Initializes a new non-resizable instance of the MemoryStream class based on the * specified byte array. @@ -392,4 +405,4 @@ public void write(int b) { memoryStream.writeByte((byte) b); } } -} \ No newline at end of file +} diff --git a/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java b/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java index aabdecd7..07912113 100644 --- a/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java +++ b/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java @@ -54,7 +54,7 @@ public void load() { server.shutdown(); } catch (Exception e) { - fail(e.getMessage()); + fail(e); } } } diff --git a/src/test/java/in/dragonbra/javasteam/util/CollectionsTest.java b/src/test/java/in/dragonbra/javasteam/util/CollectionsTest.java new file mode 100644 index 00000000..ca415f9a --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/CollectionsTest.java @@ -0,0 +1,35 @@ +package in.dragonbra.javasteam.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +public class CollectionsTest { + + private HashMap values; + + @BeforeEach + public void setUp() { + values = new HashMap<>(); + values.put(1, "Some Value"); + values.put(2, "A Value"); + values.put(3, "All the values"); + } + + @Test + public void getKeyByValueExistingValue() { + var result = CollectionUtils.getKeyByValue(values, "A Value"); + Assertions.assertEquals(2, result); + } + + @Test + public void getKeyByValueNullValue() { + var nullResult = CollectionUtils.getKeyByValue(values, "Null Value"); + Assertions.assertNull(nullResult); + + var nullResult2 = CollectionUtils.getKeyByValue(values, null); + Assertions.assertNull(nullResult2); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/MsgUtilTest.java b/src/test/java/in/dragonbra/javasteam/util/MsgUtilTest.java new file mode 100644 index 00000000..8014cc29 --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/MsgUtilTest.java @@ -0,0 +1,38 @@ +package in.dragonbra.javasteam.util; + +import in.dragonbra.javasteam.enums.EMsg; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MsgUtilTest { + + @Test + public void isProtoBuf() { + var result = MsgUtil.isProtoBuf(-2147482868); // ClientLicenseList + Assertions.assertTrue(result); + } + + @Test + public void isNotProtoBuf() { + var result = MsgUtil.isProtoBuf(798); // ClientUpdateGuestPassesList + Assertions.assertFalse(result); + } + + @Test + public void getMsgAsServiceMethodResponse() { + var result = MsgUtil.getMsg(-2147483501); // ServiceMethodResponse + Assertions.assertEquals(EMsg.ServiceMethodResponse, result); + } + + @Test + public void getMsgAsClientUpdateGuestPassesList() { + var result = MsgUtil.getMsg(798); // ClientUpdateGuestPassesList + Assertions.assertEquals(EMsg.ClientUpdateGuestPassesList, result); + } + + @Test + public void getMsgAsWrongMsg() { + var result = MsgUtil.getMsg(-2147483501); // ServiceMethodResponse + Assertions.assertNotEquals(EMsg.ClientUpdateGuestPassesList, result); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/NetHelpersTest.java b/src/test/java/in/dragonbra/javasteam/util/NetHelpersTest.java index 35444d23..00b4a5af 100644 --- a/src/test/java/in/dragonbra/javasteam/util/NetHelpersTest.java +++ b/src/test/java/in/dragonbra/javasteam/util/NetHelpersTest.java @@ -1,25 +1,111 @@ package in.dragonbra.javasteam.util; import in.dragonbra.javasteam.TestBase; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.UnknownHostException; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class NetHelpersTest extends TestBase { + private static final InetAddress loopbackV4 = InetAddress.getLoopbackAddress(); + private static final InetAddress loopbackV6; + + static { + try { + loopbackV6 = InetAddress.getByName("::1"); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Test + public void getMsgIPAddress() { + byte[] ipv6Bytes = { + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1 + }; + + Assertions.assertEquals(2130706433, NetHelpers.getMsgIPAddress(loopbackV4).getV4()); + Assertions.assertArrayEquals(ipv6Bytes, NetHelpers.getMsgIPAddress(loopbackV6).getV6().toByteArray()); + } + + @Test + public void getIPAddressFromMsg() { + var v4Addr = NetHelpers.getMsgIPAddress(loopbackV4); + Assertions.assertEquals(loopbackV4, NetHelpers.getIPAddress(v4Addr)); + + var v6Addr = NetHelpers.getMsgIPAddress(loopbackV6); + Assertions.assertEquals(loopbackV6, NetHelpers.getIPAddress(v6Addr)); + } + + @Test + public void getIPAddress() throws UnknownHostException { + Assertions.assertEquals(loopbackV4, NetHelpers.getIPAddress(2130706433)); + Assertions.assertEquals(InetAddress.getByName("0.0.0.1"), NetHelpers.getIPAddress(1)); + Assertions.assertEquals(InetAddress.getByName("255.255.255.255"), NetHelpers.getIPAddress(0xFFFFFFFF)); + Assertions.assertEquals(InetAddress.getByName("0.0.0.0"), NetHelpers.getIPAddress(0)); + } + + @Test + public void getIPAddressAsInt() throws UnknownHostException { + Assertions.assertEquals(2130706433, NetHelpers.getIPAddress(loopbackV4)); + Assertions.assertEquals(1, NetHelpers.getIPAddress(InetAddress.getByName("0.0.0.1"))); + Assertions.assertEquals(0xFFFFFFFF, NetHelpers.getIPAddress(InetAddress.getByName("255.255.255.255"))); + Assertions.assertEquals(-1062731775, NetHelpers.getIPAddress(InetAddress.getByName("192.168.0.1"))); + Assertions.assertEquals(167772161, NetHelpers.getIPAddress(InetAddress.getByName("10.0.0.1"))); + Assertions.assertEquals(-1408237567, NetHelpers.getIPAddress(InetAddress.getByName("172.16.0.1"))); + } + + @Test + public void obfuscatePrivateIP() { + byte[] ipv6Bytes = { + (byte) 0x0D, (byte) 0xF0, (byte) 0xAD, (byte) 0xBA, + (byte) 0x0D, (byte) 0xF0, (byte) 0xAD, (byte) 0xBA, + (byte) 0x0D, (byte) 0xF0, (byte) 0xAD, (byte) 0xBA, + (byte) 0x0D, (byte) 0xF0, (byte) 0xAD, (byte) 0xBB + }; + + var v4Addr = NetHelpers.getMsgIPAddress(loopbackV4); + Assertions.assertEquals(3316510732L, NetHelpers.obfuscatePrivateIP(v4Addr).getV4() & 0xFFFFFFFFL); + var v6Addr = NetHelpers.getMsgIPAddress(loopbackV6); + Assertions.assertArrayEquals(ipv6Bytes, NetHelpers.obfuscatePrivateIP(v6Addr).getV6().toByteArray()); + } + + @Test + public void tryParseIPEndPoint() throws UnknownHostException { + var v4Addr = NetHelpers.tryParseIPEndPoint("127.0.0.1:1337"); + Assertions.assertNotNull(v4Addr); + Assertions.assertEquals(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1337), v4Addr); + + var v6Addr = NetHelpers.tryParseIPEndPoint("[::1]:1337"); + Assertions.assertNotNull(v6Addr); + Assertions.assertEquals(new InetSocketAddress(loopbackV6, 1337), v6Addr); + + var v6Addr2 = NetHelpers.tryParseIPEndPoint("::1:1337"); + Assertions.assertNotNull(v6Addr2); + Assertions.assertEquals(new InetSocketAddress(loopbackV6, 1337), v6Addr2); + } + + @Test + public void fail_getIPAddressAsLong_If_V6() { + Assertions.assertThrows(IllegalArgumentException.class, () -> NetHelpers.getIPAddress(loopbackV6)); + } + @Test public void testIntToAddress() throws UnknownHostException { int ipAddress = -1560361686; InetAddress address = NetHelpers.getIPAddress(ipAddress); - assertEquals(InetAddress.getByName("162.254.197.42"), address); + Assertions.assertEquals(InetAddress.getByName("162.254.197.42"), address); } @Test public void testAddressToInt() throws UnknownHostException { int address = NetHelpers.getIPAddress(InetAddress.getByName("162.254.197.42")); - assertEquals(-1560361686, address); + Assertions.assertEquals(-1560361686, address); } -} \ No newline at end of file +} diff --git a/src/test/java/in/dragonbra/javasteam/util/PassableTest.java b/src/test/java/in/dragonbra/javasteam/util/PassableTest.java new file mode 100644 index 00000000..e6b7e791 --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/PassableTest.java @@ -0,0 +1,33 @@ +package in.dragonbra.javasteam.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PassableTest { + + @Test + public void BooleanPassable() { + var passableValue = new Passable<>(false); + + Assertions.assertFalse(passableValue.getValue()); + + passableValue.setValue(true); + Assertions.assertTrue(passableValue.getValue()); + + passableValue.setValue(null); + Assertions.assertNull(passableValue.getValue()); + } + + @Test + public void IntegerPassable() { + var passableValue = new Passable(null); + + Assertions.assertNull(passableValue.getValue()); + + passableValue.setValue(1); + Assertions.assertEquals(1, passableValue.getValue()); + + passableValue.setValue(2); + Assertions.assertEquals(2, passableValue.getValue()); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/StringsTest.java b/src/test/java/in/dragonbra/javasteam/util/StringsTest.java new file mode 100644 index 00000000..6cfae69b --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/StringsTest.java @@ -0,0 +1,31 @@ +package in.dragonbra.javasteam.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class StringsTest { + + @Test + public void toHex() { + byte[] byteArray = {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}; + Assertions.assertEquals("DEADBEEF", Strings.toHex(byteArray)); + } + + @Test + public void decodeHex() { + byte[] byteArray = {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}; + Assertions.assertArrayEquals(byteArray, Strings.decodeHex("deadbeef")); + } + + @Test + public void invalid_decodeHex() { + Assertions.assertThrows(StringIndexOutOfBoundsException.class, () -> Strings.decodeHex("odd")); + } + + @Test + public void isNullOrEmpty() { + Assertions.assertTrue(Strings.isNullOrEmpty(null)); + Assertions.assertTrue(Strings.isNullOrEmpty("")); + Assertions.assertFalse(Strings.isNullOrEmpty("Hello World!")); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/UtilsTest.java b/src/test/java/in/dragonbra/javasteam/util/UtilsTest.java index eb39853d..3fdf40cc 100644 --- a/src/test/java/in/dragonbra/javasteam/util/UtilsTest.java +++ b/src/test/java/in/dragonbra/javasteam/util/UtilsTest.java @@ -1,9 +1,8 @@ package in.dragonbra.javasteam.util; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * @author lngtr * @since 2019-01-15 @@ -17,6 +16,6 @@ public class UtilsTest { @Test public void crc32() { long result = Utils.crc32("test_string"); - assertEquals(0x0967B587, result); + Assertions.assertEquals(0x0967B587, result); } } diff --git a/src/test/java/in/dragonbra/javasteam/util/WebHelpersTest.java b/src/test/java/in/dragonbra/javasteam/util/WebHelpersTest.java index 0056cf89..e440649d 100644 --- a/src/test/java/in/dragonbra/javasteam/util/WebHelpersTest.java +++ b/src/test/java/in/dragonbra/javasteam/util/WebHelpersTest.java @@ -1,9 +1,9 @@ package in.dragonbra.javasteam.util; import in.dragonbra.javasteam.TestBase; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author lngtr @@ -12,8 +12,15 @@ public class WebHelpersTest extends TestBase { @Test - public void urlEncode() { + public void urlEncodeWithString() { String result = WebHelpers.urlEncode("encrypt THIS sTrInG1234 \10 \11 \12"); - assertEquals("encrypt+THIS+sTrInG1234+%08+%09+%0A", result); + Assertions.assertEquals("encrypt+THIS+sTrInG1234+%08+%09+%0A", result); } -} \ No newline at end of file + + @Test + public void urlEncodeWithByteArray() { + var input = "encrypt THIS sTrInG1234 \10 \11 \12".getBytes(); + String result = WebHelpers.urlEncode(input); + Assertions.assertEquals("encrypt+THIS+sTrInG1234+%08+%09+%0A", result); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/compat/ConsumerTest.java b/src/test/java/in/dragonbra/javasteam/util/compat/ConsumerTest.java new file mode 100644 index 00000000..e0983aaa --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/compat/ConsumerTest.java @@ -0,0 +1,21 @@ +package in.dragonbra.javasteam.util.compat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ConsumerTest { + + @Test + void testConsumerString() { + Consumer consumer = s -> Assertions.assertEquals("Test String", s); + + consumer.accept("Test String"); + } + + @Test + void testConsumerBoolean() { + Consumer consumer = b -> Assertions.assertEquals(true, b); + + consumer.accept(true); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/crypto/RSACryptoTest.java b/src/test/java/in/dragonbra/javasteam/util/crypto/RSACryptoTest.java new file mode 100644 index 00000000..a226fd17 --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/crypto/RSACryptoTest.java @@ -0,0 +1,37 @@ +package in.dragonbra.javasteam.util.crypto; + +import in.dragonbra.javasteam.enums.EUniverse; +import in.dragonbra.javasteam.util.KeyDictionary; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public class RSACryptoTest { + + private RSACrypto rsaCrypto; + + @BeforeEach + public void setUp() { + var pubKey = KeyDictionary.getPublicKey(EUniverse.Public); + rsaCrypto = new RSACrypto(pubKey); + } + + @Test + public void encrypt() { + var input = CryptoHelper.generateRandomBlock(32); + var encrypted = rsaCrypto.encrypt(input); + + Assertions.assertNotNull(encrypted); + } + + @Test + public void cipherInstance() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + var cipher = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding", CryptoHelper.SEC_PROV); + Assertions.assertNotNull(cipher); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/event/EventTest.java b/src/test/java/in/dragonbra/javasteam/util/event/EventTest.java new file mode 100644 index 00000000..bb066c62 --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/event/EventTest.java @@ -0,0 +1,47 @@ +package in.dragonbra.javasteam.util.event; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class EventTest { + + static class TestEventHandler implements EventHandler { + boolean eventHandled = false; + Object sender = null; + EventArgs eventArgs = null; + + @Override + public void handleEvent(Object sender, EventArgs e) { + this.eventHandled = true; + this.sender = sender; + this.eventArgs = e; + } + } + + @Test + void addEventHandlerAndHandleEvent() { + var event = new Event<>(); + var handler = new TestEventHandler(); + + event.addEventHandler(handler); + + event.handleEvent(this, EventArgs.EMPTY); + + Assertions.assertTrue(handler.eventHandled); + Assertions.assertEquals(this, handler.sender); + Assertions.assertEquals(EventArgs.EMPTY, handler.eventArgs); + } + + @Test + void removeEventHandler() { + var event = new Event<>(); + var handler = new TestEventHandler(); + + event.addEventHandler(handler); + event.removeEventHandler(handler); + + event.handleEvent(this, EventArgs.EMPTY); + + Assertions.assertFalse(handler.eventHandled); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/stream/BinaryReaderTest.java b/src/test/java/in/dragonbra/javasteam/util/stream/BinaryReaderTest.java new file mode 100644 index 00000000..76ef7e44 --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/stream/BinaryReaderTest.java @@ -0,0 +1,167 @@ +package in.dragonbra.javasteam.util.stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class BinaryReaderTest { + + private BinaryReader binaryReader; + + @Test + void testReadInt() throws IOException { + byte[] data = {1, 0, 0, 0}; // 1 in little-endian + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readInt(); + + Assertions.assertEquals(1, result); + Assertions.assertEquals(4, binaryReader.getPosition()); + } + + @Test + void testReadBytes() throws IOException { + byte[] data = {0x01, 0x02, 0x03, 0x04}; + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readBytes(4); + + Assertions.assertArrayEquals(data, result); + Assertions.assertEquals(4, binaryReader.getPosition()); + } + + @Test + void testReadShort() throws IOException { + byte[] data = {0x01, 0x00}; // 1 in little-endian + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readShort(); + + Assertions.assertEquals(1, result); + Assertions.assertEquals(2, binaryReader.getPosition()); + } + + @Test + void testReadLong() throws IOException { + byte[] data = {1, 0, 0, 0, 0, 0, 0, 0}; // 1 in little-endian + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readLong(); + + Assertions.assertEquals(1L, result); + Assertions.assertEquals(8, binaryReader.getPosition()); + } + + @Test + void testReadChar() throws IOException { + byte[] data = {65}; // ASCII 'A' + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readChar(); + + Assertions.assertEquals('A', result); + Assertions.assertEquals(1, binaryReader.getPosition()); + } + + @Test + void testReadBoolean() throws IOException { + byte[] data = {1}; // true + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readBoolean(); + + Assertions.assertTrue(result); + Assertions.assertEquals(1, binaryReader.getPosition()); + } + + @Test + void testReadNullTermUtf8String() throws IOException { + var string = "Hello\0"; + var data = string.getBytes(StandardCharsets.UTF_8); + + binaryReader = new BinaryReader(new MemoryStream(data)); + + Assertions.assertEquals("Hello", binaryReader.readNullTermString()); + Assertions.assertEquals(6, binaryReader.getPosition()); + } + + @Test + void testReadNullTermUtf8StringWithSpaces() throws IOException { + var string = "Hello World With Spaces\0"; + var data = string.getBytes(StandardCharsets.UTF_8); + + binaryReader = new BinaryReader(new MemoryStream(data)); + + Assertions.assertEquals("Hello World With Spaces", binaryReader.readNullTermString()); + Assertions.assertEquals(24, binaryReader.getPosition()); + } + + @Test + void testReadFloat() throws IOException { + int floatAsInt = Float.floatToIntBits(3.14f); + byte[] data = { + (byte) (floatAsInt & 0xff), + (byte) ((floatAsInt >> 8) & 0xff), + (byte) ((floatAsInt >> 16) & 0xff), + (byte) ((floatAsInt >> 24) & 0xff) + }; + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readFloat(); + + Assertions.assertEquals(3.14f, result, 0.001); + Assertions.assertEquals(4, binaryReader.getPosition()); + } + + @Test + void testReadDouble() throws IOException { + var doubleAsLong = Double.doubleToLongBits(3.14159); + byte[] data = { + (byte) (doubleAsLong & 0xff), + (byte) ((doubleAsLong >> 8) & 0xff), + (byte) ((doubleAsLong >> 16) & 0xff), + (byte) ((doubleAsLong >> 24) & 0xff), + (byte) ((doubleAsLong >> 32) & 0xff), + (byte) ((doubleAsLong >> 40) & 0xff), + (byte) ((doubleAsLong >> 48) & 0xff), + (byte) ((doubleAsLong >> 56) & 0xff) + }; + + binaryReader = new BinaryReader(new MemoryStream(data)); + + var result = binaryReader.readDouble(); + + Assertions.assertEquals(3.14159, result, 0.00001); + Assertions.assertEquals(8, binaryReader.getPosition()); + } + + @Test + void testEOFException() { + byte[] data = {}; + binaryReader = new BinaryReader(new MemoryStream(data)); + + Assertions.assertThrows(EOFException.class, () -> binaryReader.readBoolean()); + Assertions.assertThrows(EOFException.class, () -> binaryReader.readByte()); + Assertions.assertThrows(EOFException.class, () -> binaryReader.readChar()); + Assertions.assertThrows(EOFException.class, () -> binaryReader.readInt()); + Assertions.assertThrows(EOFException.class, () -> binaryReader.readShort()); + } + + @Test + void testEmptyNullTermString() throws IOException { + byte[] data = {}; + binaryReader = new BinaryReader(new MemoryStream(data)); + + Assertions.assertEquals("", binaryReader.readNullTermString()); + } +} diff --git a/src/test/java/in/dragonbra/javasteam/util/stream/BinaryWriterTest.java b/src/test/java/in/dragonbra/javasteam/util/stream/BinaryWriterTest.java new file mode 100644 index 00000000..2028b77e --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/stream/BinaryWriterTest.java @@ -0,0 +1,92 @@ +package in.dragonbra.javasteam.util.stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class BinaryWriterTest { + + private MemoryStream memoryStream; + private BinaryWriter binaryWriter; + + @BeforeEach + void setUp() { + memoryStream = new MemoryStream(); + binaryWriter = new BinaryWriter(memoryStream.asOutputStream()); + } + + @Test + void testWriteInt() throws IOException { + binaryWriter.writeInt(1); // 1 should be written as [01 00 00 00] + + byte[] expected = {0x01, 0x00, 0x00, 0x00}; + + Assertions.assertArrayEquals(expected, memoryStream.toByteArray()); + } + + @Test + void testWriteShort() throws IOException { + binaryWriter.writeShort((short) 1); // 1 should be written as [01 00] + + byte[] expected = {0x01, 0x00}; + + Assertions.assertArrayEquals(expected, memoryStream.toByteArray()); + } + + @Test + void testWriteLong() throws IOException { + binaryWriter.writeLong(1L); // 1 should be written as [01 00 00 00 00 00 00 00] + + byte[] expected = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + Assertions.assertArrayEquals(expected, memoryStream.toByteArray()); + } + + @Test + void testWriteFloat() throws IOException { + binaryWriter.writeFloat(3.14f); + + var ms = new MemoryStream(memoryStream.toByteArray()); + var br = new BinaryReader(ms); + + Assertions.assertEquals(3.14f, br.readFloat()); + } + + @Test + void testWriteDouble() throws IOException { + binaryWriter.writeDouble(3.14159); + + var ms = new MemoryStream(memoryStream.toByteArray()); + var br = new BinaryReader(ms); + + Assertions.assertEquals(3.14159, br.readDouble(), 0.001); + } + + @Test + void testWriteBoolean() throws IOException { + binaryWriter.writeBoolean(true); + + Assertions.assertArrayEquals(new byte[]{1}, memoryStream.toByteArray()); + + memoryStream.reset(); // Reset stream for next test + binaryWriter.writeBoolean(false); + + Assertions.assertArrayEquals(new byte[]{0}, memoryStream.toByteArray()); + } + + @Test + void testWriteByte() throws IOException { + binaryWriter.writeByte((byte) 0xAB); + + Assertions.assertArrayEquals(new byte[]{(byte) 0xAB}, memoryStream.toByteArray()); + } + + @Test + void testWriteChar() throws IOException { + binaryWriter.writeChar('A'); // ASCII value of 'A' is 65 + + Assertions.assertArrayEquals(new byte[]{65}, memoryStream.toByteArray()); + } +}