diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3e665d --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +# Created by .ignore support plugin (hsz.mobi) +### Eclipse template +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + + + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + + + + +### Project escape directories/files +/.idea/ +/target/ +*.iml + +# Making private tests +/src/test/java/com/antonio112009/steam4j/privatetest/ +Notes.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9be5ed9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Anton Rogalskiy and Steam4J contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..43964fe --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +add Java 15v +add MIT Lisence +add .. + + +# Steam4J +Steam4J is an open-source project that strives provides full wrapping of game server queries. + +## Navigation + +1. [Dependencies](##Dependencies) +1. [Dependencies](##Features) +1. [Dependencies](##Installation) + + +##Dependencies +This project requires JDK 11+ +Maven dependencies in the project: +* [Lombok](https://github.com/rzwitserloot/lombok): 1.18.16 + +##Features +The list of the features currently implemented in the project: +* [Rcon](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol): + * Authentication to server with IP and Port + * Executing commands to the server and receiving answer + * Receiving commands from the server **without sending commands**. Listing to game console + * Support of multi-package answers +* [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) + * A2S_INFO + * A2S_PLAYER + * A2S_RULES + * A2S_PING and A2S_SERVERQUERY_GETCHALLENGE are depricated + +##Installation +Download project from release page of the project + +or + +Add dependency to your project manager +**Maven**: +```xml + + com.antonio112009 + steam4j + 1.0.0 + +``` + +## Code examples + +### Rcon examples: +Execute single command: +```java +public class SendingSingleCommand { + + public static void main(String[] args) { + Rcon rcon = new Rcon("0.0.0.0", 21114); + rcon.openStream(); + boolean isAuthenticated = rcon.authenticate(password); + if(isAuthenticated) { + String answerFromServer = rcon.sendCommand("ListPlayers"); + System.out.println("Answer: " + answerFromServer); + } + rcon.closeStream(); //optional if it's in the end of the program + } + +} +``` + +Execute multiple command: +```java +public class SendingMultipleCommand { + + public static void main(String[] args) { + Rcon rcon = new Rcon("0.0.0.0", 21114); + rcon.openStream(); + boolean isAuthenticated = rcon.authenticate(password); + if(isAuthenticated) { + System.out.println("Answer1: " + rcon.sendCommand("ListPlayers")); + System.out.println("Answer2: " + rcon.sendCommand("GetCurrentScore")); + System.out.println("Answer3: " + rcon.sendCommand("ChatToAll Hello world!")); + System.out.println("Answer: " + answerFromServer); + } + rcon.closeStream(); //optional if it's in the end of the program + } + +} +``` + +Receiving non-stop messages from a server (no execution of commands). Usually console logs: +```java +public class ReceiveServerLogs { + + public static void main(String[] args) { + Rcon rcon = new Rcon("0.0.0.0", 21114); + rcon.openStream(); + boolean isAuthenticated = rcon.authenticate(password); + while (isAuthenticated) { + try { + System.out.println("Answer: " + rcon.receivingServerMessages()); + } catch (IOException e) { + e.printStackTrace(); + rcon.closeStream(); //optional if it's in the end of the program + break; + } + } + } + +} +``` + +### Server Queries (A2S): +All objects represent response format of data that are shown in details on the [official Valve developer page](https://developer.valvesoftware.com/wiki/Server_queries). + +### Server information +Getting information of the server. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO) +```java +public class GetServerInformation { + + public static void main(String[] args) { + A2s a2s = new A2S("0.0.0.0", 27166); + ServerInfo serverInfo = a2S.getServerInfo(); + System.out.println("Result:\n" + serverInfo.toString()); + } + +} +``` + +### Server players +Getting information of the server. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER) +```java +public class GetServerInformation { + + public static void main(String[] args) { + A2s a2s = new A2S("0.0.0.0", 27166); + Players players = a2S.getPlayers(); + System.out.println("Result:\n" + players.toString()); + } + +} +``` + +### Server rules +Getting information of the server. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES) +```java +public class GetServerInformation { + + public static void main(String[] args) { + A2s a2s = new A2S("0.0.0.0", 27166); + Rules rules = a2S.getRules(); + System.out.println("Result:\n" + rules.toString()); + } + +} +``` + +## Contribution +The contribution template and format is currently under development. Anyway, you are welcome to contribute this project. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6cdda5c --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.antonio112009 + steam4j + 1.0.0 + jar + + + UTF-8 + UTF-8 + 11 + 11 + + + + + org.projectlombok + lombok + 1.18.16 + + + + org.junit.jupiter + junit-jupiter + 5.7.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + + + + + github + GitHub Antonio112009 Apache Maven Packages + https://maven.pkg.github.com/Antonio112009/Steam4J + + + \ No newline at end of file diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/A2S.java b/src/main/java/com/antonio112009/steam4j/core/a2s/A2S.java new file mode 100644 index 0000000..482c865 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/A2S.java @@ -0,0 +1,313 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s; + +import com.antonio112009.steam4j.core.a2s.model.info.ServerInfo; +import com.antonio112009.steam4j.core.a2s.model.player.Player; +import com.antonio112009.steam4j.core.a2s.model.player.Players; +import com.antonio112009.steam4j.core.a2s.model.rule.Rule; +import com.antonio112009.steam4j.core.a2s.model.rule.Rules; +import com.antonio112009.steam4j.core.a2s.type.A2SType; +import com.antonio112009.steam4j.core.a2s.util.Converter; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.Arrays; + +public class A2S { + + private String serverIp; + private int port; + private DatagramSocket datagramSocket; + private int index; + + public A2S(String serverIp, int port) { + this.serverIp = serverIp; + this.port = port; + } + + + private byte[] sendData(byte[] data) throws IOException { + datagramSocket = new DatagramSocket(); + + InetAddress address = InetAddress.getByName(serverIp); + DatagramPacket datagramPacket = new DatagramPacket(data, data.length, address, port); + datagramSocket.setSoTimeout(500); + + datagramSocket.send(datagramPacket); + + // Steam uses a packet size of 1400 bytes + IP/UDP headers. + // If a request or response needs more packets for the data it starts the packets with an additional header. + byte[] receiveBuffer = new byte[1480]; + + DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length); + datagramSocket.receive(receivePacket); + return receivePacket.getData(); + } + + + /** + * GET SERVER RULES + */ + public Rules getRules() { + try { + byte[] data = sendData(Converter.hexStrToBinaryStr(A2SType.A2S_RULES)); + + // probably, store challenge in local variable + if(data[4] == (byte) 0x41) { + data = sendData(Converter.hexStrToBinaryStr(A2SType.A2S_RULES_REPLY + " " + Converter.binaryToString(Arrays.copyOfRange(data, 5,9)))); + } + + return convertResponseToRules(data); + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + + // First 4 elements from the array - are header. It shows if packet is split or not. + private Rules convertResponseToRules(byte[] data) { + index = 4; + + Rules rules = new Rules(); + rules.setHeader(Converter.hexToAscii(Converter.byteToHex(data[index++]))); + rules.setTotalRules(data[index++]); + index++; + + for (int i = 0; i < rules.getTotalRules(); i++) { + try { + Rule rule = new Rule(); + rule.setName(byteArrayToString(data)); + rule.setValue(byteArrayToString(data)); + rules.getRuleList().add(rule); + } catch (ArrayIndexOutOfBoundsException ex) { + ex.printStackTrace(); + } + } + + return rules; + } + + + + + + + + + + + + + + /** + * GET PLAYERS + */ + public Players getPlayers() { + try { + byte[] data = sendData(Converter.hexStrToBinaryStr(A2SType.A2S_PLAYER)); + + // probably, store challenge in local variable + if(data[4] == (byte) 0x41) { + data = sendData(Converter.hexStrToBinaryStr(A2SType.A2S_PLAYER_REPLY + " " + Converter.binaryToString(Arrays.copyOfRange(data, 5,9)))); + } + + return convertResponseToPlayers(data); + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + + + // First 4 elements from the array - are header. It shows if packet is split or not. + private Players convertResponseToPlayers(byte[] data) { + index = 4; + + Players players = new Players(); + players.setHeader(Converter.hexToAscii(Converter.byteToHex(data[index++]))); + players.setTotalPlayers(data[index++]); + + for (int i = 0; i < players.getTotalPlayers(); i++) { + try { + Player player = new Player(); + + player.setIndex(data[index++]); + player.setName(byteArrayToString(data)); + player.setScore(calculateLong(data)); + player.setDuration(calculateFloat(data)); + + players.getPlayerList().add(player); + } catch (ArrayIndexOutOfBoundsException ex) { + //TODO: add error handler + } + } + + return players; + } + + + + + + + + + /** + * GET SERVER INFO + * @return + */ + public ServerInfo getServerInfo() { + try { + byte[] data = sendData(Converter.hexStrToBinaryStr(A2SType.A2S_INFO)); + + if (data[4] == (byte) 0x6d) { +// oldSourceServer(resBytes, jsonObject); + } else { + return convertResponseToServerInfo(data, false); +// sourceServer(resBytes, jsonObject); + } + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + + // First 4 elements from the array - are header. It shows if packet is split or not. + private ServerInfo convertResponseToServerInfo(byte[] data, boolean addEDF) { + index = 4; + + ServerInfo serverInfo = new ServerInfo(); + + serverInfo.setHeader(Converter.hexToAscii(Converter.byteToHex(data[index++]))); + + // TODO: possibly getting wrong data + serverInfo.setProtocol(data[index++]); + serverInfo.setName(byteArrayToString(data)); + serverInfo.setMap(byteArrayToString(data)); + serverInfo.setFolder(byteArrayToString(data)); + serverInfo.setGame(byteArrayToString(data)); + + // space with 2 zero elements + index += 2; + serverInfo.setPlayers(data[index++]); + serverInfo.setMaxPlayers(data[index++]); + index++; + serverInfo.setServerType(Converter.hexToAscii(Converter.byteToHex(data[index++]))); + serverInfo.setEnvironment(Converter.hexToAscii(Converter.byteToHex(data[index++]))); + serverInfo.setVisibility(data[index++]); + serverInfo.setVac(data[index++]); + serverInfo.setVersion(byteArrayToString(data)); + + // TODO: modify later + // Work with EDF + while (data[index] != 0 && addEDF) { + switch (Converter.byteToHex(data[index++])) { + case "80": { + System.out.println("80"); + index++; + } + case "10": { + System.out.println("10"); + index++; + } + case "40": { + System.out.println("40"); + index++; + } + case "20": { + System.out.println("20"); + serverInfo.setKeywords(byteArrayToString(data)); + } + case "01": { + System.out.println("1"); + StringBuilder gameId = new StringBuilder(); + for (int i = 0; i < 4; i++) { + gameId.append(Converter.byteToHex(data[index + i])); + } + serverInfo.setGameId(Long.parseLong(gameId.toString(), 16)); + index += 5; + } + default: { + index++; + } + } + } + + + // adding data left in the message in case of broken query from developers of the game) + serverInfo.setRestDataLeft(Arrays.copyOfRange(data, index, data.length)); + return serverInfo; + } + + + + + + + + private String byteArrayToString (byte[] data) { + byte[] tmp = new byte[1000]; + int indexTmp = 0; + while (data[index] != 0) { + tmp[indexTmp] = data[index]; + indexTmp++; + index++; + } + index++; + return Converter.byteToString(tmp); + } + + private long calculateLong(byte[] data) { + long longi = 0; + for (int j = 0; j < 4; j++, index++) { + longi = longi | ((long) data[index] << 8 * j); + } + return longi; + } + + private float calculateFloat(byte[] data) { + byte[] tmp = new byte[4]; + for (int j = 0; j < 4; j++, index++) { + tmp[j] = data[index]; + } + + int accum = 0; + accum = accum | (tmp[0] & 0xff); + accum = accum | (tmp[1] & 0xff) << 8; + accum = accum | (tmp[2] & 0xff) << 16; + accum = accum | (tmp[3] & 0xff) << 24; + + return Float.intBitsToFloat(accum) * 1; + } +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/model/info/ServerInfo.java b/src/main/java/com/antonio112009/steam4j/core/a2s/model/info/ServerInfo.java new file mode 100644 index 0000000..83ee6b0 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/model/info/ServerInfo.java @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.model.info; + +import lombok.Data; + +@Data +public class ServerInfo { + + private String header; + private byte protocol; + private String name; + private String map; + private String folder; + private String game; + private short id; + private int players; + private int maxPlayers; + private int bots; + private String serverType; // https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO + private String environment; // https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO + private byte visibility; + private byte vac; + + // If server running "The Ship" + //TODO: add Ship support + + private String version; + + //Extra Data Flag (EDF) + private int serverGamePort; // 0x80 + private long steamId; // 0x10 + private long spectatorPort; // 0x40 + private long spectatorName; // 0x40 + private String keywords; // 0x20 + private long gameId; // 0x01 + + private byte[] restDataLeft; +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Player.java b/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Player.java new file mode 100644 index 0000000..a638c06 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Player.java @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.model.player; + +import lombok.Data; + +@Data +public class Player { + + private int index; + private String name; + private long score; + private float duration; +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Players.java b/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Players.java new file mode 100644 index 0000000..597b8ea --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/model/player/Players.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.model.player; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class Players { + + private String header; + private int totalPlayers; + private List playerList = new ArrayList<>(); +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rule.java b/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rule.java new file mode 100644 index 0000000..04d598c --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rule.java @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.model.rule; + +import lombok.Data; + +@Data +public class Rule { + private String name; + private String value; +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rules.java b/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rules.java new file mode 100644 index 0000000..fcb4cce --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/model/rule/Rules.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.model.rule; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class Rules { + + private String header; + private int totalRules; + List ruleList = new ArrayList<>(); +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/packet/A2SPacket.java b/src/main/java/com/antonio112009/steam4j/core/a2s/packet/A2SPacket.java new file mode 100644 index 0000000..3067e52 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/packet/A2SPacket.java @@ -0,0 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.packet; + +public class A2SPacket { +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/type/A2SType.java b/src/main/java/com/antonio112009/steam4j/core/a2s/type/A2SType.java new file mode 100644 index 0000000..0a3c796 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/type/A2SType.java @@ -0,0 +1,35 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.type; + +public class A2SType { + public static final String A2S_INFO = "FF FF FF FF 54 53 6F 75 72 63 65 20 45 6E 67 69 6E 65 20 51 75 65 72 79 00"; + public static final String A2S_PLAYER = "FF FF FF FF 55 FF FF FF FF"; + public static final String A2S_PLAYER_REPLY = "FF FF FF FF 55"; + public static final String A2S_RULES = "FF FF FF FF 56 FF FF FF FF"; + public static final String A2S_RULES_REPLY = "FF FF FF FF 56"; + public static final String A2S_SERVERQUERY_GETCHALLENGE = "FF FF FF FF 57"; + +} diff --git a/src/main/java/com/antonio112009/steam4j/core/a2s/util/Converter.java b/src/main/java/com/antonio112009/steam4j/core/a2s/util/Converter.java new file mode 100644 index 0000000..15cd471 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/a2s/util/Converter.java @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.a2s.util; + +public class Converter { + + public static String byteToString(byte[] src) { + try { + int length = 0; + for (int i = 0; i < src.length; ++i) { + if (src[i] == 0) { + length = i; + break; + } + } + return new String(src, 0, length, "UTF-8"); + } catch (Exception e) { +// log.error(e.toString()); + return ""; + } + } + + public static byte[] hexStrToBinaryStr(String hexString) { + if (hexString == null) return null; + + String[] tmp = hexString.split(" "); + byte[] tmpBytes = new byte[tmp.length]; + int i = 0; + for (String b : tmp) { + if (b.equals("FF")) { + tmpBytes[i++] = -1; + } else { + tmpBytes[i++] = Integer.valueOf(b, 16).byteValue(); + } + } + return tmpBytes; + } + + public static String binaryToString(byte[] data) { + try { + char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + StringBuilder stringBuffer = new StringBuilder(); + for (byte datum : data) { + stringBuffer.append(HEX_CHAR[(datum < 0 ? datum + 256 : datum) / 16]); + stringBuffer.append(HEX_CHAR[(datum < 0 ? datum + 256 : datum) % 16]); + stringBuffer.append(" "); + } + return stringBuffer.toString(); + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + return null; + } + + + public static String byteToHex(byte input) { + char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + return String.valueOf(HEX_CHAR[(input < 0 ? input + 256 : input) / 16]) + + HEX_CHAR[(input < 0 ? input + 256 : input) % 16]; + } + + + // POSSIBLY NO NEED!!! + public static String hexToAscii(String hexStr) { + StringBuilder output = new StringBuilder(); + + for (int i = 0; i < hexStr.length(); i += 2) { + String str = hexStr.substring(i, i + 2); + output.append((char) Integer.parseInt(str, 16)); + } + + return output.toString(); + } +} diff --git a/src/main/java/com/antonio112009/steam4j/core/rcon/Rcon.java b/src/main/java/com/antonio112009/steam4j/core/rcon/Rcon.java new file mode 100644 index 0000000..7436cfc --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/rcon/Rcon.java @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.rcon; + +import com.antonio112009.steam4j.core.rcon.packet.RconPacket; +import com.antonio112009.steam4j.core.rcon.type.RconPacketType; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; + +//@Slf4j +public class Rcon { + + private String serverIp; + private int serverPort; + + private Socket socket; + private static final int RESPONSE_TIMEOUT = 1000; + private boolean authenticated = false; + + public Rcon(String serverIp, int serverPort) { + this.serverIp = serverIp; + this.serverPort = serverPort; + } + + + public boolean openStream() { + try { + socket = new Socket(serverIp, serverPort); + socket.setSoTimeout(RESPONSE_TIMEOUT); + return socket.isConnected(); + + } catch (IOException e) { + e.printStackTrace(); +// log.error("Error with server connection: " + e.getMessage()); + return false; + } + } + + + + // DO NOT MODIFY!!!! + public boolean authenticate(String password) { + RconPacket rconPacket = new RconPacket(); + int id = (int) (Math.random() * 9999999) + 1; + byte[] authRequest = rconPacket.sendPacket(id, RconPacketType.SERVERDATA_AUTH, password); + + ByteBuffer response; + try { + socket.getOutputStream().write(authRequest); + + rconPacket.receivePacket(socket.getInputStream()); //junk response packet + response = rconPacket.receivePacket(socket.getInputStream()); + + if ((response.getInt(4) == id) && (response.getInt(8) == RconPacketType.SERVERDATA_AUTH_RESPONSE)) + authenticated = true; + } catch (IOException e) { + e.printStackTrace(); + } + return authenticated; + } + + + + + public String sendCommand(String command) { + try { + ByteBuffer[] response = sendToServer(command); + if (response != null) { + return RconPacket.assemblePackets(response); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + + private ByteBuffer[] sendToServer(String command) { + RconPacket rconPacket = new RconPacket(); + int id = (int) (Math.random() * (9999999) + 1); + + byte[] request = rconPacket.sendPacket(id, RconPacketType.SERVERDATA_EXECCOMMAND, command); + byte[] emptyRequest = rconPacket.sendEmptyPacket(id, RconPacketType.SERVERDATA_RESPONSE_VALUE); + + ByteBuffer[] resp = new ByteBuffer[4096]; + int i = 0; + + try { + socket.getOutputStream().write(request); + socket.getOutputStream().write(emptyRequest); + + while(true) { + ByteBuffer response = rconPacket.receivePacket(socket.getInputStream()); + if(response != null) { + if (response.getInt(4) == id) { + if (RconPacket.getBodySinglePacket(response).equals("")) { + rconPacket.receivePacket(socket.getInputStream()); + break; + } else { + resp[i] = response; + } + i++; + } + } + } + + return resp; + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + public String receivingServerMessages() throws IOException{ + RconPacket rconPacket = new RconPacket(); + + try { + ByteBuffer response = rconPacket.receivePacket(socket.getInputStream()); + if(response != null) { + return RconPacket.getBodySinglePacket(response); + } + + } catch (SocketTimeoutException ignores) { + } + return null; + } + + public void closeStream() { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/antonio112009/steam4j/core/rcon/packet/RconPacket.java b/src/main/java/com/antonio112009/steam4j/core/rcon/packet/RconPacket.java new file mode 100644 index 0000000..40c5fee --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/rcon/packet/RconPacket.java @@ -0,0 +1,126 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.rcon.packet; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Basic Packet Structure + * + * Size: 32-bit little-endian Signed Integer + * ID: 32-bit little-endian Signed Integer (4 Bytes) + * Type: 32-bit little-endian Signed Integer (4 Bytes) + * Body: Null-terminated ASCII String (at least 1 Byte) + * Empty String: Null-terminated ASCII String (1 Byte) + */ +//@Slf4j +public class RconPacket { + + private ByteBuffer payload = null; + + + /** + * Setting size of the buffer. + * + * From valve's wiki: + * Since the only one of these values that can change in length is the body, + * an easy way to calculate the size of a packet is to find + * the byte-length of the packet body, then add 10 to it. + * + */ + public byte[] sendPacket(int id, int type, String body) { + payload = ByteBuffer.allocate(body.getBytes(StandardCharsets.UTF_8).length + 14); + payload.order(ByteOrder.LITTLE_ENDIAN); + + // Setting Size + payload.putInt(body.getBytes(StandardCharsets.UTF_8).length + 10); + + // Setting ID + payload.putInt(id); + + // Setting Type + payload.putInt(type); + + // Setting Body + payload.put(body.getBytes()); + + // Adding two null bytes at the end + payload.put((byte) 0x00); + payload.put((byte) 0x00); + return payload.array(); + } + + public byte[] sendEmptyPacket(int id, int type) { + return sendPacket(id, type, ""); + } + + + public ByteBuffer receivePacket(InputStream inputStream) throws IOException { + payload = ByteBuffer.allocate(4096); + payload.order(ByteOrder.LITTLE_ENDIAN); + + byte[] length = new byte[4]; + if(inputStream.read(length, 0, 4) == 4) { + if(length[0] == 0) + return null; // TODO: модернизировать + payload.put(length); + for (int i = 0; i < payload.getInt(0); i++) { + payload.put((byte) inputStream.read()); + } + return payload; + } else { + return null; + } + } + + + + + public static String assemblePackets(ByteBuffer[] packets) { + // Return the text from all the response packets together + StringBuilder response = new StringBuilder(); + + for (ByteBuffer packet : packets) { + response.append(getBodySinglePacket(packet)); + } + return response.toString(); + } + + public static String getBodySinglePacket(ByteBuffer packet) { + if (packet != null) { + return new String(packet.array(), 12, packet.position() - 14); +// log.debug(response.toString()); + } else { + return ""; + } + } + + +} diff --git a/src/main/java/com/antonio112009/steam4j/core/rcon/type/RconPacketType.java b/src/main/java/com/antonio112009/steam4j/core/rcon/type/RconPacketType.java new file mode 100644 index 0000000..73cd9e9 --- /dev/null +++ b/src/main/java/com/antonio112009/steam4j/core/rcon/type/RconPacketType.java @@ -0,0 +1,32 @@ +/* + * MIT License + * + * Copyright (c) 2021 Anton Rogalskiy and Steam4J contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.antonio112009.steam4j.core.rcon.type; + +public class RconPacketType { + public final static byte SERVERDATA_AUTH = 3; + public final static byte SERVERDATA_EXECCOMMAND = 2; + public final static byte SERVERDATA_AUTH_RESPONSE = 2; + public final static byte SERVERDATA_RESPONSE_VALUE = 0; +}