From f8777ad035d78c7941aab7ee9b38066cc94d4045 Mon Sep 17 00:00:00 2001 From: 1e99 <1e99@1e99.eu> Date: Sun, 17 Nov 2024 13:45:23 +0100 Subject: [PATCH] first commit --- .gitignore | 4 + LICENSE.md | 347 ++++++++++++++++++ build.gradle | 3 + client-fabric/build.gradle | 45 +++ client-fabric/gradle.properties | 7 + client-fabric/resources/fabric.mod.json | 33 ++ client-fabric/resources/svc.mixins.json | 10 + .../client/fabric/FabricSimplerVoiceChat.java | 18 + common/build.gradle | 16 + common/src/eu/e99/svc/AllTrustManager.java | 20 + common/src/eu/e99/svc/Connection.java | 41 +++ common/src/eu/e99/svc/SimplerVoiceChat.java | 100 +++++ common/src/eu/e99/svc/auth/MojangAPI.java | 42 +++ common/src/eu/e99/svc/auth/PlayerProfile.java | 11 + common/src/eu/e99/svc/io/BinaryMessage.java | 10 + common/src/eu/e99/svc/io/MessageRegistry.java | 50 +++ common/src/eu/e99/svc/io/Reader.java | 48 +++ common/src/eu/e99/svc/io/Writer.java | 43 +++ .../eu/e99/svc/packet/AuthRequestPacket.java | 22 ++ .../eu/e99/svc/packet/AuthResponsePacket.java | 22 ++ .../eu/e99/svc/packet/AuthSuccessPacket.java | 20 + .../eu/e99/svc/packet/ClientHelloPacket.java | 22 ++ .../eu/e99/svc/packet/DisconnectPacket.java | 22 ++ .../src/eu/e99/svc/packet/MessagePacket.java | 26 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56921 bytes gradle/wrapper/gradle-wrapper.properties | 1 + gradlew | 176 +++++++++ gradlew.bat | 84 +++++ server/build.gradle | 18 + server/src/eu/e99/svc/server/Main.java | 65 ++++ server/src/eu/e99/svc/server/Server.java | 195 ++++++++++ settings.gradle | 15 + 32 files changed, 1536 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 build.gradle create mode 100644 client-fabric/build.gradle create mode 100644 client-fabric/gradle.properties create mode 100644 client-fabric/resources/fabric.mod.json create mode 100644 client-fabric/resources/svc.mixins.json create mode 100644 client-fabric/src/eu/e99/svc/client/fabric/FabricSimplerVoiceChat.java create mode 100644 common/build.gradle create mode 100644 common/src/eu/e99/svc/AllTrustManager.java create mode 100644 common/src/eu/e99/svc/Connection.java create mode 100644 common/src/eu/e99/svc/SimplerVoiceChat.java create mode 100644 common/src/eu/e99/svc/auth/MojangAPI.java create mode 100644 common/src/eu/e99/svc/auth/PlayerProfile.java create mode 100644 common/src/eu/e99/svc/io/BinaryMessage.java create mode 100644 common/src/eu/e99/svc/io/MessageRegistry.java create mode 100644 common/src/eu/e99/svc/io/Reader.java create mode 100644 common/src/eu/e99/svc/io/Writer.java create mode 100644 common/src/eu/e99/svc/packet/AuthRequestPacket.java create mode 100644 common/src/eu/e99/svc/packet/AuthResponsePacket.java create mode 100644 common/src/eu/e99/svc/packet/AuthSuccessPacket.java create mode 100644 common/src/eu/e99/svc/packet/ClientHelloPacket.java create mode 100644 common/src/eu/e99/svc/packet/DisconnectPacket.java create mode 100644 common/src/eu/e99/svc/packet/MessagePacket.java create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 server/build.gradle create mode 100644 server/src/eu/e99/svc/server/Main.java create mode 100644 server/src/eu/e99/svc/server/Server.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffdc958 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.gradle +.idea +build +client-fabric/run diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..63ca2d0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,347 @@ +Mozilla Public License Version 2.0 +================================== + +### 1. Definitions + +**1.1. “Contributor”** +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +**1.2. “Contributor Version”** +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +**1.3. “Contribution”** +means Covered Software of a particular Contributor. + +**1.4. “Covered Software”** +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +**1.5. “Incompatible With Secondary Licenses”** +means + +* **(a)** that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or +* **(b)** that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +**1.6. “Executable Form”** +means any form of the work other than Source Code Form. + +**1.7. “Larger Work”** +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +**1.8. “License”** +means this document. + +**1.9. “Licensable”** +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +**1.10. “Modifications”** +means any of the following: + +* **(a)** any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or +* **(b)** any new file in Source Code Form that contains any Covered + Software. + +**1.11. “Patent Claims” of a Contributor** +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +**1.12. “Secondary License”** +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +**1.13. “Source Code Form”** +means the form of the work preferred for making modifications. + +**1.14. “You” (or “Your”)** +means an individual or a legal entity exercising rights under this +License. For legal entities, “You” includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, “control” means **(a)** the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or **(b)** ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + +### 2. License Grants and Conditions + +#### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +* **(a)** under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and +* **(b)** under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +#### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +#### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +* **(a)** for any code that a Contributor has removed from Covered Software; + or +* **(b)** for infringements caused by: **(i)** Your and any other third party's + modifications of Covered Software, or **(ii)** the combination of its + Contributions with other software (except as part of its Contributor + Version); or +* **(c)** under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +#### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +#### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +#### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +#### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +### 3. Responsibilities + +#### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +#### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +* **(a)** such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +* **(b)** You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +#### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +#### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +#### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +### 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: **(a)** comply with +the terms of this License to the maximum extent possible; and **(b)** +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +### 5. Termination + +**5.1.** The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated **(a)** provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and **(b)** on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +**5.2.** If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +### 6. Disclaimer of Warranty + +> Covered Software is provided under this License on an “as is” +> basis, without warranty of any kind, either expressed, implied, or +> statutory, including, without limitation, warranties that the +> Covered Software is free of defects, merchantable, fit for a +> particular purpose or non-infringing. The entire risk as to the +> quality and performance of the Covered Software is with You. +> Should any Covered Software prove defective in any respect, You +> (not any Contributor) assume the cost of any necessary servicing, +> repair, or correction. This disclaimer of warranty constitutes an +> essential part of this License. No use of any Covered Software is +> authorized under this License except under this disclaimer. + +### 7. Limitation of Liability + +> Under no circumstances and under no legal theory, whether tort +> (including negligence), contract, or otherwise, shall any +> Contributor, or anyone who distributes Covered Software as +> permitted above, be liable to You for any direct, indirect, +> special, incidental, or consequential damages of any character +> including, without limitation, damages for lost profits, loss of +> goodwill, work stoppage, computer failure or malfunction, or any +> and all other commercial damages or losses, even if such party +> shall have been informed of the possibility of such damages. This +> limitation of liability shall not apply to liability for death or +> personal injury resulting from such party's negligence to the +> extent applicable law prohibits such limitation. Some +> jurisdictions do not allow the exclusion or limitation of +> incidental or consequential damages, so this exclusion and +> limitation may not apply to You. + +### 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +### 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +### 10. Versions of the License + +#### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +#### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +#### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..075ba3d --- /dev/null +++ b/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'java' +} diff --git a/client-fabric/build.gradle b/client-fabric/build.gradle new file mode 100644 index 0000000..9eb9a19 --- /dev/null +++ b/client-fabric/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'fabric-loom' version '1.8-SNAPSHOT' +} + +repositories { + mavenCentral() +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + implementation project(':common') +} + +sourceSets { + main.java.srcDirs = ['src'] + main.resources.srcDirs = ['resources'] +} + +processResources { + inputs.property "version", version + + filesMatching("fabric.mod.json") { + expand "version": version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + from("LICENSE") { + rename { "${it}_${project.base.archivesName.get()}" } + } +} diff --git a/client-fabric/gradle.properties b/client-fabric/gradle.properties new file mode 100644 index 0000000..33a0d45 --- /dev/null +++ b/client-fabric/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true +# https://fabricmc.net/develop +minecraft_version=1.21.3 +yarn_mappings=1.21.3+build.2 +loader_version=0.16.9 +fabric_version=0.108.0+1.21.3 diff --git a/client-fabric/resources/fabric.mod.json b/client-fabric/resources/fabric.mod.json new file mode 100644 index 0000000..a063aaa --- /dev/null +++ b/client-fabric/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "svc", + "version": "${version}", + "name": "Simpler Voice Chat", + "description": "Simpler voice chat for our favorite block game", + "authors": [ + "1e99" + ], + "contact": { + "sources": "https://git.1e99.eu/1e99/simplervoicechat" + }, + "license": "MPL-2.0", + "icon": "assets/svc/icon.png", + "environment": "*", + "entrypoints": { + "client": [ + "eu.e99.svc.client.fabric.FabricSimplerVoiceChat" + ] + }, + "mixins": [ + { + "config": "svc.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.16.7", + "minecraft": "~1.21.3", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/client-fabric/resources/svc.mixins.json b/client-fabric/resources/svc.mixins.json new file mode 100644 index 0000000..7b7c489 --- /dev/null +++ b/client-fabric/resources/svc.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "package": "eu.e99.svc.client.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/client-fabric/src/eu/e99/svc/client/fabric/FabricSimplerVoiceChat.java b/client-fabric/src/eu/e99/svc/client/fabric/FabricSimplerVoiceChat.java new file mode 100644 index 0000000..088dd7b --- /dev/null +++ b/client-fabric/src/eu/e99/svc/client/fabric/FabricSimplerVoiceChat.java @@ -0,0 +1,18 @@ +package eu.e99.svc.client.fabric; + +import net.fabricmc.api.ClientModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FabricSimplerVoiceChat implements ClientModInitializer { + + public static final String NAMESPACE = "svc"; + public static final Logger LOGGER = LoggerFactory.getLogger(NAMESPACE); + + @Override + public void onInitializeClient() { + LOGGER.info("Simpler Voice Chat Running"); + + + } +} diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..0e19848 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.google.code.gson:gson:2.11.0' +} + +sourceSets { + main.java.srcDirs = ['src'] + main.resources.srcDirs = ['resources'] +} diff --git a/common/src/eu/e99/svc/AllTrustManager.java b/common/src/eu/e99/svc/AllTrustManager.java new file mode 100644 index 0000000..fd52591 --- /dev/null +++ b/common/src/eu/e99/svc/AllTrustManager.java @@ -0,0 +1,20 @@ +package eu.e99.svc; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; + +public class AllTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} diff --git a/common/src/eu/e99/svc/Connection.java b/common/src/eu/e99/svc/Connection.java new file mode 100644 index 0000000..f0f8daf --- /dev/null +++ b/common/src/eu/e99/svc/Connection.java @@ -0,0 +1,41 @@ +package eu.e99.svc; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; +import eu.e99.svc.packet.DisconnectPacket; + +import java.io.IOException; +import java.net.Socket; + +public class Connection { + + private final Socket socket; + private final Reader reader; + private final Writer writer; + + public Connection(Socket socket) throws IOException { + this.socket = socket; + this.reader = new Reader(this.socket.getInputStream()); + this.writer = new Writer(this.socket.getOutputStream()); + } + + public void writePacket(BinaryMessage packet) throws IOException { + synchronized (this.socket) { + SimplerVoiceChat.PACKETS.writeMessage(packet, this.writer); + } + } + + public BinaryMessage readPacket() throws IOException { + synchronized (this.socket) { + return SimplerVoiceChat.PACKETS.readMessage(this.reader); + } + } + + public void disconnect(String reason) throws IOException { + DisconnectPacket disconnect = new DisconnectPacket(); + disconnect.reason = reason; + this.writePacket(disconnect); + this.socket.close(); + } +} diff --git a/common/src/eu/e99/svc/SimplerVoiceChat.java b/common/src/eu/e99/svc/SimplerVoiceChat.java new file mode 100644 index 0000000..c670ebd --- /dev/null +++ b/common/src/eu/e99/svc/SimplerVoiceChat.java @@ -0,0 +1,100 @@ +package eu.e99.svc; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.MessageRegistry; +import eu.e99.svc.packet.*; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class SimplerVoiceChat { + + public static final int PROTOCOL_VERSION = 0; + + public static final MessageRegistry PACKETS = new MessageRegistry(); + public static final MessageRegistry MESSAGES = new MessageRegistry(); + + static { + PACKETS.registerMessage(0, ClientHelloPacket::new); + // TODO: Keep alive? + PACKETS.registerMessage(2, AuthRequestPacket::new); + PACKETS.registerMessage(3, AuthResponsePacket::new); + PACKETS.registerMessage(4, AuthSuccessPacket::new); + PACKETS.registerMessage(100, MessagePacket::new); + PACKETS.registerMessage(255, DisconnectPacket::new); + } + + + public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException { + TrustManager[] trustManagers = new TrustManager[]{ + new AllTrustManager(), + }; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, new SecureRandom()); + + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + + SimplerVoiceChat voiceChat = new SimplerVoiceChat("localhost", 6969, socketFactory); + voiceChat.start(); + } + + private final String host; + private final int port; + private final SSLSocketFactory socketFactory; + + public SimplerVoiceChat(String host, int port, SSLSocketFactory socketFactory) { + this.host = host; + this.port = port; + this.socketFactory = socketFactory; + } + + public void start() { + // Connect and auth + + try (Socket socket = this.socketFactory.createSocket()) { + socket.connect(new InetSocketAddress(this.host, this.port)); + Connection conn = new Connection(socket); + + this.handleHandshake(conn); + } catch (IOException e) { + System.out.printf("Failed to connect.%n"); + e.printStackTrace(System.out); + } + } + + private void handleHandshake(Connection conn) throws IOException { + ClientHelloPacket clientHello = new ClientHelloPacket(); + clientHello.version = PROTOCOL_VERSION; + conn.writePacket(clientHello); + + while (true) { + BinaryMessage packet = conn.readPacket(); + + switch (packet) { + case AuthRequestPacket authRequest -> { + System.out.printf("Joining fake server to authenticate.%n"); + + AuthResponsePacket authResponse = new AuthResponsePacket(); + authResponse.username = "Test"; + conn.writePacket(authResponse); + } + case AuthSuccessPacket authComplete -> { + System.out.printf("Successfully authenticated.%n"); + return; + } + default -> { + System.out.printf("Got unexpected packet.%n"); + } + } + } + + } +} diff --git a/common/src/eu/e99/svc/auth/MojangAPI.java b/common/src/eu/e99/svc/auth/MojangAPI.java new file mode 100644 index 0000000..9771b47 --- /dev/null +++ b/common/src/eu/e99/svc/auth/MojangAPI.java @@ -0,0 +1,42 @@ +package eu.e99.svc.auth; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.UUID; + +// https://wiki.vg/Protocol_Encryption +public class MojangAPI { + + public static PlayerProfile hasJoined(String username, String serverId) throws IOException, InterruptedException { + try (HttpClient client = HttpClient.newHttpClient()) { + String loc = String.format("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s", username, serverId); + URI uri = URI.create(loc); + + HttpRequest req = HttpRequest.newBuilder() + .uri(uri) + .GET() + .build(); + + HttpResponse res = client.send(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() == 204) { + return null; + } + + JsonObject object = JsonParser + .parseString(res.body()) + .getAsJsonObject(); + + String name = object.get("name").getAsString(); + String rawUuid = object.get("id").getAsString(); + UUID uuid = UUID.fromString(rawUuid.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + + return new PlayerProfile(name, uuid); + } + } +} diff --git a/common/src/eu/e99/svc/auth/PlayerProfile.java b/common/src/eu/e99/svc/auth/PlayerProfile.java new file mode 100644 index 0000000..4544355 --- /dev/null +++ b/common/src/eu/e99/svc/auth/PlayerProfile.java @@ -0,0 +1,11 @@ +package eu.e99.svc.auth; + +import java.util.UUID; + +public record PlayerProfile( + String username, + UUID uuid +) { + + +} diff --git a/common/src/eu/e99/svc/io/BinaryMessage.java b/common/src/eu/e99/svc/io/BinaryMessage.java new file mode 100644 index 0000000..acba43e --- /dev/null +++ b/common/src/eu/e99/svc/io/BinaryMessage.java @@ -0,0 +1,10 @@ +package eu.e99.svc.io; + +import java.io.IOException; + +public interface BinaryMessage { + + void read(Reader reader) throws IOException; + + void write(Writer writer) throws IOException; +} diff --git a/common/src/eu/e99/svc/io/MessageRegistry.java b/common/src/eu/e99/svc/io/MessageRegistry.java new file mode 100644 index 0000000..b7bd42d --- /dev/null +++ b/common/src/eu/e99/svc/io/MessageRegistry.java @@ -0,0 +1,50 @@ +package eu.e99.svc.io; + +import eu.e99.svc.packet.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class MessageRegistry { + + private final Map> messagesById; + private final Map, Integer> messagesByClass; + + public MessageRegistry() { + this.messagesById = new HashMap<>(); + this.messagesByClass = new HashMap<>(); + } + + public BinaryMessage readMessage(Reader reader) throws IOException { + int id = reader.readInt(); + Supplier supplier = this.messagesById.get(id);; + if (supplier == null) { + throw new RuntimeException("Failed to find message with id " + id); + } + + BinaryMessage msg = supplier.get(); + msg.read(reader); + return msg; + } + + public void writeMessage(BinaryMessage msg, Writer writer) throws IOException { + Class clazz = msg.getClass(); + Integer id = this.messagesByClass.get(clazz); + if (id == null) { + throw new IllegalArgumentException("Unknown message sent."); + } + + writer.writeInt(id); + msg.write(writer); + } + + public void registerMessage(int id, Supplier supplier) { + BinaryMessage msg = supplier.get(); + Class clazz = msg.getClass(); + + this.messagesById.put(id, supplier); + this.messagesByClass.put(clazz, id); + } +} diff --git a/common/src/eu/e99/svc/io/Reader.java b/common/src/eu/e99/svc/io/Reader.java new file mode 100644 index 0000000..ea154da --- /dev/null +++ b/common/src/eu/e99/svc/io/Reader.java @@ -0,0 +1,48 @@ +package eu.e99.svc.io; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class Reader { + + private final DataInputStream in; + + public Reader(InputStream in) { + this.in = new DataInputStream(in); + } + + public int readInt() throws IOException { + return this.in.readInt(); + } + + public long readLong() throws IOException { + return this.in.readLong(); + } + + public boolean readBool() throws IOException { + return this.in.readBoolean(); + } + + public String readString() throws IOException { + byte[] bytes = this.readByteArray(); + return new String(bytes, StandardCharsets.UTF_8); + } + + public byte[] readByteArray() throws IOException { + int length = this.readInt(); + byte[] bytes = new byte[length]; + this.in.readFully(bytes); + + return bytes; + } + + public UUID readUUID() throws IOException { + long most = this.readLong(); + long least = this.readLong(); + + return new UUID(most, least); + } +} diff --git a/common/src/eu/e99/svc/io/Writer.java b/common/src/eu/e99/svc/io/Writer.java new file mode 100644 index 0000000..11692a4 --- /dev/null +++ b/common/src/eu/e99/svc/io/Writer.java @@ -0,0 +1,43 @@ +package eu.e99.svc.io; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class Writer { + + private final DataOutputStream out; + + public Writer(OutputStream out) { + this.out = new DataOutputStream(out); + } + + public void writeInt(int i) throws IOException { + this.out.writeInt(i); + } + + public void writeLong(long l) throws IOException { + this.out.writeLong(l); + } + + public void writeBool(boolean bool) throws IOException { + this.out.writeBoolean(bool); + } + + public void writeString(String string) throws IOException { + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + this.writeByteArray(bytes); + } + + public void writeByteArray(byte[] bytes) throws IOException { + this.writeInt(bytes.length); + this.out.write(bytes); + } + + public void writeUUID(UUID uuid) throws IOException { + this.writeLong(uuid.getMostSignificantBits()); + this.writeLong(uuid.getLeastSignificantBits()); + } +} diff --git a/common/src/eu/e99/svc/packet/AuthRequestPacket.java b/common/src/eu/e99/svc/packet/AuthRequestPacket.java new file mode 100644 index 0000000..08249e6 --- /dev/null +++ b/common/src/eu/e99/svc/packet/AuthRequestPacket.java @@ -0,0 +1,22 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; + +public class AuthRequestPacket implements BinaryMessage { + + public String serverId; + + @Override + public void read(Reader reader) throws IOException { + this.serverId = reader.readString(); + } + + @Override + public void write(Writer writer) throws IOException { + writer.writeString(this.serverId); + } +} diff --git a/common/src/eu/e99/svc/packet/AuthResponsePacket.java b/common/src/eu/e99/svc/packet/AuthResponsePacket.java new file mode 100644 index 0000000..cb2a425 --- /dev/null +++ b/common/src/eu/e99/svc/packet/AuthResponsePacket.java @@ -0,0 +1,22 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; + +public class AuthResponsePacket implements BinaryMessage { + + public String username; + + @Override + public void read(Reader reader) throws IOException { + this.username = reader.readString(); + } + + @Override + public void write(Writer writer) throws IOException { + writer.writeString(this.username); + } +} diff --git a/common/src/eu/e99/svc/packet/AuthSuccessPacket.java b/common/src/eu/e99/svc/packet/AuthSuccessPacket.java new file mode 100644 index 0000000..b16940f --- /dev/null +++ b/common/src/eu/e99/svc/packet/AuthSuccessPacket.java @@ -0,0 +1,20 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; + +public class AuthSuccessPacket implements BinaryMessage { + + @Override + public void read(Reader reader) throws IOException { + + } + + @Override + public void write(Writer writer) throws IOException { + + } +} diff --git a/common/src/eu/e99/svc/packet/ClientHelloPacket.java b/common/src/eu/e99/svc/packet/ClientHelloPacket.java new file mode 100644 index 0000000..02eb23a --- /dev/null +++ b/common/src/eu/e99/svc/packet/ClientHelloPacket.java @@ -0,0 +1,22 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; + +public class ClientHelloPacket implements BinaryMessage { + + public int version; + + @Override + public void read(Reader reader) throws IOException { + this.version = reader.readInt(); + } + + @Override + public void write(Writer writer) throws IOException { + writer.writeInt(this.version); + } +} diff --git a/common/src/eu/e99/svc/packet/DisconnectPacket.java b/common/src/eu/e99/svc/packet/DisconnectPacket.java new file mode 100644 index 0000000..82590fd --- /dev/null +++ b/common/src/eu/e99/svc/packet/DisconnectPacket.java @@ -0,0 +1,22 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; + +public class DisconnectPacket implements BinaryMessage { + + public String reason; + + @Override + public void read(Reader reader) throws IOException { + this.reason = reader.readString(); + } + + @Override + public void write(Writer writer) throws IOException { + writer.writeString(this.reason); + } +} diff --git a/common/src/eu/e99/svc/packet/MessagePacket.java b/common/src/eu/e99/svc/packet/MessagePacket.java new file mode 100644 index 0000000..c08d1fe --- /dev/null +++ b/common/src/eu/e99/svc/packet/MessagePacket.java @@ -0,0 +1,26 @@ +package eu.e99.svc.packet; + +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.io.Reader; +import eu.e99.svc.io.Writer; + +import java.io.IOException; +import java.util.UUID; + +public class MessagePacket implements BinaryMessage { + + public UUID player; + public byte[] payload; + + @Override + public void read(Reader reader) throws IOException { + this.player = reader.readUUID(); + this.payload = reader.readByteArray(); + } + + @Override + public void write(Writer writer) throws IOException { + writer.writeUUID(this.player); + writer.writeByteArray(this.payload); + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..955e78691802d891ed0aee5033b575d9cc6c6870 GIT binary patch literal 56921 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..87e601d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..17a9170 --- /dev/null +++ b/gradlew @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then + DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS" +fi + +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..6085679 --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':common') + implementation 'org.slf4j:slf4j-api:2.0.16' + implementation 'org.slf4j:slf4j-simple:2.0.16' +} + +sourceSets { + main.java.srcDirs = ['src'] + main.resources.srcDirs = ['resources'] +} diff --git a/server/src/eu/e99/svc/server/Main.java b/server/src/eu/e99/svc/server/Main.java new file mode 100644 index 0000000..700e86b --- /dev/null +++ b/server/src/eu/e99/svc/server/Main.java @@ -0,0 +1,65 @@ +package eu.e99.svc.server; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.security.KeyStore; + +public class Main { + + public static void main(String[] args) { + SSLServerSocketFactory socketFactory = createSocketFactory(); + if (socketFactory == null) { + System.exit(1); + } + + try (ServerSocket socket = socketFactory.createServerSocket()) { + socket.bind(new InetSocketAddress("0.0.0.0", 6969)); + + Server server = new Server(socket); + server.start(); + } catch (IOException e) { + e.printStackTrace(System.out); + } + } + + private static SSLServerSocketFactory createSocketFactory() { + try { + // keytool -genkeypair -keyalg RSA -keysize 2048 -validity 365 -alias mykey -keystore keystore.jks + String path = System.getenv("SVC_KEYSTORE_PATH"); + if (path == null) { + System.out.printf("Missing SVC_KEYSTORE_PATH env variable.%n"); + return null; + } + + String password = System.getenv("SVC_KEYSTORE_PASSWORD"); + if (password == null) { + System.out.printf("Missing SVC_KEYSTORE_PASSWORD env variable.%n"); + return null; + } + + KeyStore keyStore = KeyStore.getInstance("JKS"); + + try (FileInputStream keystoreFile = new FileInputStream(path)) { + keyStore.load(keystoreFile, password.toCharArray()); + } + + KeyManagerFactory managerFactory = KeyManagerFactory.getInstance("SunX509"); + managerFactory.init(keyStore, password.toCharArray()); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(managerFactory.getKeyManagers(), null, null); + + return context.getServerSocketFactory(); + } catch (Exception e) { + System.out.printf("Failed to create SSL Server Socket Factory.%n"); + e.printStackTrace(System.out); + return null; + } + } +} diff --git a/server/src/eu/e99/svc/server/Server.java b/server/src/eu/e99/svc/server/Server.java new file mode 100644 index 0000000..97f047c --- /dev/null +++ b/server/src/eu/e99/svc/server/Server.java @@ -0,0 +1,195 @@ +package eu.e99.svc.server; + +import eu.e99.svc.Connection; +import eu.e99.svc.SimplerVoiceChat; +import eu.e99.svc.auth.MojangAPI; +import eu.e99.svc.auth.PlayerProfile; +import eu.e99.svc.io.BinaryMessage; +import eu.e99.svc.packet.*; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Server { + + private final ServerSocket socket; + private final Map connections; + + public Server(ServerSocket socket) { + this.socket = socket; + this.connections = new HashMap<>(); + } + + public void start() { + while (true) { + Thread.ofVirtual().start(() -> { + try (Socket client = this.socket.accept()) { + synchronized (this.socket) { + this.socket.notify(); + } + + this.handleConnection(client); + } catch (IOException e) { + synchronized (this.socket) { + this.socket.notify(); + } + + System.out.printf("Failed to accept client.%n"); + e.printStackTrace(System.out); + } + }); + + try { + synchronized (this.socket) { + this.socket.wait(); + } + } catch (InterruptedException e) { + System.out.printf("Failed to wait on server socket to accept client.%n"); + e.printStackTrace(System.out); + } + } + } + + private void handleConnection(Socket client) throws IOException { + System.out.printf("Accepted client %s.%n", client.getInetAddress()); + Connection conn = new Connection(client); + + PlayerProfile profile = this.handleHandshake(conn); + if (profile == null) { + System.out.printf("Client failed authentication.%n"); + return; + } + + try { + synchronized (this.connections) { + Connection existingConn = this.connections.get(profile.uuid()); + if (existingConn != null) { + existingConn.disconnect("Logged in from another location."); + System.out.printf("Client %s logged in from another location.%n", profile.username()); + } + + this.connections.put(profile.uuid(), conn); + System.out.printf("Client %s successfully authenticated.%n", profile.username()); + } + + this.handle(conn, profile); + } finally { + synchronized (this.connections) { + this.connections.remove(profile.uuid()); + } + } + } + + private PlayerProfile handleHandshake(Connection conn) throws IOException { + /* + 1. Client sends ClientHelloPacket + 2. Server sends AuthRequestPacket with a random server ID + 3. Client joins fake server ID using Mojang's session server + 4. Client sends AuthResponsePacket with its username + 5. Server checks if player joined the server with Mojang's session server + 6. Server sends AuthSuccessPacket + */ + + String serverId = this.generateServerId(); + + while (true) { + BinaryMessage packet = conn.readPacket(); + + switch (packet) { + case ClientHelloPacket clientHello -> { + if (clientHello.version > SimplerVoiceChat.PROTOCOL_VERSION) { + System.out.printf("Refusing to accept client, outdated server.%n"); + conn.disconnect("Outdated server. Please downgrade the mod."); + return null; + } + + if (clientHello.version < SimplerVoiceChat.PROTOCOL_VERSION) { + System.out.printf("Refusing to accept client, outdated client.%n"); + conn.disconnect("Outdated client. Please update the mode."); + return null; + } + + AuthRequestPacket authRequest = new AuthRequestPacket(); + authRequest.serverId = serverId; + conn.writePacket(authRequest); + } + case AuthResponsePacket response -> { + PlayerProfile profile; + try { + profile = MojangAPI.hasJoined(response.username, serverId); + } catch (InterruptedException e) { + System.out.printf("Failed to reach Mojang's authentication server.%n"); + e.printStackTrace(System.out); + + conn.disconnect("Mojang's authentication servers are offline. Please try again later."); + return null; + } + + if (profile == null) { + System.out.printf("Client failed authentication.%n"); + conn.disconnect("Authentication failed."); + return null; + } + + AuthSuccessPacket success = new AuthSuccessPacket(); + conn.writePacket(success); + return profile; + } + default -> { + System.out.printf("Received unexpected packet.%n"); + } + } + } + } + + private void handle(Connection conn, PlayerProfile profile) throws IOException { + while (true) { + BinaryMessage packet = conn.readPacket(); + + switch (packet) { + case DisconnectPacket disconnect -> { + System.out.printf("Client %s disconnected with the reason \"%s\"%n", profile.username(), disconnect.reason); + return; + } + case MessagePacket message -> { + Connection otherConn; + synchronized (this.connections) { + otherConn = this.connections.get(message.player); + } + + if (otherConn == null) { + // TODO: Send error to client? + continue; + } + + MessagePacket newMessage = new MessagePacket(); + newMessage.player = profile.uuid(); + newMessage.payload = message.payload; + + otherConn.writePacket(newMessage); + } + default -> { + System.out.printf("Received unexpected packet.%n"); + } + } + } + } + + private String generateServerId() { + int length = 32; + + String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder builder = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + int letter = (int) (letters.length() * Math.random()); + builder.append(letters.charAt(letter)); + } + + return builder.toString(); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..fc5f453 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'simplervoicechat' +include 'client-fabric' +include 'common' +include 'server'