mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-12-23 15:18:09 +01:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a4f41056 | ||
|
|
5174a84a17 | ||
|
|
a7b164b513 | ||
|
|
ffa5059c67 | ||
|
|
dfe37dfc1b | ||
|
|
6edd40742d | ||
|
|
b697dc6655 | ||
|
|
36974450ce | ||
|
|
57a59045ce | ||
|
|
11cc4eabc0 | ||
|
|
53e02d5457 | ||
|
|
0f85674ec1 | ||
|
|
378ab09bc8 | ||
|
|
740b11b434 | ||
|
|
77f0184899 | ||
|
|
2885daf8b9 | ||
|
|
1e128d12f5 | ||
|
|
f2a8446c8d | ||
|
|
cc8c49e25b | ||
|
|
25e182148f | ||
|
|
f00608c321 | ||
|
|
b86bdf5f23 | ||
|
|
9a30a0b299 | ||
|
|
fd3b1ed8b6 | ||
|
|
f3e675e547 | ||
|
|
0967f31b9a | ||
|
|
8cb4621055 | ||
|
|
f610001c9b | ||
|
|
dd386408d1 | ||
|
|
10bfd279d6 | ||
|
|
5608821fe3 | ||
|
|
9c0ad7d70c | ||
|
|
099b8e5d0a | ||
|
|
b4ade882be | ||
|
|
4a3cb42152 | ||
|
|
8fc5050e8e | ||
|
|
015739fe4c | ||
|
|
b2ae46a90a | ||
|
|
353cd17823 | ||
|
|
157b8499a9 | ||
|
|
bd46dae086 | ||
|
|
eacbb1ed76 | ||
|
|
e389433138 | ||
|
|
d1b2fe8865 | ||
|
|
3b4c4a1c79 | ||
|
|
b22df62f90 | ||
|
|
d118de8649 | ||
|
|
f8c10d6890 | ||
|
|
c3f8e59a9a | ||
|
|
834818bb7a | ||
|
|
fa46dc690b | ||
|
|
fdc2772f38 | ||
|
|
53af09ae34 | ||
|
|
f6f6aaf1de | ||
|
|
c0ef95e808 | ||
|
|
cb129547f5 | ||
|
|
eb394b5f60 | ||
|
|
aebbc84621 | ||
|
|
0eee6ba2be | ||
|
|
7e2057a7a2 | ||
|
|
800f077be0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
# netbeans
|
||||
/nbproject
|
||||
nb-configuration.xml
|
||||
/bukkit/nbproject/
|
||||
|
||||
# maven
|
||||
/target
|
||||
@@ -39,4 +40,4 @@ hs_err_pid*
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
!gradle-wrapper.jar
|
||||
15
.travis.yml
Normal file
15
.travis.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Use https://travis-ci.org/ for automatic tests
|
||||
|
||||
# speed up testing http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
# This is a java project
|
||||
language: java
|
||||
|
||||
script: mvn compile test
|
||||
|
||||
# We run on 7+
|
||||
jdk:
|
||||
- openjdk7
|
||||
- oraclejdk7
|
||||
- oraclejdk8
|
||||
99
CHANGELOG.md
Normal file
99
CHANGELOG.md
Normal file
@@ -0,0 +1,99 @@
|
||||
######1.0
|
||||
|
||||
* Massive refactor to handle errors on force actions safely
|
||||
* force Methods now runs async too
|
||||
* force methods now returns a boolean to reflect if the method was successful
|
||||
* isRegistered method should now throw an exception if the plugin was unable to query the requested data
|
||||
|
||||
######0.8
|
||||
|
||||
* Fixed BungeeCord support for the Bukkit module
|
||||
* Added database storage to save the premium state
|
||||
* Fix logical error on /premium (Thanks to @NorbiPeti)
|
||||
* Fixed issues with host lookup from hosts file (Thanks to @NorbiPeti)
|
||||
* Remove handshake listener because it creates errors on some systems
|
||||
|
||||
######0.7
|
||||
|
||||
* Added BungeeAuth support
|
||||
* Added /premium [player] command with optional player parameter
|
||||
* Added a check if the player is already on the premium list
|
||||
* Added a forwardSkin config option
|
||||
* Added premium UUID support
|
||||
* Updated to the newest changes of Spigot
|
||||
* Removes the need of an Bukkit auth plugin if you use a bungeecord one
|
||||
* Optimize performance and thread-safety
|
||||
* Fixed BungeeCord support
|
||||
* Changed config option autologin to autoregister to clarify the usage
|
||||
|
||||
######0.6
|
||||
|
||||
* Fixed 1.9 bugs
|
||||
* Added UltraAuth support
|
||||
|
||||
######0.5
|
||||
|
||||
* Added unpremium command
|
||||
* Added autologin - See config
|
||||
* Added config
|
||||
* Added isRegistered API method
|
||||
* Added forceRegister API method
|
||||
|
||||
* Fixed CrazyLogin player data restore -> Fixes memory leaks with this plugin
|
||||
* Fixed premium name check to protocolsupport
|
||||
* Improved permissions management
|
||||
|
||||
######0.4
|
||||
|
||||
* Added forward premium skin
|
||||
* Added plugin support for protocolsupport
|
||||
|
||||
######0.3.2
|
||||
|
||||
* Run packet readers in a different thread (separated from the Netty I/O Thread)
|
||||
-> Improves performance
|
||||
* Fixed Plugin disable if the server is in online mode but have to be in offline mode
|
||||
|
||||
######0.3.1
|
||||
|
||||
* Improved BungeeCord security
|
||||
|
||||
#####0.3
|
||||
|
||||
* Added BungeeCord support
|
||||
* Decrease timeout checks in order to fail faster on connection problems
|
||||
* Code style improvements
|
||||
|
||||
######0.2.4
|
||||
|
||||
* Fixed NPE on invalid sessions
|
||||
* Improved security by generating a randomized serverId
|
||||
* Removed /premium [player] because it's safer for premium players who join without registration
|
||||
|
||||
######0.2.3
|
||||
|
||||
* Remove useless AuthMe forcelogin code
|
||||
* Send a kick message to the client instead of just "Disconnect"
|
||||
* Reformat source code
|
||||
* Fix thread safety for fake start packets (Bukkit.getOfflinePlayer doesn't look like to be thread-safe)
|
||||
* Added more documentation
|
||||
|
||||
######0.2.2
|
||||
|
||||
* Compile project with Java 7 :(
|
||||
|
||||
######0.2.1
|
||||
* A couple of security fixes (premium players cannot longer steal the account of a cracked account)
|
||||
* Added a /premium command to mark you as premium player
|
||||
|
||||
#####0.2
|
||||
|
||||
* Added support for CrazyLogin and LoginSecurity
|
||||
* Now minecraft version independent
|
||||
* Added debug logging
|
||||
* Code clean up
|
||||
* More state validation
|
||||
* Added better error handling
|
||||
|
||||
#####0.1
|
||||
* First release
|
||||
163
README.md
163
README.md
@@ -1,13 +1,156 @@
|
||||
# FastLogin
|
||||
|
||||
Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentification.
|
||||
[](https://travis-ci.org/games647/FastLogin)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8ZBULMAPN7MZC)
|
||||
|
||||
Requirements:
|
||||
* [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
|
||||
* Bukkit 1.8.8
|
||||
* Java 8 or above
|
||||
* An auth plugin. Supported Plugins:
|
||||
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
||||
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
||||
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
|
||||
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
|
||||
Checks if a minecraft player has a paid account (premium). If so, they can skip offline authentication (auth plugins).
|
||||
So they don't need to enter passwords. This is also called auto login (auto-login).
|
||||
|
||||
###Features:
|
||||
|
||||
* Detect paid accounts from others
|
||||
* Automatically login paid accounts (premium)
|
||||
* Support various of auth plugins
|
||||
* Cauldron support
|
||||
* Forge/Sponge message support
|
||||
* Premium UUID support
|
||||
* Forwards Skins
|
||||
* BungeeCord support
|
||||
* Auto register new premium players
|
||||
* Plugin: ProtocolSupport is supported and can be used as an alternative to ProtocolLib
|
||||
* No client modifications needed
|
||||
* Good performance by using async non blocking operations
|
||||
* Free
|
||||
* Open source
|
||||
|
||||
***
|
||||
|
||||
###Commands:
|
||||
* /premium [player] Label the invoker as paid account
|
||||
* /cracked [player] Label the invoker as cracked account
|
||||
|
||||
###Permissions:
|
||||
* fastlogin.bukkit.command.premium
|
||||
|
||||
###Requirements:
|
||||
* Plugin: [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
|
||||
* Tested Bukkit/[Spigot](https://www.spigotmc.org) 1.9 (could also work with other versions)
|
||||
* Java 7+
|
||||
* Run Spigot and/or BungeeCord/Waterfall in offline mode (see server.properties or config.yml)
|
||||
* An auth plugin. Supported Plugins
|
||||
|
||||
####Bukkit/Spigot/PaperSPigot
|
||||
|
||||
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
||||
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
||||
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
|
||||
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
|
||||
* [RoyalAuth](http://dev.bukkit.org/bukkit-plugins/royalauth/)
|
||||
* [UltraAuth](http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/)
|
||||
|
||||
####BungeeCord/Waterfall
|
||||
|
||||
* [BungeeAuth](https://www.spigotmc.org/resources/bungeeauth.493/)
|
||||
|
||||
###Downloads
|
||||
|
||||
https://www.spigotmc.org/resources/fastlogin.14153/history
|
||||
|
||||
***
|
||||
|
||||
###FAQ
|
||||
|
||||
####Index
|
||||
1. [How does minecraft logins work?](#how-does-minecraft-logins-work)
|
||||
2. [How does this plugin work?](#how-does-this-plugin-work)
|
||||
3. [Why does the plugin require offline mode?](#why-does-the-plugin-require-offline-mode)
|
||||
4. [Can cracked player join with premium usernames?](#can-cracked-player-join-with-premium-usernames)
|
||||
5. [Why do players have to invoke a command?](#why-do-players-have-to-invoke-a-command)
|
||||
6. [What happens if a paid account joins with a used username?](#what-happens-if-a-paid-account-joins-with-a-used-username)
|
||||
7. [Does the plugin have BungeeCord support?](#does-the-plugin-have-bungeecord-support)
|
||||
8. [Could premium players have a premium UUID and Skin?](#could-premium-players-have-a-premium-uuid-and-skin)
|
||||
9. [Is this plugin compatible with Cauldron?](#is-this-plugin-compatible-with-cauldron)
|
||||
|
||||
####How does minecraft logins work?
|
||||
######Online Mode
|
||||
1. Client -> Server: I want to login, here is my username
|
||||
2. Server -> Client: Okay. I'm in online mode so here is my public key for encryption and my serverid
|
||||
3. Client -> Mojang: I'm player "xyz". I want to join a server with that serverid
|
||||
4. Mojang -> Client: Session data checked. You can continue
|
||||
5. Client -> Server: I received a successful response from Mojang. Heres our shared secret key
|
||||
6. Server -> Mojang: Does the player "xyz" with this shared secret key has a valid account to join me?
|
||||
7. Mojang -> Server: Yes, the player has the following additionally properties (UUID, Skin)
|
||||
8. Client and Server: encrypt all following communication packet
|
||||
9. Server -> Client: Everything checked you can play now
|
||||
|
||||
|
||||
######Offline Mode
|
||||
In offline mode step 2-7 is skipped. So a login request is directly followed by 8.
|
||||
|
||||
######More details
|
||||
http://wiki.vg/Protocol#Login
|
||||
|
||||
####How does this plugin work?
|
||||
By using ProtocolLib, this plugin works as a proxy between the client and server. This plugin will fake that the server
|
||||
runs in online mode. It does everything an online mode server would do. This will be for example, generating keys or
|
||||
checking for valid sessions. Because everything is the same compared to an offline mode login after an encrypted
|
||||
connection, we will intercept only **login** packets of **premium** players.
|
||||
|
||||
1. Player is connecting to the server.
|
||||
2. Plugin checks if the username we received activated the fast login method (i.e. using command)
|
||||
3. Run a check if the username is currently used by a paid account.
|
||||
(We don't know yet if the client connecting is premium)
|
||||
4. Request an Mojang Session Server authentication
|
||||
5. On response check if all data is correct
|
||||
6. Encrypt the connection
|
||||
7. On success intercept all related login packets and fake a new login packet as a normal offline login
|
||||
|
||||
####Why does the plugin require offline mode?
|
||||
1. As you can see in the question "how does minecraft login works", offline mode is equivalent to online mode except of
|
||||
the encryption and session checks on login. So we can intercept and cancel the first packets for premium players and
|
||||
enable an encrypted connection. Then we send a new fake packet in order to pretend that this a new login request from
|
||||
a offline mode player. The server will handle the rest.
|
||||
2. Some plugins check if the server is in online mode. If so, they could process the real offline (cracked) accounts
|
||||
incorrectly. For example, a plugin tries to fetch the UUID from Mojang, but the name of the player is not associated to
|
||||
a paid account.
|
||||
3. Servers, who allow cracked players and just speed up logins for premium players, are **already** in offline mode.
|
||||
|
||||
####Can cracked player join with premium usernames?
|
||||
Yes, indeed. Therefore the command for toggling the fast login method exists.
|
||||
|
||||
####Why do players have to invoke a command?
|
||||
1. It's a secure way to make sure a person with a paid account cannot steal the account
|
||||
of a cracked player that has the same username. The player have to proof first that it's his own account.
|
||||
2. We only receive the username from the player on login. We could check if that username is associated
|
||||
to a paid account but if we request a online mode login from a cracked player (who uses a username from
|
||||
a paid account), the player will disconnect with the reason "bad login" or "Invalid session". There is no way to change
|
||||
that message on the server side (without client modifications), because it's a connection between the Client and the
|
||||
Sessionserver.
|
||||
3. If a premium player would skip registration too, a player of a cracked account could later still register the
|
||||
account and would claim and steal the account from the premium player. Because commands cannot be invoked unless the
|
||||
player has a account or is logged in, protects this method also premium players
|
||||
|
||||
###What happens if a paid account joins with a used username?
|
||||
The player on the server have to activate the feature of this plugin by command. If a person buys the username
|
||||
of his own account, it's still secured. A normal offline mode login makes sure he's the owner of the server account
|
||||
and Mojang account. Then the command can be executed. So someone different cannot steal the account of cracked player
|
||||
by buying the username.
|
||||
|
||||
####Does the plugin have BungeeCord support?
|
||||
Yes it has. Just activate ipForward in your BungeeCord config and place the plugin in the plugins folder of
|
||||
Bukkit/Spigot and BungeeCord. Then you have fill your BungeeCord Id (from the Stats-Option in the BungeeCord config)
|
||||
into the whitelist file of your Bukkit/Spigot server. For security reasons, don't post this Id on Forums.
|
||||
|
||||
This plugin will automatically detect if BungeeCord is running and handle premium checks on BungeeCord.
|
||||
|
||||
####Could premium players have a premium UUID and Skin?
|
||||
Since 0.7 both features are implemented. You can check the config.yml in order to activate it.
|
||||
|
||||
####Is this plugin compatible with Cauldron?
|
||||
It's not tested yet, but all needed methods also exists in Cauldron so it could work together.
|
||||
|
||||
***
|
||||
|
||||
###Useful Links:
|
||||
* [Login Protocol](http://wiki.vg/Protocol#Login)
|
||||
* [Protocol Encryption](http://wiki.vg/Protocol_Encryption)
|
||||
BIN
bukkit/lib/ProtocolSupport b337.jar
Normal file
BIN
bukkit/lib/ProtocolSupport b337.jar
Normal file
Binary file not shown.
BIN
bukkit/lib/UltraAuth v2.0.2.jar
Normal file
BIN
bukkit/lib/UltraAuth v2.0.2.jar
Normal file
Binary file not shown.
155
bukkit/pom.xml
Normal file
155
bukkit/pom.xml
Normal file
@@ -0,0 +1,155 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.0</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin.bukkit</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLoginBukkit</name>
|
||||
|
||||
<repositories>
|
||||
<!--Bukkit-Server-API -->
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--ProtocolLib-->
|
||||
<repository>
|
||||
<id>dmulloy2-repo</id>
|
||||
<url>http://repo.dmulloy2.net/content/groups/public/</url>
|
||||
</repository>
|
||||
|
||||
<!--Authme Reloaded-->
|
||||
<repository>
|
||||
<id>xephi-repo</id>
|
||||
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--xAuth-->
|
||||
<repository>
|
||||
<id>luricos.de-repo</id>
|
||||
<url>http://repo.luricos.de/bukkit-plugins/</url>
|
||||
</repository>
|
||||
|
||||
<!--Github automatic maven builds-->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!--Server API-->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.9-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Library for listening and sending Minecraft packets-->
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>3.6.5-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>protcolsupport</groupId>
|
||||
<artifactId>ProtocolSupport</artifactId>
|
||||
<version>Build-337</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/ProtocolSupport b337.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!--Login Plugins-->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.2-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.lenis0012</groupId>
|
||||
<artifactId>LoginSecurity-2</artifactId>
|
||||
<version>-9c09e73b7f-1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.RoyalDev</groupId>
|
||||
<artifactId>RoyalAuth</artifactId>
|
||||
<version>-e21354a9b7-1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.luricos.bukkit</groupId>
|
||||
<artifactId>xAuth</artifactId>
|
||||
<version>2.6</version>
|
||||
<optional>true</optional>
|
||||
<!--These artifacts produce conflicts on downloading-->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!--No maven repository :(-->
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyCore</artifactId>
|
||||
<version>10.7.7</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyCore v10.7.7.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyLogin</artifactId>
|
||||
<version>7.23</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ultraauth</groupId>
|
||||
<artifactId>ultraauth</artifactId>
|
||||
<version>2.0.2</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/UltraAuth v2.0.2.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,50 +1,48 @@
|
||||
package com.github.games647.fastlogin;
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Encryption and decryption minecraft util for connection between servers
|
||||
* and paid minecraft account clients.
|
||||
*
|
||||
* Source: https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java
|
||||
*
|
||||
* Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8
|
||||
*/
|
||||
public class Encryption {
|
||||
public class EncryptionUtil {
|
||||
|
||||
public static KeyPair generateKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
|
||||
keypairgenerator.initialize(1024);
|
||||
return keypairgenerator.generateKeyPair();
|
||||
keyPairGenerator.initialize(1_024);
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
|
||||
//Should be existing in every vm
|
||||
return null;
|
||||
throw new ExceptionInInitializerError(nosuchalgorithmexception);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getServerIdHash(String serverId, PublicKey publickey, SecretKey secretkey) {
|
||||
public static byte[] getServerIdHash(String serverId, PublicKey publicKey, SecretKey secretKey) {
|
||||
return digestOperation("SHA-1"
|
||||
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretkey.getEncoded(), publickey.getEncoded()});
|
||||
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded()});
|
||||
}
|
||||
|
||||
private static byte[] digestOperation(String algo, byte[]... content) {
|
||||
@@ -61,26 +59,26 @@ public class Encryption {
|
||||
}
|
||||
}
|
||||
|
||||
public static PublicKey decodePublicKey(byte[] encodedKey) {
|
||||
try {
|
||||
X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
|
||||
KeyFactory keyfactory = KeyFactory.getInstance("RSA");
|
||||
// public static PublicKey decodePublicKey(byte[] encodedKey) {
|
||||
// try {
|
||||
// KeyFactory keyfactory = KeyFactory.getInstance("RSA");
|
||||
//
|
||||
// X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
|
||||
// return keyfactory.generatePublic(x509encodedkeyspec);
|
||||
// } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
|
||||
// //ignore
|
||||
// }
|
||||
//
|
||||
// System.err.println("Public key reconstitute failed!");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return keyfactory.generatePublic(x509encodedkeyspec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
|
||||
;
|
||||
}
|
||||
|
||||
System.err.println("Public key reconstitute failed!");
|
||||
return null;
|
||||
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] encryptedSharedKey) {
|
||||
return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES");
|
||||
}
|
||||
|
||||
public static SecretKey decryptSharedKey(PrivateKey privatekey, byte[] encryptedSharedKey) {
|
||||
return new SecretKeySpec(decryptData(privatekey, encryptedSharedKey), "AES");
|
||||
}
|
||||
|
||||
public static byte[] decryptData(Key key, byte[] abyte) {
|
||||
return cipherOperation(Cipher.DECRYPT_MODE, key, abyte);
|
||||
public static byte[] decryptData(Key key, byte[] data) {
|
||||
return cipherOperation(Cipher.DECRYPT_MODE, key, data);
|
||||
}
|
||||
|
||||
private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
|
||||
@@ -107,19 +105,19 @@ public class Encryption {
|
||||
System.err.println("Cipher creation failed!");
|
||||
return null;
|
||||
}
|
||||
//
|
||||
// public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
|
||||
// try {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||||
//
|
||||
// cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
|
||||
// return cipher;
|
||||
// } catch (GeneralSecurityException generalsecurityexception) {
|
||||
// throw new RuntimeException(generalsecurityexception);
|
||||
// }
|
||||
// }
|
||||
|
||||
public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||||
|
||||
cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
|
||||
return cipher;
|
||||
} catch (GeneralSecurityException generalsecurityexception) {
|
||||
throw new RuntimeException(generalsecurityexception);
|
||||
}
|
||||
}
|
||||
|
||||
private Encryption() {
|
||||
private EncryptionUtil() {
|
||||
//utility
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.comphenix.protocol.AsynchronousManager;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.utility.SafeCacheBuilder;
|
||||
import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
|
||||
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
import com.github.games647.fastlogin.bukkit.listener.BukkitJoinListener;
|
||||
import com.github.games647.fastlogin.bukkit.listener.BungeeCordListener;
|
||||
import com.github.games647.fastlogin.bukkit.listener.EncryptionPacketListener;
|
||||
import com.github.games647.fastlogin.bukkit.listener.ProtocolSupportListener;
|
||||
import com.github.games647.fastlogin.bukkit.listener.StartPacketListener;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
|
||||
*/
|
||||
public class FastLoginBukkit extends JavaPlugin {
|
||||
|
||||
private static final int WORKER_THREADS = 5;
|
||||
|
||||
public static UUID parseId(String withoutDashes) {
|
||||
return UUID.fromString(withoutDashes.substring(0, 8)
|
||||
+ "-" + withoutDashes.substring(8, 12)
|
||||
+ "-" + withoutDashes.substring(12, 16)
|
||||
+ "-" + withoutDashes.substring(16, 20)
|
||||
+ "-" + withoutDashes.substring(20, 32));
|
||||
}
|
||||
|
||||
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
|
||||
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
|
||||
|
||||
|
||||
private boolean bungeeCord;
|
||||
private Storage storage;
|
||||
|
||||
//this map is thread-safe for async access (Packet Listener)
|
||||
//SafeCacheBuilder is used in order to be version independent
|
||||
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
|
||||
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
|
||||
.expireAfterWrite(1, TimeUnit.MINUTES)
|
||||
//mapped by ip:port -> PlayerSession
|
||||
.build(new CacheLoader<String, PlayerSession>() {
|
||||
|
||||
@Override
|
||||
public PlayerSession load(String key) throws Exception {
|
||||
//A key should be inserted manually on start packet
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
});
|
||||
|
||||
private BukkitAuthPlugin authPlugin;
|
||||
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
|
||||
if (getServer().getOnlineMode()) {
|
||||
//we need to require offline to prevent a session request for a offline player
|
||||
getLogger().severe("Server have to be in offline mode");
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord");
|
||||
boolean hookFound = registerHooks();
|
||||
if (bungeeCord) {
|
||||
getLogger().info("BungeeCord setting detected. No auth plugin is required");
|
||||
} else if (!hookFound) {
|
||||
getLogger().info("No auth plugin were found and bungeecord is deactivated. "
|
||||
+ "Either one or both of the checks have to pass in order to use this plugin");
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bungeeCord) {
|
||||
//check for incoming messages from the bungeecord version of this plugin
|
||||
getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this));
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, getName());
|
||||
//register listeners on success
|
||||
} else {
|
||||
String driver = getConfig().getString("driver");
|
||||
String host = getConfig().getString("host", "");
|
||||
int port = getConfig().getInt("port", 3306);
|
||||
String database = getConfig().getString("database");
|
||||
|
||||
String username = getConfig().getString("username", "");
|
||||
String password = getConfig().getString("password", "");
|
||||
|
||||
this.storage = new Storage(this, driver, host, port, database, username, password);
|
||||
try {
|
||||
storage.createTables();
|
||||
} catch (SQLException sqlEx) {
|
||||
getLogger().log(Level.SEVERE, "Failed to create database tables. Disabling plugin...", sqlEx);
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) {
|
||||
getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this);
|
||||
} else {
|
||||
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
|
||||
|
||||
//we are performing HTTP request on these so run it async (seperate from the Netty IO threads)
|
||||
AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager();
|
||||
|
||||
StartPacketListener startPacketListener = new StartPacketListener(this, protocolManager);
|
||||
EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this, protocolManager);
|
||||
|
||||
asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS);
|
||||
asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS);
|
||||
}
|
||||
}
|
||||
|
||||
getServer().getPluginManager().registerEvents(new BukkitJoinListener(this), this);
|
||||
|
||||
//register commands using a unique name
|
||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
||||
getCommand("cracked").setExecutor(new CrackedCommand(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
//clean up
|
||||
session.clear();
|
||||
|
||||
//remove old blacklists
|
||||
for (Player player : getServer().getOnlinePlayers()) {
|
||||
player.removeMetadata(getName(), this);
|
||||
}
|
||||
|
||||
if (storage != null) {
|
||||
storage.close();
|
||||
}
|
||||
}
|
||||
|
||||
public String generateStringPassword() {
|
||||
return RandomStringUtils.random(8, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
|
||||
* account)
|
||||
*
|
||||
* @return a thread-safe session map
|
||||
*/
|
||||
public ConcurrentMap<String, PlayerSession> getSessions() {
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between the client and server
|
||||
*
|
||||
* @return the server KeyPair
|
||||
*/
|
||||
public KeyPair getServerKey() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the auth plugin hook in order to interact with the plugins. This can be null if no supporting auth plugin
|
||||
* was found.
|
||||
*
|
||||
* @return interface to any supported auth plugin
|
||||
*/
|
||||
public BukkitAuthPlugin getAuthPlugin() {
|
||||
return authPlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the a connection in order to access important features from the Mojang API.
|
||||
*
|
||||
* @return the connector instance
|
||||
*/
|
||||
public MojangApiConnector getApiConnector() {
|
||||
return mojangApiConnector;
|
||||
}
|
||||
|
||||
private boolean registerHooks() {
|
||||
BukkitAuthPlugin authPluginHook = null;
|
||||
try {
|
||||
String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
|
||||
//Look through all classes in the hooks package and look for supporting plugins on the server
|
||||
for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
|
||||
//remove the hook suffix
|
||||
String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
|
||||
Class<?> clazz = clazzInfo.load();
|
||||
//uses only member classes which uses AuthPlugin interface (skip interfaces)
|
||||
if (BukkitAuthPlugin.class.isAssignableFrom(clazz)
|
||||
//check only for enabled plugins. A single plugin could be disabled by plugin managers
|
||||
&& getServer().getPluginManager().isPluginEnabled(pluginName)) {
|
||||
authPluginHook = (BukkitAuthPlugin) clazz.newInstance();
|
||||
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException | IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
|
||||
}
|
||||
|
||||
if (authPluginHook == null) {
|
||||
//run this check for exceptions (errors) and not found plugins
|
||||
getLogger().warning("No support offline Auth plugin found. ");
|
||||
return false;
|
||||
}
|
||||
|
||||
authPlugin = authPluginHook;
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isBungeeCord() {
|
||||
return bungeeCord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
|
||||
public class ForceLoginTask implements Runnable {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
private final Player player;
|
||||
|
||||
public ForceLoginTask(FastLoginBukkit plugin, Player player) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//remove the bungeecord identifier
|
||||
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
|
||||
PlayerSession session = plugin.getSessions().get(id);
|
||||
|
||||
//blacklist this target player for BungeeCord Id brute force attacks
|
||||
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
|
||||
//check if it's the same player as we checked before
|
||||
|
||||
final BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
if (session == null || !player.getName().equals(session.getUsername()) || authPlugin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage storage = plugin.getStorage();
|
||||
PlayerProfile playerProfile = null;
|
||||
if (storage != null) {
|
||||
playerProfile = storage.getProfile(session.getUsername(), false);
|
||||
}
|
||||
|
||||
if (session.isVerified()) {
|
||||
boolean success = true;
|
||||
if (playerProfile != null) {
|
||||
playerProfile.setUuid(session.getUuid());
|
||||
playerProfile.setPremium(true);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (session.needsRegistration()) {
|
||||
if (forceRegister(authPlugin, player)) {
|
||||
storage.save(playerProfile);
|
||||
}
|
||||
} else {
|
||||
if (forceLogin(authPlugin, player)) {
|
||||
storage.save(playerProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (playerProfile != null) {
|
||||
storage.save(playerProfile);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean forceRegister(BukkitAuthPlugin authPlugin, Player player) {
|
||||
plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName());
|
||||
|
||||
String generatedPassword = plugin.generateStringPassword();
|
||||
boolean success = authPlugin.forceRegister(player, generatedPassword);
|
||||
player.sendMessage(ChatColor.DARK_GREEN + "Auto registered with password: " + generatedPassword);
|
||||
player.sendMessage(ChatColor.DARK_GREEN + "You may want change it?");
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean forceLogin(BukkitAuthPlugin authPlugin, Player player) {
|
||||
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
|
||||
boolean success = authPlugin.forceLogin(player);
|
||||
player.sendMessage(ChatColor.DARK_GREEN + "Auto logged in");
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
public class MojangApiConnector {
|
||||
|
||||
//http connection, read timeout and user agent for a connection to mojang api servers
|
||||
private static final int TIMEOUT = 1 * 1_000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
//mojang api check to prove a player is logged in minecraft and made a join server request
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
//only premium (paid account) users have a uuid from here
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
//this includes a-zA-Z1-9_
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
|
||||
//compile the pattern only on plugin enable -> and this have to be threadsafe
|
||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
public MojangApiConnector(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param playerName
|
||||
* @return null on non-premium
|
||||
*/
|
||||
public UUID getPremiumUUID(String playerName) {
|
||||
//check if it's a valid playername
|
||||
if (playernameMatcher.matcher(playerName).matches()) {
|
||||
//only make a API call if the name is valid existing mojang account
|
||||
try {
|
||||
HttpURLConnection connection = getConnection(UUID_LINK + playerName);
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != null && !line.equals("null")) {
|
||||
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) userData.get("id");
|
||||
return parseId(uuid);
|
||||
}
|
||||
}
|
||||
//204 - no content for not found
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasJoinedServer(PlayerSession session, String serverId) {
|
||||
try {
|
||||
String url = HAS_JOINED_URL + "username=" + session.getUsername() + "&serverId=" + serverId;
|
||||
HttpURLConnection conn = getConnection(url);
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != null && !line.equals("null")) {
|
||||
//validate parsing
|
||||
//http://wiki.vg/Protocol_Encryption#Server
|
||||
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) userData.get("id");
|
||||
session.setUuid(parseId(uuid));
|
||||
|
||||
JSONArray properties = (JSONArray) userData.get("properties");
|
||||
JSONObject skinProperty = (JSONObject) properties.get(0);
|
||||
|
||||
String propertyName = (String) skinProperty.get("name");
|
||||
if (propertyName.equals("textures")) {
|
||||
String skinValue = (String) skinProperty.get("value");
|
||||
String signature = (String) skinProperty.get("signature");
|
||||
session.setSkin(WrappedSignedProperty.fromValues(propertyName, skinValue, signature));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
//catch not only ioexceptions also parse and NPE on unexpected json format
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to verify session", ex);
|
||||
}
|
||||
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
return false;
|
||||
}
|
||||
|
||||
private UUID parseId(String withoutDashes) {
|
||||
return UUID.fromString(withoutDashes.substring(0, 8)
|
||||
+ "-" + withoutDashes.substring(8, 12)
|
||||
+ "-" + withoutDashes.substring(12, 16)
|
||||
+ "-" + withoutDashes.substring(16, 20)
|
||||
+ "-" + withoutDashes.substring(20, 32));
|
||||
}
|
||||
|
||||
private HttpURLConnection getConnection(String url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
connection.setReadTimeout(TIMEOUT);
|
||||
//the new Mojang API just uses json as response
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PlayerProfile {
|
||||
|
||||
private final String playerName;
|
||||
|
||||
private long userId;
|
||||
|
||||
private UUID uuid;
|
||||
private boolean premium;
|
||||
private String lastIp;
|
||||
private long lastLogin;
|
||||
|
||||
public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium
|
||||
, String lastIp, long lastLogin) {
|
||||
this.userId = userId;
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
|
||||
public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
|
||||
this.userId = -1;
|
||||
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public synchronized long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
protected synchronized void setUserId(long generatedId) {
|
||||
this.userId = generatedId;
|
||||
}
|
||||
|
||||
public synchronized UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public synchronized void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public synchronized boolean isPremium() {
|
||||
return premium;
|
||||
}
|
||||
|
||||
public synchronized void setPremium(boolean premium) {
|
||||
this.premium = premium;
|
||||
}
|
||||
|
||||
public synchronized String getLastIp() {
|
||||
return lastIp;
|
||||
}
|
||||
|
||||
public synchronized void setLastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public long getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public synchronized void setLastLogin(long lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
|
||||
/**
|
||||
* Represents a client connecting to the server.
|
||||
*
|
||||
* This session is invalid if the player disconnects or the login was successful
|
||||
*/
|
||||
public class PlayerSession {
|
||||
|
||||
private final String username;
|
||||
private final String serverId;
|
||||
private final byte[] verifyToken;
|
||||
|
||||
private UUID uuid;
|
||||
private WrappedSignedProperty skinProperty;
|
||||
private boolean verified;
|
||||
private boolean registered;
|
||||
|
||||
public PlayerSession(String username, String serverId, byte[] verifyToken) {
|
||||
this.username = username;
|
||||
this.serverId = serverId;
|
||||
this.verifyToken = ArrayUtils.clone(verifyToken);
|
||||
}
|
||||
|
||||
public PlayerSession(String username) {
|
||||
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the random generated server id. This makes sure the request
|
||||
* sent from the client is just for this server.
|
||||
*
|
||||
* See this for details
|
||||
* http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
|
||||
*
|
||||
* Empty if it's a BungeeCord connection
|
||||
*
|
||||
* @return random generated server id
|
||||
*/
|
||||
public String getServerId() {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the verify token the server sent to the client.
|
||||
*
|
||||
* Empty if it's a BungeeCord connection
|
||||
*
|
||||
* @return the verify token from the server
|
||||
*/
|
||||
public byte[] getVerifyToken() {
|
||||
return ArrayUtils.clone(verifyToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the username the player sent to the server
|
||||
*
|
||||
* @return the client sent username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the premium skin of this player
|
||||
*
|
||||
* @return skin property or null if the player has no skin or is a cracked account
|
||||
*/
|
||||
public synchronized WrappedSignedProperty getSkin() {
|
||||
return this.skinProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the premium skin property which was retrieved by the session server
|
||||
*
|
||||
* @param skinProperty premium skin property
|
||||
*/
|
||||
public synchronized void setSkin(WrappedSignedProperty skinProperty) {
|
||||
this.skinProperty = skinProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the account of this player already exists
|
||||
*
|
||||
* @param registered whether the account exists
|
||||
*/
|
||||
public synchronized void setRegistered(boolean registered) {
|
||||
this.registered = registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the account of this player already exists.
|
||||
*
|
||||
* @return whether the account exists
|
||||
*/
|
||||
public synchronized boolean needsRegistration() {
|
||||
return !registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @param verified whether the player has valid session
|
||||
*/
|
||||
public synchronized void setVerified(boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the premium UUID of this player
|
||||
*
|
||||
* @return the premium UUID or null if not fetched
|
||||
*/
|
||||
public synchronized UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the online UUID if it's fetched
|
||||
*
|
||||
* @param uuid premium UUID
|
||||
*/
|
||||
public synchronized void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @return whether the player has a valid session
|
||||
*/
|
||||
public synchronized boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.comphenix.protocol.utility.SafeCacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Storage {
|
||||
|
||||
private static final String PREMIUM_TABLE = "premium";
|
||||
|
||||
private final ConcurrentMap<String, PlayerProfile> profileCache = SafeCacheBuilder
|
||||
.<String, PlayerProfile>newBuilder()
|
||||
.concurrencyLevel(20)
|
||||
.expireAfterAccess(30, TimeUnit.MINUTES)
|
||||
.build(new CacheLoader<String, PlayerProfile>() {
|
||||
@Override
|
||||
public PlayerProfile load(String key) throws Exception {
|
||||
//should be fetched manually
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
});
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
public Storage(FastLoginBukkit plugin, String driver, String host, int port, String databasePath
|
||||
, String user, String pass) {
|
||||
this.plugin = plugin;
|
||||
|
||||
HikariConfig databaseConfig = new HikariConfig();
|
||||
databaseConfig.setUsername(user);
|
||||
databaseConfig.setPassword(pass);
|
||||
databaseConfig.setDriverClassName(driver);
|
||||
|
||||
databasePath = databasePath.replace("{pluginDir}", plugin.getDataFolder().getAbsolutePath());
|
||||
|
||||
String jdbcUrl = "jdbc:";
|
||||
if (driver.contains("sqlite")) {
|
||||
jdbcUrl += "sqlite" + "://" + databasePath;
|
||||
databaseConfig.setConnectionTestQuery("SELECT 1");
|
||||
} else {
|
||||
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
|
||||
}
|
||||
|
||||
databaseConfig.setJdbcUrl(jdbcUrl);
|
||||
this.dataSource = new HikariDataSource(databaseConfig);
|
||||
}
|
||||
|
||||
public void createTables() throws SQLException {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
Statement statement = con.createStatement();
|
||||
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
|
||||
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
|
||||
+ "`UUID` CHAR(36), "
|
||||
+ "`Name` VARCHAR(16) NOT NULL, "
|
||||
+ "`Premium` BOOLEAN NOT NULL, "
|
||||
+ "`LastIp` VARCHAR(255) NOT NULL, "
|
||||
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
|
||||
+ "UNIQUE (`UUID`), "
|
||||
//the premium shouldn't steal the cracked account by changing the name
|
||||
+ "UNIQUE (`Name`) "
|
||||
+ ")";
|
||||
|
||||
if (dataSource.getJdbcUrl().contains("sqlite")) {
|
||||
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
|
||||
}
|
||||
|
||||
statement.executeUpdate(createDataStmt);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerProfile getProfile(String name, boolean fetch) {
|
||||
if (profileCache.containsKey(name)) {
|
||||
return profileCache.get(name);
|
||||
} else if (fetch) {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE
|
||||
+ " WHERE `Name`=? LIMIT 1");
|
||||
loadStatement.setString(1, name);
|
||||
|
||||
ResultSet resultSet = loadStatement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
long userId = resultSet.getInt(1);
|
||||
|
||||
String unparsedUUID = resultSet.getString(2);
|
||||
UUID uuid;
|
||||
if (unparsedUUID == null) {
|
||||
uuid = null;
|
||||
} else {
|
||||
uuid = FastLoginBukkit.parseId(unparsedUUID);
|
||||
}
|
||||
|
||||
// String name = resultSet.getString(3);
|
||||
boolean premium = resultSet.getBoolean(4);
|
||||
String lastIp = resultSet.getString(5);
|
||||
long lastLogin = resultSet.getTimestamp(6).getTime();
|
||||
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
|
||||
profileCache.put(name, playerProfile);
|
||||
return playerProfile;
|
||||
} else {
|
||||
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
|
||||
profileCache.put(name, crackedProfile);
|
||||
return crackedProfile;
|
||||
}
|
||||
} catch (SQLException sqlEx) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean save(PlayerProfile playerProfile) {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
|
||||
UUID uuid = playerProfile.getUuid();
|
||||
|
||||
if (playerProfile.getUserId() == -1) {
|
||||
PreparedStatement saveStatement = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
|
||||
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
|
||||
|
||||
if (uuid == null) {
|
||||
saveStatement.setString(1, null);
|
||||
} else {
|
||||
saveStatement.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStatement.setString(2, playerProfile.getPlayerName());
|
||||
saveStatement.setBoolean(3, playerProfile.isPremium());
|
||||
saveStatement.setString(4, playerProfile.getLastIp());
|
||||
saveStatement.execute();
|
||||
|
||||
ResultSet generatedKeys = saveStatement.getGeneratedKeys();
|
||||
if (generatedKeys != null && generatedKeys.next()) {
|
||||
playerProfile.setUserId(generatedKeys.getInt(1));
|
||||
}
|
||||
} else {
|
||||
PreparedStatement saveStatement = con.prepareStatement("UPDATE " + PREMIUM_TABLE
|
||||
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
|
||||
|
||||
if (uuid == null) {
|
||||
saveStatement.setString(1, null);
|
||||
} else {
|
||||
saveStatement.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStatement.setString(2, playerProfile.getPlayerName());
|
||||
saveStatement.setBoolean(3, playerProfile.isPremium());
|
||||
saveStatement.setString(4, playerProfile.getLastIp());
|
||||
// saveStatement.setTimestamp(5, new Timestamp(playerProfile.getLastLogin()));
|
||||
|
||||
saveStatement.setLong(5, playerProfile.getUserId());
|
||||
saveStatement.execute();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
dataSource.close();
|
||||
profileCache.clear();
|
||||
}
|
||||
|
||||
private void closeQuietly(Connection con) {
|
||||
if (con != null) {
|
||||
try {
|
||||
con.close();
|
||||
} catch (SQLException sqlEx) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to close connection", sqlEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.github.games647.fastlogin.bukkit.commands;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerProfile;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class CrackedCommand implements CommandExecutor {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public CrackedCommand(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Only players can remove themselves from the premium list");
|
||||
return true;
|
||||
}
|
||||
|
||||
final Player player = (Player) sender;
|
||||
// UUID uuid = player.getUniqueId();
|
||||
|
||||
if (plugin.isBungeeCord()) {
|
||||
notifiyBungeeCord((Player) sender);
|
||||
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
|
||||
} else {
|
||||
//todo: load async if it's not in the cache anymore
|
||||
final PlayerProfile profile = plugin.getStorage().getProfile(player.getName(), true);
|
||||
if (profile.isPremium()) {
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players");
|
||||
profile.setPremium(false);
|
||||
profile.setUuid(null);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
plugin.getStorage().save(profile);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "You are not in the premium list");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "NOT IMPLEMENTED YET");
|
||||
//todo:
|
||||
// String playerName = args[0];
|
||||
// boolean existed = plugin.getEnabledPremium().remove(playerName);
|
||||
// if (existed) {
|
||||
// sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players");
|
||||
// notifiyBungeeCord((Player) sender);
|
||||
// } else {
|
||||
// sender.sendMessage(ChatColor.DARK_RED + "User is not in the premium list");
|
||||
// }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void notifiyBungeeCord(Player target) {
|
||||
if (plugin.isBungeeCord()) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
dataOutput.writeUTF("OFF");
|
||||
|
||||
target.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.games647.fastlogin.bukkit.commands;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerProfile;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Let users activate fast login by command. This only be accessible if
|
||||
* the user has access to it's account. So we can make sure that not another
|
||||
* person with a paid account and the same username can steal his account.
|
||||
*/
|
||||
public class PremiumCommand implements CommandExecutor {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public PremiumCommand(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Only players can add themselves as premium");
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
// UUID uuid = player.getUniqueId();
|
||||
|
||||
if (plugin.isBungeeCord()) {
|
||||
notifiyBungeeCord(player);
|
||||
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
|
||||
} else {
|
||||
// //todo: load async if it's not in the cache anymore
|
||||
final PlayerProfile profile = plugin.getStorage().getProfile(player.getName(), true);
|
||||
if (profile.isPremium()) {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list");
|
||||
} else {
|
||||
//todo: resolve uuid
|
||||
profile.setPremium(true);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
plugin.getStorage().save(profile);
|
||||
}
|
||||
});
|
||||
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "NOT IMPLEMENTED YET");
|
||||
//todo: async load
|
||||
// String playerName = args[0];
|
||||
// boolean didntexist = plugin.getEnabledPremium().add(playerName);
|
||||
// if (!didntexist) {
|
||||
// sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list");
|
||||
// } else {
|
||||
// sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
|
||||
// }
|
||||
// notifiyBungeeCord();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void notifiyBungeeCord(Player target) {
|
||||
if (plugin.isBungeeCord()) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
dataOutput.writeUTF("ON");
|
||||
|
||||
target.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/Xephi/AuthMeReloaded/
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/authme-reloaded/
|
||||
* Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
|
||||
*/
|
||||
public class AuthMeHook implements BukkitAuthPlugin {
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(Player player) {
|
||||
//skips registration and login
|
||||
NewAPI.getInstance().forceLogin(player);
|
||||
//commented because the operation above is performed async -> race conditions
|
||||
// return NewAPI.getInstance().isAuthenticated(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
return NewAPI.getInstance().isRegistered(playerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
NewAPI.getInstance().forceRegister(player, password);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin in Bukkit/Spigot/... servers
|
||||
*/
|
||||
public interface BukkitAuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player after
|
||||
* the player joined successfully the server.
|
||||
*
|
||||
* <strong>This operation will be performed async while the player successfully
|
||||
* joined the server.</strong>
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceLogin(Player player);
|
||||
|
||||
/**
|
||||
* Checks whether an account exists for this player name.
|
||||
*
|
||||
* This check should check if a cracked player account exists
|
||||
* so we can be sure the premium player doesn't steal the account
|
||||
* of that player.
|
||||
*
|
||||
* This operation will be performed async while the player is
|
||||
* connecting.
|
||||
*
|
||||
* @param playerName player name
|
||||
* @return if the player has an account
|
||||
* @throws Exception if an error occurred
|
||||
*/
|
||||
boolean isRegistered(String playerName) throws Exception;
|
||||
|
||||
/**
|
||||
* Forces a register in order to protect the paid account.
|
||||
*
|
||||
* <strong>This operation will be performed async while the player successfully
|
||||
* joined the server.</strong>
|
||||
*
|
||||
* After a successful registration the player should be logged
|
||||
* in too.
|
||||
*
|
||||
* The method will be called only for premium accounts.
|
||||
* So it's recommended to set additionally premium property
|
||||
* if possible.
|
||||
*
|
||||
* Background: If we don't register an account, cracked players
|
||||
* could steal the unregistered account from the paid
|
||||
* player account
|
||||
*
|
||||
* @param player the premium account
|
||||
* @param password a strong random generated password
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceRegister(Player player, String password);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
|
||||
import de.st_ddt.crazylogin.CrazyLogin;
|
||||
import de.st_ddt.crazylogin.data.LoginPlayerData;
|
||||
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
|
||||
import de.st_ddt.crazylogin.listener.PlayerListener;
|
||||
import de.st_ddt.crazylogin.metadata.Authenticated;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/ST-DDT/CrazyLogin
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/server-mods/crazylogin/
|
||||
*/
|
||||
public class CrazyLoginHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
|
||||
private final PlayerListener playerListener = getListener();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe operation
|
||||
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin
|
||||
, new Callable<LoginPlayerData>() {
|
||||
@Override
|
||||
public LoginPlayerData call() throws Exception {
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData != null) {
|
||||
//mark the account as logged in
|
||||
playerData.setLoggedIn(true);
|
||||
|
||||
String ip = player.getAddress().getAddress().getHostAddress();
|
||||
//this should be done after login to restore the inventory, unhide players, prevent potential memory leaks...
|
||||
//from: https://github.com/ST-DDT/CrazyLogin/blob/master/src/main/java/de/st_ddt/crazylogin/CrazyLogin.java#L1948
|
||||
playerData.resetLoginFails();
|
||||
player.setFireTicks(0);
|
||||
|
||||
if (playerListener != null) {
|
||||
playerListener.removeMovementBlocker(player);
|
||||
playerListener.disableHidenInventory(player);
|
||||
playerListener.disableSaveLogin(player);
|
||||
playerListener.unhidePlayer(player);
|
||||
}
|
||||
|
||||
//loginFailuresPerIP.remove(IP);
|
||||
//illegalCommandUsesPerIP.remove(IP);
|
||||
//tempBans.remove(IP);
|
||||
playerData.addIP(ip);
|
||||
player.setMetadata("Authenticated", new Authenticated(crazyLoginPlugin, player));
|
||||
crazyLoginPlugin.unregisterDynamicHooks();
|
||||
return playerData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
LoginPlayerData result = future.get();
|
||||
if (result != null) {
|
||||
//SQL-Queries should run async
|
||||
crazyLoginPlugin.getCrazyDatabase().saveWithoutPassword(result);
|
||||
return true;
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
return crazyLoginPlugin.getPlayerData(playerName) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final Player player, String password) {
|
||||
final CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
|
||||
|
||||
//this executes a sql query and accesses only thread safe collections so we can run it async
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData == null) {
|
||||
//create a fake account - this will be saved to the database with the password=FAILEDLOADING
|
||||
//user cannot login with that password unless the admin uses plain text
|
||||
//this automatically marks the player as logged in
|
||||
playerData = new LoginPlayerData(player);
|
||||
crazyDatabase.save(playerData);
|
||||
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private PlayerListener getListener() {
|
||||
PlayerListener listener;
|
||||
try {
|
||||
listener = FuzzyReflection.getFieldValue(crazyLoginPlugin, PlayerListener.class, true);
|
||||
} catch (Exception ex) {
|
||||
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to get the listener instance for auto login", ex);
|
||||
listener = null;
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.lenis0012.bukkit.ls.LoginSecurity;
|
||||
import com.lenis0012.bukkit.ls.data.DataManager;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/lenis0012/LoginSecurity-2 Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/loginsecurity/ Spigot:
|
||||
* https://www.spigotmc.org/resources/loginsecurity.19362/
|
||||
*
|
||||
* on join:
|
||||
* https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282
|
||||
*/
|
||||
public class LoginSecurityHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final LoginSecurity securityPlugin = LoginSecurity.instance;
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//Login command of this plugin: (How the plugin logs the player in)
|
||||
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39
|
||||
|
||||
//not thread-safe operation
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(securityPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
String name = player.getName().toLowerCase();
|
||||
|
||||
//mark the user as logged in
|
||||
securityPlugin.authList.remove(name);
|
||||
//cancel timeout timer
|
||||
securityPlugin.thread.timeout.remove(name);
|
||||
//remove effects and restore location
|
||||
securityPlugin.rehabPlayer(player, name);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
securityPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L296
|
||||
DataManager dataManager = securityPlugin.data;
|
||||
|
||||
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L283
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(Charsets.UTF_8));
|
||||
return dataManager.isRegistered(offlineUuid.toString().replace("-", ""));
|
||||
//check for loginsecurity sessions in order to prevent a sql query?
|
||||
//sesUse && thread.getSession().containsKey(uuid) && checkLastIp(player)) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final Player player, final String password) {
|
||||
final DataManager dataManager = securityPlugin.data;
|
||||
|
||||
UUID playerUUID = player.getUniqueId();
|
||||
final String uuidString = playerUUID.toString().replace("-", "");
|
||||
final InetAddress ipAddress = player.getAddress().getAddress();
|
||||
final String passwordHash = securityPlugin.hasher.hash(password);
|
||||
|
||||
//this executes a sql query without interacting with other parts so we can run it async.
|
||||
dataManager.register(uuidString, passwordHash, securityPlugin.hasher.getTypeId(), ipAddress.toString());
|
||||
return forceLogin(player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.royaldev.royalauth.AuthPlayer;
|
||||
import org.royaldev.royalauth.Config;
|
||||
import org.royaldev.royalauth.RoyalAuth;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/RoyalDev/RoyalAuth
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/royalauth/
|
||||
*/
|
||||
public class RoyalAuthHook implements BukkitAuthPlugin {
|
||||
|
||||
private final RoyalAuth royalAuthPlugin = (RoyalAuth) Bukkit.getPluginManager().getPlugin("RoyalAuth");
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(royalAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
|
||||
|
||||
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdLogin.java#L62
|
||||
//not thread-safe
|
||||
authPlayer.login();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
royalAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(playerName);
|
||||
return authPlayer.isRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdRegister.java#L50
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
|
||||
|
||||
boolean registerSuccess = authPlayer.setPassword(password, Config.passwordHashType);
|
||||
if (registerSuccess) {
|
||||
//login in the player after registration
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import de.luricos.bukkit.xAuth.xAuth;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/LycanDevelopment/xAuth/
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/xauth/
|
||||
*/
|
||||
public class xAuthHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final xAuth xAuthPlugin = xAuth.getPlugin();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
if (xAuthPlayer != null) {
|
||||
//we checked that the player is premium (paid account)
|
||||
//unprotect the inventory, op status...
|
||||
xAuthPlayer.setPremium(true);
|
||||
|
||||
xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
//this will load the player if it's not in the cache
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(playerName);
|
||||
return xAuthPlayer != null && xAuthPlayer.isRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final Player player, final String password) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
if (xAuthPlayer != null) {
|
||||
//this should run async because the plugin executes a sql query, but the method
|
||||
//accesses non thread-safe collections :(
|
||||
boolean registerSuccess = xAuthPlugin.getAuthClass(xAuthPlayer)
|
||||
.adminRegister(player.getName(), password, null);
|
||||
|
||||
if (registerSuccess) {
|
||||
//login in the player after registration
|
||||
return forceLogin(player);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.ForceLoginTask;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerSession;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
/**
|
||||
* This listener tells authentication plugins if the player has a premium account and we checked it successfully. So the
|
||||
* plugin can skip authentication.
|
||||
*/
|
||||
public class BukkitJoinListener implements Listener {
|
||||
|
||||
private static final long DELAY_LOGIN = 1 * 20L / 2;
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public BukkitJoinListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
|
||||
final Player player = joinEvent.getPlayer();
|
||||
|
||||
//removing the session because we now use it
|
||||
final PlayerSession session = plugin.getSessions().get(player.getAddress().toString());
|
||||
if (session != null && plugin.getConfig().getBoolean("forwardSkin")) {
|
||||
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
|
||||
WrappedSignedProperty skin = session.getSkin();
|
||||
if (skin != null) {
|
||||
gameProfile.getProperties().put("textures", skin);
|
||||
}
|
||||
}
|
||||
|
||||
//Wait before auth plugin and we received a message from BungeeCord initializes the player
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin, player), DELAY_LOGIN);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
|
||||
final Player player = quitEvent.getPlayer();
|
||||
|
||||
//prevent memory leaks
|
||||
player.removeMetadata(plugin.getName(), plugin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerSession;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
/**
|
||||
* Responsible for receiving messages from a BungeeCord instance.
|
||||
*
|
||||
* This class also receives the plugin message from the bungeecord version of this plugin in order to get notified if
|
||||
* the connection is in online mode.
|
||||
*/
|
||||
public class BungeeCordListener implements PluginMessageListener {
|
||||
|
||||
private static final String FILE_NAME = "proxy-whitelist.txt";
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
//null if whitelist is empty so bungeecord support is disabled
|
||||
private final UUID proxyId;
|
||||
|
||||
public BungeeCordListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
this.proxyId = loadBungeeCordId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
|
||||
if (!channel.equals(plugin.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayDataInput dataInput = ByteStreams.newDataInput(message);
|
||||
String subchannel = dataInput.readUTF();
|
||||
plugin.getLogger().log(Level.FINEST, "Received plugin message for subchannel {0} from {1}"
|
||||
, new Object[]{subchannel, player});
|
||||
|
||||
final String playerName = dataInput.readUTF();
|
||||
|
||||
//check if the player is still online or disconnected
|
||||
final Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
|
||||
//fail if target player is blacklisted because already authed or wrong bungeecord id
|
||||
if (checkedPlayer != null && !checkedPlayer.hasMetadata(plugin.getName())) {
|
||||
//bungeecord UUID
|
||||
long mostSignificantBits = dataInput.readLong();
|
||||
long leastSignificantBits = dataInput.readLong();
|
||||
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
|
||||
//fail if BungeeCord support is disabled (id = null)
|
||||
if (sourceId.equals(proxyId)) {
|
||||
final PlayerSession playerSession = new PlayerSession(playerName);
|
||||
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
|
||||
playerSession.setVerified(true);
|
||||
playerSession.setRegistered(true);
|
||||
plugin.getSessions().put(checkedPlayer.getAddress().toString(), playerSession);
|
||||
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
|
||||
playerSession.setVerified(true);
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
try {
|
||||
//we need to check if the player is registered on Bukkit too
|
||||
if (authPlugin != null && !authPlugin.isRegistered(playerName)) {
|
||||
plugin.getSessions().put(checkedPlayer.getAddress().toString(), playerSession);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//blacklist target for the current login
|
||||
checkedPlayer.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UUID loadBungeeCordId() {
|
||||
File whitelistFile = new File(plugin.getDataFolder(), FILE_NAME);
|
||||
//create a new folder if it doesn't exist. Fail silently otherwise
|
||||
whitelistFile.getParentFile().mkdir();
|
||||
try {
|
||||
if (!whitelistFile.exists()) {
|
||||
whitelistFile.createNewFile();
|
||||
}
|
||||
|
||||
String firstLine = Files.readFirstLine(whitelistFile, Charsets.UTF_8);
|
||||
if (firstLine != null && !firstLine.isEmpty()) {
|
||||
return UUID.fromString(firstLine.trim());
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
@@ -7,30 +7,34 @@ import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedChatComponent;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.github.games647.fastlogin.Encryption;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerSession;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
/**
|
||||
* Handles incoming encryption responses from connecting clients.
|
||||
* It prevents them from reaching the server because that cannot handle
|
||||
* it in offline mode.
|
||||
*
|
||||
* Moreover this manages a started premium check from
|
||||
* this plugin. So check if all data is correct and we can prove him as a
|
||||
* owner of a paid minecraft account.
|
||||
*
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Encryption_Response
|
||||
*
|
||||
@@ -39,13 +43,11 @@ import org.json.simple.JSONValue;
|
||||
*/
|
||||
public class EncryptionPacketListener extends PacketAdapter {
|
||||
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need this type
|
||||
private final FastLogin plugin;
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
public EncryptionPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
||||
public EncryptionPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
|
||||
//run async in order to not block the server, because we make api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
|
||||
|
||||
@@ -69,36 +71,101 @@ public class EncryptionPacketListener extends PacketAdapter {
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//the player name is unknown to ProtocolLib - now uses ip:port as key
|
||||
//the player name is unknown to ProtocolLib (so getName() doesn't work) - now uses ip:port as key
|
||||
String uniqueSessionKey = player.getAddress().toString();
|
||||
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
|
||||
if (session == null) {
|
||||
disconnect(packetEvent, "Invalid request", Level.FINE
|
||||
, "Player {0} tried to send encryption response"
|
||||
+ "on an invalid connection state"
|
||||
, "Player {0} tried to send encryption response at invalid state"
|
||||
, player.getAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] sharedSecret = packet.getByteArrays().read(0);
|
||||
byte[] clientVerify = packet.getByteArrays().read(1);
|
||||
PrivateKey privateKey = plugin.getServerKey().getPrivate();
|
||||
|
||||
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
|
||||
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
|
||||
if (!checkVerifyToken(session, privateKey, packetEvent) || !encryptConnection(player, loginKey, packetEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//this makes sure the request from the client is for us
|
||||
//this might be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
|
||||
String generatedId = session.getServerId();
|
||||
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
|
||||
//generate the server id based on client and server data
|
||||
byte[] serverIdHash = EncryptionUtil.getServerIdHash(generatedId, plugin.getServerKey().getPublic(), loginKey);
|
||||
String serverId = (new BigInteger(serverIdHash)).toString(16);
|
||||
|
||||
String username = session.getUsername();
|
||||
if (plugin.getApiConnector().hasJoinedServer(session, serverId)) {
|
||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||
|
||||
session.setVerified(true);
|
||||
setPremiumUUID(session, player);
|
||||
receiveFakeStartPacket(username, player);
|
||||
} else {
|
||||
//user tried to fake a authentication
|
||||
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||
, session.getUsername(), player.getAddress(), serverId);
|
||||
}
|
||||
|
||||
//this is a fake packet; it shouldn't be send to the server
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private void setPremiumUUID(PlayerSession session, Player player) {
|
||||
UUID uuid = session.getUuid();
|
||||
if (plugin.getConfig().getBoolean("premiumUuid") && uuid != null) {
|
||||
try {
|
||||
Object networkManager = getNetworkManager(player);
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
|
||||
Field spoofField = FuzzyReflection.fromObject(networkManager).getFieldByType("spoofedUUID", UUID.class);
|
||||
spoofField.set(networkManager, uuid);
|
||||
} catch (ReflectiveOperationException reflectiveOperationException) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", reflectiveOperationException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkVerifyToken(PlayerSession session, PrivateKey privateKey, PacketEvent packetEvent) {
|
||||
byte[] requestVerify = session.getVerifyToken();
|
||||
//encrypted verify token
|
||||
byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1);
|
||||
|
||||
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
|
||||
byte[] serverVerify = session.getVerifyToken();
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
|
||||
if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
|
||||
if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
|
||||
//check if the verify token are equal to the server sent one
|
||||
disconnect(packetEvent, "Invalid token", Level.FINE
|
||||
, "Player {0} ({1}) tried to login with an invalid verify token. "
|
||||
+ "Server: {2} Client: {3}"
|
||||
, session.getUsername(), player.getAddress(), serverVerify, clientVerify);
|
||||
return;
|
||||
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
|
||||
return false;
|
||||
}
|
||||
|
||||
SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
|
||||
return true;
|
||||
}
|
||||
|
||||
//try to get the networkManager from ProtocolLib
|
||||
private Object getNetworkManager(Player player)
|
||||
throws IllegalAccessException, NoSuchFieldException {
|
||||
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
||||
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
|
||||
injectorField.setAccessible(true);
|
||||
|
||||
Object rawInjector = injectorField.get(socketInjector);
|
||||
|
||||
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||
injectorField.setAccessible(true);
|
||||
return injectorField.get(rawInjector);
|
||||
}
|
||||
|
||||
private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent)
|
||||
throws IllegalArgumentException {
|
||||
try {
|
||||
//get the NMS connection handle of this player
|
||||
Object networkManager = getNetworkManager(player);
|
||||
@@ -112,90 +179,50 @@ public class EncryptionPacketListener extends PacketAdapter {
|
||||
encryptConnectionMethod.invoke(networkManager, loginKey);
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
|
||||
//generate the server id based on client and server data
|
||||
String serverId = (new BigInteger(Encryption.getServerIdHash("", plugin.getKeyPair().getPublic(), loginKey)))
|
||||
.toString(16);
|
||||
|
||||
String username = session.getUsername();
|
||||
if (hasJoinedServer(username, serverId)) {
|
||||
session.setVerified(true);
|
||||
|
||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||
|
||||
receiveFakeStartPacket(username, player);
|
||||
} else {
|
||||
//user tried to fake a authentification
|
||||
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||
, session.getUsername(), player.getAddress(), serverId);
|
||||
}
|
||||
|
||||
packetEvent.setCancelled(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
|
||||
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
|
||||
, Object... arguments) {
|
||||
plugin.getLogger().log(logLevel, logMessage, arguments);
|
||||
packetEvent.getPlayer().kickPlayer(kickMessage);
|
||||
kickPlayer(packetEvent.getPlayer(), kickReason);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private Object getNetworkManager(Player player)
|
||||
throws SecurityException, IllegalAccessException, NoSuchFieldException {
|
||||
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
||||
Field injectorField = injector.getClass().getDeclaredField("injector");
|
||||
injectorField.setAccessible(true);
|
||||
private void kickPlayer(Player player, String reason) {
|
||||
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
|
||||
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
|
||||
|
||||
Object rawInjector = injectorField.get(injector);
|
||||
|
||||
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||
injectorField.setAccessible(true);
|
||||
return injectorField.get(rawInjector);
|
||||
}
|
||||
|
||||
private boolean hasJoinedServer(String username, String serverId) {
|
||||
try {
|
||||
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
|
||||
|
||||
HttpURLConnection conn = plugin.getConnection(url);
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (!line.equals("null")) {
|
||||
//validate parsing
|
||||
JSONObject object = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) object.get("id");
|
||||
String name = (String) object.get("name");
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
//catch not only ioexceptions also parse and NPE on unexpected json format
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to verify if session is valid", ex);
|
||||
//send kick packet at login state
|
||||
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
|
||||
protocolManager.sendServerPacket(player, kickPacket);
|
||||
//tell the server that we want to close the connection
|
||||
player.kickPlayer("Disconnect");
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Error sending kickpacket", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//fake a new login packet in order to let the server handle all the other stuff
|
||||
private void receiveFakeStartPacket(String username, Player from) {
|
||||
//fake a new login packet
|
||||
//see StartPacketListener for packet information
|
||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
|
||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
|
||||
|
||||
WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
|
||||
//uuid is ignored by the packet definition
|
||||
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
|
||||
startPacket.getGameProfiles().write(0, fakeProfile);
|
||||
try {
|
||||
//we don't want to handle our own packets so ignore filters
|
||||
protocolManager.recieveClientPacket(from, startPacket, false);
|
||||
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
from.kickPlayer("Error occurred");
|
||||
kickPlayer(from, "Error occured");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerProfile;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerSession;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import protocolsupport.api.events.PlayerLoginStartEvent;
|
||||
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
|
||||
|
||||
public class ProtocolSupportListener implements Listener {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public ProtocolSupportListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onLoginStart(PlayerLoginStartEvent loginStartEvent) {
|
||||
if (loginStartEvent.isLoginDenied()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = loginStartEvent.getName();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(username);
|
||||
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
|
||||
if (playerProfile != null) {
|
||||
if (playerProfile.isPremium()) {
|
||||
if (playerProfile.getUserId() != -1) {
|
||||
startPremiumSession(username, loginStartEvent, true);
|
||||
}
|
||||
} else if (playerProfile.getUserId() == -1) {
|
||||
//user not exists in the db
|
||||
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
try {
|
||||
if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
|
||||
UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username);
|
||||
if (premiumUUID != null) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
startPremiumSession(username, loginStartEvent, false);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPropertiesResolve(PlayerPropertiesResolveEvent propertiesResolveEvent) {
|
||||
//skin was resolved -> premium player
|
||||
if (propertiesResolveEvent.hasProperty("textures")) {
|
||||
InetSocketAddress address = propertiesResolveEvent.getAddress();
|
||||
PlayerSession session = plugin.getSessions().get(address.toString());
|
||||
if (session != null) {
|
||||
session.setVerified(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startPremiumSession(String playerName, PlayerLoginStartEvent loginStartEvent, boolean registered) {
|
||||
loginStartEvent.setOnlineMode(true);
|
||||
InetSocketAddress address = loginStartEvent.getAddress();
|
||||
|
||||
PlayerSession playerSession = new PlayerSession(playerName, null, null);
|
||||
playerSession.setRegistered(registered);
|
||||
plugin.getSessions().put(address.toString(), playerSession);
|
||||
if (plugin.getConfig().getBoolean("premiumUuid")) {
|
||||
loginStartEvent.setUseOnlineModeUUID(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerProfile;
|
||||
import com.github.games647.fastlogin.bukkit.PlayerSession;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Handles incoming start packets from connecting clients. It
|
||||
* checks if we can start checking if the player is premium and
|
||||
* start a request to the client that it should start online mode
|
||||
* login.
|
||||
*
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Login_Start
|
||||
*
|
||||
* String=Username
|
||||
*/
|
||||
public class StartPacketListener extends PacketAdapter {
|
||||
|
||||
private static final int VERIFY_TOKEN_LENGTH = 4;
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
//just create a new once on plugin enable. This used for verify token generation
|
||||
private final Random random = new Random();
|
||||
|
||||
public StartPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
|
||||
//run async in order to not block the server, because we are making api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
final Player player = packetEvent.getPlayer();
|
||||
|
||||
//this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
|
||||
String sessionKey = player.getAddress().toString();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(sessionKey);
|
||||
|
||||
//player.getName() won't work at this state
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
String username = packet.getGameProfiles().read(0).getName();
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
||||
, new Object[]{sessionKey, username});
|
||||
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
|
||||
if (playerProfile != null) {
|
||||
if (playerProfile.isPremium()) {
|
||||
if (playerProfile.getUserId() != -1) {
|
||||
enablePremiumLogin(username, sessionKey, player, packetEvent, true);
|
||||
}
|
||||
} else if (playerProfile.getUserId() == -1) {
|
||||
//user not exists in the db
|
||||
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
try {
|
||||
if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
|
||||
UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username);
|
||||
if (premiumUUID != null) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
enablePremiumLogin(username, sessionKey, player, packetEvent, false);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//minecraft server implementation
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
||||
private void enablePremiumLogin(String username, String sessionKey, Player player, PacketEvent packetEvent
|
||||
, boolean registered) {
|
||||
//randomized server id to make sure the request is for our server
|
||||
//this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
|
||||
String serverId = Long.toString(random.nextLong(), 16);
|
||||
|
||||
//generate a random token which should be the same when we receive it from the client
|
||||
byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
|
||||
random.nextBytes(verifyToken);
|
||||
|
||||
boolean success = sentEncryptionRequest(player, serverId, verifyToken);
|
||||
if (success) {
|
||||
PlayerSession playerSession = new PlayerSession(username, serverId, verifyToken);
|
||||
playerSession.setRegistered(registered);
|
||||
plugin.getSessions().put(sessionKey, playerSession);
|
||||
//cancel only if the player has a paid account otherwise login as normal offline player
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) {
|
||||
try {
|
||||
/**
|
||||
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
|
||||
*
|
||||
* ServerID="" (String)
|
||||
* key=public server key
|
||||
* verifyToken=random 4 byte array
|
||||
*/
|
||||
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN);
|
||||
|
||||
newPacket.getStrings().write(0, serverId);
|
||||
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic());
|
||||
|
||||
newPacket.getByteArrays().write(0, verifyToken);
|
||||
|
||||
//serverId is a empty string
|
||||
protocolManager.sendServerPacket(player, newPacket);
|
||||
return true;
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
73
bukkit/src/main/resources/config.yml
Normal file
73
bukkit/src/main/resources/config.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
# FastLogin config
|
||||
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
|
||||
# Source code: https://github.com/games647/FastLogin
|
||||
#
|
||||
# You can access the newest config here:
|
||||
# https://github.com/games647/FastLogin/blob/master/bukkit/src/main/resources/config.yml
|
||||
|
||||
# Request a premium login without forcing the player to type a command
|
||||
#
|
||||
# If you activate autoRegister, this plugin will check/do these points on login:
|
||||
# 1. An existing cracked account shouldn't exist
|
||||
# -> paid accounts cannot steal the existing account of cracked players
|
||||
# - (Already registered players could still use the /premium command to activate premium checks)
|
||||
# 2. Automatically registers an account with a strong random generated password
|
||||
# -> cracked player cannot register an account for the premium player and so cannot the steal the account
|
||||
#
|
||||
# Furthermore the premium player check have to be made based on the player name
|
||||
# This means if a cracked player connects to the server and we request a paid account login from this player
|
||||
# the player just disconnect and sees the message: 'bad login' or 'invalid session'
|
||||
# There is no way to change this message
|
||||
# For more information: https://github.com/games647/FastLogin#why-do-players-have-to-invoke-a-command
|
||||
autoRegister: false
|
||||
|
||||
# If this plugin detected that a player has a premium, it can also set the associated
|
||||
# uuid from that account. So if the players changes their usernames, they will still have
|
||||
# the same playerdata (inventory, permissions, ...)
|
||||
#
|
||||
# Warning: This also means that the UUID will be different if the player is connecting
|
||||
# through a offline mode connection. This **could** cause plugin compatibility issues.
|
||||
#
|
||||
# This is a example and doesn't apply for every plugin.
|
||||
# Example: If you want to ban players who aren't online at the moment, the ban plugin will look
|
||||
# after a offline uuid associated to the player, because the server is in offline mode. Then the premium
|
||||
# players could still join the server, because they have different UUID.
|
||||
#
|
||||
# Moreover you may want to convert the offline UUID to a premium UUID. This will ensure that the player
|
||||
# will have the same inventory, permissions, ... if they switched to premium authentification from offline/cracked
|
||||
# authentification.
|
||||
#
|
||||
# This feature requires Cauldron, Spigot or a fork of Spigot (PaperSpigot, TacoSpigot)
|
||||
premiumUuid: false
|
||||
|
||||
# If your players have a premium account and a skin associated to their account, this plugin
|
||||
# can download the data and set it to the online player.
|
||||
#
|
||||
# Keep in mind that this will only works if the player:
|
||||
# * is the owner of the premium account
|
||||
# * the serverconnection is established through a premium connection (paid account authentification)
|
||||
# * has a skin
|
||||
#
|
||||
# This means this plugin doesn't need to create a new connection to the Mojang servers, because
|
||||
# the skin data is included in the Auth-Verification-Response sent by Mojang. If you want to use for other
|
||||
# players like cracked player, you have to use other plugins.
|
||||
#
|
||||
# If you want to use skins for your cracked player, you need an additional plugin like
|
||||
# ChangeSkin, SkinRestoer, ...
|
||||
forwardSkin: true
|
||||
|
||||
# Database configuration
|
||||
# Recommened is the use of MariaDB (a better version of MySQL)
|
||||
|
||||
# Single file SQLite database
|
||||
driver: org.sqlite.JDBC
|
||||
# File location
|
||||
database: '{pluginDir}/FastLogin.db'
|
||||
|
||||
# MySQL and SQLite
|
||||
#driver: com.mysql.jdbc.Driver
|
||||
#host: localhost
|
||||
#port: 3306
|
||||
#database: fastlogin
|
||||
#username: myUser
|
||||
#password: myPassword
|
||||
57
bukkit/src/main/resources/plugin.yml
Normal file
57
bukkit/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# project informations for Bukkit in order to register our plugin with all it components
|
||||
# ${-} are variables from Maven (pom.xml) which will be replaced after the build
|
||||
name: ${project.parent.name}
|
||||
version: ${project.version}
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
# meta informations for plugin managers
|
||||
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
|
||||
description: |
|
||||
${project.description}
|
||||
website: ${project.url}
|
||||
dev-url: ${project.url}
|
||||
|
||||
# Without Protocollib the plugin does not work at all
|
||||
depend: [ProtocolLib]
|
||||
|
||||
softdepend:
|
||||
- ProtocolSupport
|
||||
# Auth plugins
|
||||
- xAuth
|
||||
- AuthMe
|
||||
- CrazyLogin
|
||||
- LoginSecurity
|
||||
- RoyalAuth
|
||||
- UltraAuth
|
||||
|
||||
commands:
|
||||
${project.parent.name}:
|
||||
description: 'Label the invoker as premium'
|
||||
aliases: [prem, premium, loginfast]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.premium
|
||||
|
||||
unpremium:
|
||||
description: 'Label the invoker or the player specified as cracked if he was marked premium before'
|
||||
aliases: [cracked]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.unpremium
|
||||
|
||||
permissions:
|
||||
${project.artifactId}.command.premium:
|
||||
description: 'Label themselves as premium'
|
||||
default: true
|
||||
|
||||
${project.artifactId}.command.premium.other:
|
||||
description: 'Label others as premium'
|
||||
children:
|
||||
${project.artifactId}.command.premium: true
|
||||
|
||||
${project.artifactId}.command.unpremium:
|
||||
description: 'Label themselves as cracked'
|
||||
default: true
|
||||
|
||||
${project.artifactId}.command..unpremium.other:
|
||||
description: 'Label others as cracked'
|
||||
children:
|
||||
${project.artifactId}.command.unpremium: true
|
||||
60
bungee/pom.xml
Normal file
60
bungee/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.0</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin.bungee</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!--Represents the main plugin-->
|
||||
<name>FastLoginBungee</name>
|
||||
|
||||
<repositories>
|
||||
<!--Waterfall-->
|
||||
<!-- <repository>
|
||||
<id>ellune-releases</id>
|
||||
<url>https://repo.ellune.net/content/repositories/snapshots/</url>
|
||||
</repository>-->
|
||||
|
||||
<!--BungeeCord with also the part outside the API-->
|
||||
<repository>
|
||||
<id>RYRED-REPO</id>
|
||||
<url>http://mvn.ryred.co/repository/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--Github automatic maven builds-->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-proxy</artifactId>
|
||||
<version>1.9-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>io.github.waterfallmc</groupId>
|
||||
<artifactId>waterfall-api</artifactId>
|
||||
<version>1.9-SNAPSHOT</version>
|
||||
<type>jar</type>
|
||||
<scope>provided</scope>
|
||||
</dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.MatteCarra</groupId>
|
||||
<artifactId>BungeeAuth</artifactId>
|
||||
<version>-1.2.1-gc367d92-8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthHook;
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
/**
|
||||
* BungeeCord version of FastLogin. This plugin keeps track on online mode connections.
|
||||
*/
|
||||
public class FastLoginBungee extends Plugin {
|
||||
|
||||
public static UUID parseId(String withoutDashes) {
|
||||
return UUID.fromString(withoutDashes.substring(0, 8)
|
||||
+ "-" + withoutDashes.substring(8, 12)
|
||||
+ "-" + withoutDashes.substring(12, 16)
|
||||
+ "-" + withoutDashes.substring(16, 20)
|
||||
+ "-" + withoutDashes.substring(20, 32));
|
||||
}
|
||||
|
||||
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
.toCharArray();
|
||||
|
||||
private BungeeAuthPlugin bungeeAuthPlugin;
|
||||
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
|
||||
private Storage storage;
|
||||
private Configuration configuration;
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
private final ConcurrentMap<PendingConnection, Object> pendingAutoRegister = CacheBuilder
|
||||
.newBuilder()
|
||||
.expireAfterWrite(1, TimeUnit.MINUTES)
|
||||
.<PendingConnection, Object>build().asMap();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (!getDataFolder().exists()) {
|
||||
getDataFolder().mkdir();
|
||||
}
|
||||
|
||||
File configFile = new File(getDataFolder(), "config.yml");
|
||||
if (!configFile.exists()) {
|
||||
try (InputStream in = getResourceAsStream("config.yml")) {
|
||||
Files.copy(in, configFile.toPath());
|
||||
} catch (IOException ioExc) {
|
||||
getLogger().log(Level.SEVERE, "Error saving default config", ioExc);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
|
||||
String driver = configuration.getString("driver");
|
||||
String host = configuration.getString("host", "");
|
||||
int port = configuration.getInt("port", 3306);
|
||||
String database = configuration.getString("database");
|
||||
|
||||
String username = configuration.getString("username", "");
|
||||
String password = configuration.getString("password", "");
|
||||
storage = new Storage(this, driver, host, port, database, username, password);
|
||||
try {
|
||||
storage.createTables();
|
||||
} catch (Exception ex) {
|
||||
getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (IOException ioExc) {
|
||||
getLogger().log(Level.SEVERE, "Error loading config. Disabling plugin...", ioExc);
|
||||
return;
|
||||
}
|
||||
|
||||
//events
|
||||
getProxy().getPluginManager().registerListener(this, new PlayerConnectionListener(this));
|
||||
|
||||
//this is required to listen to messages from the server
|
||||
getProxy().registerChannel(getDescription().getName());
|
||||
|
||||
registerHook();
|
||||
}
|
||||
|
||||
public String generateStringPassword() {
|
||||
StringBuilder generatedPassword = new StringBuilder(8);
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
generatedPassword.append(CHARACTERS[random.nextInt(CHARACTERS.length - 1)]);
|
||||
}
|
||||
|
||||
return generatedPassword.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (storage != null) {
|
||||
storage.close();
|
||||
}
|
||||
}
|
||||
|
||||
public Configuration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public MojangApiConnector getMojangApiConnector() {
|
||||
return mojangApiConnector;
|
||||
}
|
||||
|
||||
public ConcurrentMap<PendingConnection, Object> getPendingAutoRegister() {
|
||||
return pendingAutoRegister;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auth plugin hook for BungeeCord
|
||||
*
|
||||
* @return the auth hook for BungeeCord. null if none found
|
||||
*/
|
||||
public BungeeAuthPlugin getBungeeAuthPlugin() {
|
||||
return bungeeAuthPlugin;
|
||||
}
|
||||
|
||||
private void registerHook() {
|
||||
Plugin plugin = getProxy().getPluginManager().getPlugin("BungeeAuth");
|
||||
if (plugin != null) {
|
||||
bungeeAuthPlugin = new BungeeAuthHook();
|
||||
getLogger().info("Hooked into BungeeAuth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
|
||||
public class ForceLoginTask implements Runnable {
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
private final ProxiedPlayer player;
|
||||
|
||||
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
|
||||
|
||||
if (playerProfile.getUserId() == -1) {
|
||||
playerProfile.setPremium(player.getPendingConnection().isOnlineMode());
|
||||
if (player.getPendingConnection().isOnlineMode()) {
|
||||
playerProfile.setUuid(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
//force login only on success
|
||||
if (player.getPendingConnection().isOnlineMode()) {
|
||||
Server server = player.getServer();
|
||||
|
||||
boolean autoRegister = plugin.getPendingAutoRegister().remove(player.getPendingConnection()) != null;
|
||||
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
//subchannel name
|
||||
if (autoRegister) {
|
||||
dataOutput.writeUTF("AUTO_REGISTER");
|
||||
} else {
|
||||
dataOutput.writeUTF("AUTO_LOGIN");
|
||||
}
|
||||
|
||||
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
|
||||
dataOutput.writeUTF(player.getName());
|
||||
|
||||
//proxy identifier to check if it's a acceptable proxy
|
||||
UUID proxyId = UUID.fromString(plugin.getProxy().getConfig().getUuid());
|
||||
dataOutput.writeLong(proxyId.getMostSignificantBits());
|
||||
dataOutput.writeLong(proxyId.getLeastSignificantBits());
|
||||
|
||||
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
|
||||
|
||||
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
|
||||
if (authPlugin != null) {
|
||||
if (autoRegister) {
|
||||
String password = plugin.generateStringPassword();
|
||||
if (authPlugin.forceRegister(player, password)) {
|
||||
plugin.getStorage().save(playerProfile);
|
||||
}
|
||||
} else if (authPlugin.forceLogin(player)) {
|
||||
plugin.getStorage().save(playerProfile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plugin.getStorage().save(playerProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
|
||||
public class MojangApiConnector {
|
||||
|
||||
//http connection, read timeout and user agent for a connection to mojang api servers
|
||||
private static final int TIMEOUT = 1 * 1_000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
//mojang api check to prove a player is logged in minecraft and made a join server request
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
//only premium (paid account) users have a uuid from here
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
//this includes a-zA-Z1-9_
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
|
||||
//compile the pattern only on plugin enable -> and this have to be threadsafe
|
||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public MojangApiConnector(FastLoginBungee plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param playerName
|
||||
* @return null on non-premium
|
||||
*/
|
||||
public UUID getPremiumUUID(String playerName) {
|
||||
//check if it's a valid playername
|
||||
if (playernameMatcher.matcher(playerName).matches()) {
|
||||
//only make a API call if the name is valid existing mojang account
|
||||
try {
|
||||
HttpURLConnection connection = getConnection(UUID_LINK + playerName);
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != null && !line.equals("null")) {
|
||||
MojangPlayer mojangPlayer = BungeeCord.getInstance().gson.fromJson(line, MojangPlayer.class);
|
||||
return FastLoginBungee.parseId(mojangPlayer.getId());
|
||||
}
|
||||
}
|
||||
//204 - no content for not found
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpURLConnection getConnection(String url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
connection.setReadTimeout(TIMEOUT);
|
||||
//the new Mojang API just uses json as response
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
public class MojangPlayer {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
import net.md_5.bungee.api.event.PluginMessageEvent;
|
||||
import net.md_5.bungee.api.event.PreLoginEvent;
|
||||
import net.md_5.bungee.api.event.ServerConnectedEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
|
||||
/**
|
||||
* Enables online mode logins for specified users and sends
|
||||
* plugin message to the Bukkit version of this plugin in
|
||||
* order to clear that the connection is online mode.
|
||||
*/
|
||||
public class PlayerConnectionListener implements Listener {
|
||||
|
||||
protected final FastLoginBungee plugin;
|
||||
|
||||
public PlayerConnectionListener(FastLoginBungee plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPreLogin(final PreLoginEvent preLoginEvent) {
|
||||
if (preLoginEvent.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
preLoginEvent.registerIntent(plugin);
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
PendingConnection connection = preLoginEvent.getConnection();
|
||||
String username = connection.getName();
|
||||
try {
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
|
||||
if (playerProfile != null) {
|
||||
if (playerProfile.isPremium()) {
|
||||
if (playerProfile.getUserId() != -1) {
|
||||
connection.setOnlineMode(true);
|
||||
}
|
||||
} else if (playerProfile.getUserId() == -1) {
|
||||
//user not exists in the db
|
||||
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
|
||||
if (plugin.getConfiguration().getBoolean("autoRegister")
|
||||
&& (authPlugin == null || !authPlugin.isRegistered(username))) {
|
||||
UUID premiumUUID = plugin.getMojangApiConnector().getPremiumUUID(username);
|
||||
if (premiumUUID != null) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
connection.setOnlineMode(true);
|
||||
plugin.getPendingAutoRegister().put(connection, new Object());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check premium state", ex);
|
||||
} finally {
|
||||
preLoginEvent.completeIntent(plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onServerConnected(ServerConnectedEvent serverConnectedEvent) {
|
||||
ProxiedPlayer player = serverConnectedEvent.getPlayer();
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, new ForceLoginTask(plugin, player));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPluginMessage(PluginMessageEvent pluginMessageEvent) {
|
||||
String channel = pluginMessageEvent.getTag();
|
||||
if (pluginMessageEvent.isCancelled() || !plugin.getDescription().getName().equals(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//the client shouldn't be able to read the messages in order to know something about server internal states
|
||||
//moreover the client shouldn't be able fake a running premium check by sending the result message
|
||||
pluginMessageEvent.setCancelled(true);
|
||||
|
||||
//check if the message is sent from the server
|
||||
if (Server.class.isAssignableFrom(pluginMessageEvent.getSender().getClass())) {
|
||||
byte[] data = pluginMessageEvent.getData();
|
||||
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
|
||||
String subchannel = dataInput.readUTF();
|
||||
|
||||
final ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
|
||||
if ("ON".equals(subchannel)) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), true);
|
||||
if (playerProfile.isPremium()) {
|
||||
if (forPlayer.isConnected()) {
|
||||
TextComponent textComponent = new TextComponent("You are already on the premium list");
|
||||
textComponent.setColor(ChatColor.DARK_RED);
|
||||
forPlayer.sendMessage(textComponent);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
playerProfile.setPremium(true);
|
||||
//todo: set uuid
|
||||
plugin.getStorage().save(playerProfile);
|
||||
}
|
||||
});
|
||||
} else if ("OFF".equals(subchannel)) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), true);
|
||||
if (!playerProfile.isPremium()) {
|
||||
if (forPlayer.isConnected()) {
|
||||
TextComponent textComponent = new TextComponent("You are not in the premium list");
|
||||
textComponent.setColor(ChatColor.DARK_RED);
|
||||
forPlayer.sendMessage(textComponent);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
playerProfile.setPremium(false);
|
||||
playerProfile.setUuid(null);
|
||||
//todo: set uuid
|
||||
plugin.getStorage().save(playerProfile);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PlayerProfile {
|
||||
|
||||
private final String playerName;
|
||||
|
||||
private long userId;
|
||||
|
||||
private UUID uuid;
|
||||
private boolean premium;
|
||||
private String lastIp;
|
||||
private long lastLogin;
|
||||
|
||||
public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium
|
||||
, String lastIp, long lastLogin) {
|
||||
this.userId = userId;
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
|
||||
public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
|
||||
this.userId = -1;
|
||||
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public synchronized long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
protected synchronized void setUserId(long generatedId) {
|
||||
this.userId = generatedId;
|
||||
}
|
||||
|
||||
public synchronized UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public synchronized void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public synchronized boolean isPremium() {
|
||||
return premium;
|
||||
}
|
||||
|
||||
public synchronized void setPremium(boolean premium) {
|
||||
this.premium = premium;
|
||||
}
|
||||
|
||||
public synchronized String getLastIp() {
|
||||
return lastIp;
|
||||
}
|
||||
|
||||
public synchronized void setLastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public long getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public synchronized void setLastLogin(long lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Storage {
|
||||
|
||||
private static final String PREMIUM_TABLE = "premium";
|
||||
|
||||
private final ConcurrentMap<String, PlayerProfile> profileCache = CacheBuilder
|
||||
.<String, PlayerProfile>newBuilder()
|
||||
.concurrencyLevel(20)
|
||||
.expireAfterAccess(30, TimeUnit.MINUTES)
|
||||
.build(new CacheLoader<String, PlayerProfile>() {
|
||||
@Override
|
||||
public PlayerProfile load(String key) throws Exception {
|
||||
//should be fetched manually
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
}).asMap();
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
private final FastLoginBungee plugin;
|
||||
|
||||
public Storage(FastLoginBungee plugin, String driver, String host, int port, String databasePath
|
||||
, String user, String pass) {
|
||||
this.plugin = plugin;
|
||||
|
||||
HikariConfig databaseConfig = new HikariConfig();
|
||||
databaseConfig.setUsername(user);
|
||||
databaseConfig.setPassword(pass);
|
||||
databaseConfig.setDriverClassName(driver);
|
||||
|
||||
databasePath = databasePath.replace("{pluginDir}", plugin.getDataFolder().getAbsolutePath());
|
||||
|
||||
String jdbcUrl = "jdbc:";
|
||||
if (driver.contains("sqlite")) {
|
||||
jdbcUrl += "sqlite" + "://" + databasePath;
|
||||
databaseConfig.setConnectionTestQuery("SELECT 1");
|
||||
} else {
|
||||
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
|
||||
}
|
||||
|
||||
databaseConfig.setJdbcUrl(jdbcUrl);
|
||||
this.dataSource = new HikariDataSource(databaseConfig);
|
||||
}
|
||||
|
||||
public void createTables() throws SQLException {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
Statement statement = con.createStatement();
|
||||
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
|
||||
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
|
||||
+ "`UUID` CHAR(36), "
|
||||
+ "`Name` VARCHAR(16) NOT NULL, "
|
||||
+ "`Premium` BOOLEAN NOT NULL, "
|
||||
+ "`LastIp` VARCHAR(255) NOT NULL, "
|
||||
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
|
||||
+ "UNIQUE (`UUID`), "
|
||||
//the premium shouldn't steal the cracked account by changing the name
|
||||
+ "UNIQUE (`Name`) "
|
||||
+ ")";
|
||||
|
||||
if (dataSource.getJdbcUrl().contains("sqlite")) {
|
||||
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
|
||||
}
|
||||
|
||||
statement.executeUpdate(createDataStmt);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerProfile getProfile(String name, boolean fetch) {
|
||||
if (profileCache.containsKey(name)) {
|
||||
return profileCache.get(name);
|
||||
} else if (fetch) {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE
|
||||
+ " WHERE `Name`=? LIMIT 1");
|
||||
loadStatement.setString(1, name);
|
||||
|
||||
ResultSet resultSet = loadStatement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
long userId = resultSet.getInt(1);
|
||||
|
||||
String unparsedUUID = resultSet.getString(2);
|
||||
UUID uuid;
|
||||
if (unparsedUUID == null) {
|
||||
uuid = null;
|
||||
} else {
|
||||
uuid = FastLoginBungee.parseId(unparsedUUID);
|
||||
}
|
||||
|
||||
// String name = resultSet.getString(3);
|
||||
boolean premium = resultSet.getBoolean(4);
|
||||
String lastIp = resultSet.getString(5);
|
||||
long lastLogin = resultSet.getTimestamp(6).getTime();
|
||||
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
|
||||
profileCache.put(name, playerProfile);
|
||||
return playerProfile;
|
||||
} else {
|
||||
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
|
||||
profileCache.put(name, crackedProfile);
|
||||
return crackedProfile;
|
||||
}
|
||||
} catch (SQLException sqlEx) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean save(PlayerProfile playerProfile) {
|
||||
Connection con = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
|
||||
UUID uuid = playerProfile.getUuid();
|
||||
|
||||
if (playerProfile.getUserId() == -1) {
|
||||
PreparedStatement saveStatement = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
|
||||
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
|
||||
|
||||
if (uuid == null) {
|
||||
saveStatement.setString(1, null);
|
||||
} else {
|
||||
saveStatement.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStatement.setString(2, playerProfile.getPlayerName());
|
||||
saveStatement.setBoolean(3, playerProfile.isPremium());
|
||||
saveStatement.setString(4, playerProfile.getLastIp());
|
||||
saveStatement.execute();
|
||||
|
||||
ResultSet generatedKeys = saveStatement.getGeneratedKeys();
|
||||
if (generatedKeys != null && generatedKeys.next()) {
|
||||
playerProfile.setUserId(generatedKeys.getInt(1));
|
||||
}
|
||||
} else {
|
||||
PreparedStatement saveStatement = con.prepareStatement("UPDATE " + PREMIUM_TABLE
|
||||
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
|
||||
|
||||
if (uuid == null) {
|
||||
saveStatement.setString(1, null);
|
||||
} else {
|
||||
saveStatement.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStatement.setString(2, playerProfile.getPlayerName());
|
||||
saveStatement.setBoolean(3, playerProfile.isPremium());
|
||||
saveStatement.setString(4, playerProfile.getLastIp());
|
||||
// saveStatement.setTimestamp(5, new Timestamp(playerProfile.getLastLogin()));
|
||||
|
||||
saveStatement.setLong(5, playerProfile.getUserId());
|
||||
saveStatement.execute();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
dataSource.close();
|
||||
profileCache.clear();
|
||||
}
|
||||
|
||||
private void closeQuietly(Connection con) {
|
||||
if (con != null) {
|
||||
try {
|
||||
con.close();
|
||||
} catch (SQLException sqlEx) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to close connection", sqlEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.github.games647.fastlogin.bungee.hooks;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Random;
|
||||
|
||||
import me.vik1395.BungeeAuth.ListenerClass;
|
||||
import me.vik1395.BungeeAuth.Main;
|
||||
import me.vik1395.BungeeAuth.Password.PasswordHandler;
|
||||
import me.vik1395.BungeeAuth.Tables;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/MatteCarra/BungeeAuth
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Spigot: https://www.spigotmc.org/resources/bungeeauth.493/
|
||||
*/
|
||||
public class BungeeAuthHook implements BungeeAuthPlugin {
|
||||
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L32
|
||||
private final Tables databaseConnection = new Tables();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final ProxiedPlayer player) {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L92-95
|
||||
Main.plonline.add(player.getName());
|
||||
|
||||
//renamed from ct to databaseConnection
|
||||
// databaseConnection.setStatus(player.getName(), "online");
|
||||
final Class<?>[] parameterTypes = new Class<?>[]{String.class, String.class};
|
||||
final Object[] arguments = new Object[]{player.getName(), "online"};
|
||||
|
||||
try {
|
||||
callProtected("setStatus", parameterTypes, arguments);
|
||||
ListenerClass.movePlayer(player, false);
|
||||
|
||||
//proparly not thread-safe
|
||||
ListenerClass.prelogin.get(player.getName()).cancel();
|
||||
} catch (Exception ex) {
|
||||
Main.plugin.getLogger().severe("[BungeeAuth] Error force loging in player");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L46
|
||||
//renamed t to databaseConnection
|
||||
return databaseConnection.checkPlayerEntry(playerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final ProxiedPlayer player, String password) {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L102
|
||||
PasswordHandler ph = new PasswordHandler();
|
||||
Random rand = new Random();
|
||||
int maxp = 7; //Total Password Hashing methods.
|
||||
Date dNow = new Date();
|
||||
SimpleDateFormat ft = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
|
||||
|
||||
String Pw = password;
|
||||
String pType = "" + rand.nextInt(maxp + 1);
|
||||
String regdate = ft.format(dNow);
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L60
|
||||
String lastip = player.getAddress().getAddress().getHostAddress();
|
||||
String lastseen = regdate;
|
||||
String hash = ph.newHash(Pw, pType);
|
||||
|
||||
//creates a new SQL entry with the player's details.
|
||||
|
||||
//renamed t to databaseConnection
|
||||
// databaseConnection.newPlayerEntry(player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen);
|
||||
|
||||
final Class<?>[] parameterTypes = new Class<?>[] {String.class, String.class, String.class, String.class
|
||||
, String.class, String.class, String.class, String.class};
|
||||
final Object[] arguments = new Object[] {player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen};
|
||||
|
||||
try {
|
||||
callProtected("newPlayerEntry", parameterTypes, arguments);
|
||||
//proparly not thread-safe
|
||||
forceLogin(player);
|
||||
} catch (Exception ex) {
|
||||
Main.plugin.getLogger().severe("[BungeeAuth] Error when creating a new player in the Database");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//pail ;(
|
||||
private void callProtected(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
|
||||
Class<? extends Tables> tableClass = databaseConnection.getClass();
|
||||
|
||||
Method method = tableClass.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
method.invoke(databaseConnection, arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.games647.fastlogin.bungee.hooks;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin in BungeeCord/Waterfall/... servers
|
||||
*/
|
||||
public interface BungeeAuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player after
|
||||
* the player joined successfully a server.
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceLogin(ProxiedPlayer player);
|
||||
|
||||
/**
|
||||
* Checks whether an account exists for this player name.
|
||||
*
|
||||
* This check should check if a cracked player account exists
|
||||
* so we can be sure the premium player doesn't steal the account
|
||||
* of that player.
|
||||
*
|
||||
* This operation will be performed async while the player is
|
||||
* connecting
|
||||
*
|
||||
* @param playerName player name
|
||||
* @return if the player has an account
|
||||
* @throws Exception if an error occurred
|
||||
*/
|
||||
boolean isRegistered(String playerName) throws Exception;
|
||||
|
||||
/**
|
||||
* Forces a register in order to protect the paid account.
|
||||
* The method will be invoked after the player joined a server.
|
||||
*
|
||||
* After a successful registration the player should be logged
|
||||
* in too.
|
||||
*
|
||||
* The method will be called only for premium accounts.
|
||||
* So it's recommended to set additionally premium property
|
||||
* if possible.
|
||||
*
|
||||
* If we don't register an account, cracked players
|
||||
* could steal the unregistered account from the paid
|
||||
* player account
|
||||
*
|
||||
* @param player the premium account
|
||||
* @param password a strong random generated password
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceRegister(ProxiedPlayer player, String password);
|
||||
}
|
||||
16
bungee/src/main/resources/bungee.yml
Normal file
16
bungee/src/main/resources/bungee.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# project informations for BungeeCord
|
||||
# This file will be prioritised over plugin.yml which can be also used for Bungee
|
||||
# This make it easy to combine BungeeCord and Bukkit support in one plugin
|
||||
name: ${project.parent.name}
|
||||
# ${-} will be automatically replaced by Maven
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
version: ${project.version}
|
||||
author: games647, http://github.com/games647/FastLogin/graphs/contributors
|
||||
|
||||
softdepends:
|
||||
# BungeeCord auth plugins
|
||||
- BungeeAuth
|
||||
|
||||
description: |
|
||||
${project.description}
|
||||
Binary file not shown.
115
pom.xml
115
pom.xml
@@ -5,12 +5,12 @@
|
||||
<groupId>com.github.games647</groupId>
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>FastLogin</name>
|
||||
<version>0.2</version>
|
||||
<version>1.0</version>
|
||||
<inceptionYear>2015</inceptionYear>
|
||||
<url>http://dev.bukkit.org/bukkit-plugins/fastlogin</url>
|
||||
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
|
||||
<description>
|
||||
Automatically logins premium (paid accounts) player on a offline mode server
|
||||
</description>
|
||||
@@ -21,6 +21,12 @@
|
||||
<outputDir>${basedir}/target</outputDir>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>bukkit</module>
|
||||
<module>bungee</module>
|
||||
<module>universal</module>
|
||||
</modules>
|
||||
|
||||
<issueManagement>
|
||||
<system>GitHub</system>
|
||||
<url>https://github.com/games647/FastLogin/issues</url>
|
||||
@@ -41,14 +47,12 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.2</version>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
<showWarnings>true</showWarnings>
|
||||
<showDeprecation>true</showDeprecation>
|
||||
<!--false means actual true http://jira.codehaus.org/browse/MCOMPILER-209-->
|
||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -71,7 +75,8 @@
|
||||
|
||||
<!--Add the license to jar in order to see it in the final jar-->
|
||||
<resource>
|
||||
<directory>${basedir}</directory>
|
||||
<!--Parent folder-->
|
||||
<directory>${basedir}/..</directory>
|
||||
<includes>
|
||||
<include>LICENSE</include>
|
||||
</includes>
|
||||
@@ -79,95 +84,19 @@
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<!--Bukkit-Server-API -->
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--ProtocolLib-->
|
||||
<repository>
|
||||
<id>dmulloy2-repo</id>
|
||||
<url>http://repo.dmulloy2.net/content/groups/public/</url>
|
||||
</repository>
|
||||
|
||||
<!--Authme Reloaded-->
|
||||
<repository>
|
||||
<id>xephi-repo</id>
|
||||
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--xAuth-->
|
||||
<repository>
|
||||
<id>luricos.de-repo</id>
|
||||
<url>http://repo.luricos.de/bukkit-plugins/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!--Server API-->
|
||||
<!--Database pooling-->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.8.8-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>2.4.6</version>
|
||||
</dependency>
|
||||
|
||||
<!--Library for listening and sending Minecraft packets-->
|
||||
<!--Logging framework implements slf4j which is required by hikari-->
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>3.6.5-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!--Login Plugins-->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.luricos.bukkit</groupId>
|
||||
<artifactId>xAuth</artifactId>
|
||||
<version>2.6</version>
|
||||
<!--These artifacts produce conflicts on downloading-->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>net.gravitydevelopment.updater</groupId>
|
||||
<artifactId>updater</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>net.ess3</groupId>
|
||||
<artifactId>EssentialsGroupManager</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyCore</artifactId>
|
||||
<version>10.7.7</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyCore v10.7.7.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyLogin</artifactId>
|
||||
<version>7.23</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.lenis0012.ls</groupId>
|
||||
<artifactId>LoginSecurity</artifactId>
|
||||
<version>2.0.10</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/LoginSecurity v2.0.10.jar</systemPath>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-jdk14</artifactId>
|
||||
<version>1.7.20</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import com.github.games647.fastlogin.listener.PlayerListener;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.utility.SafeCacheBuilder;
|
||||
import com.github.games647.fastlogin.hooks.AuthPlugin;
|
||||
import com.github.games647.fastlogin.listener.EncryptionPacketListener;
|
||||
import com.github.games647.fastlogin.listener.StartPacketListener;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class FastLogin extends JavaPlugin {
|
||||
|
||||
private static final int TIMEOUT = 15000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
//provide a immutable key pair to be thread safe
|
||||
private final KeyPair keyPair = Encryption.generateKeyPair();
|
||||
|
||||
//we need a thread-safe set because we access it async in the packet listener
|
||||
private final Set<String> enabledPremium = Sets.newConcurrentHashSet();
|
||||
|
||||
//this map is thread-safe for async access (Packet Listener)
|
||||
//SafeCacheBuilder is used in order to be version independent
|
||||
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
|
||||
//mapped by ip:port
|
||||
.expireAfterWrite(2, TimeUnit.MINUTES)
|
||||
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
|
||||
.build(new CacheLoader<String, PlayerSession>() {
|
||||
|
||||
@Override
|
||||
public PlayerSession load(String key) throws Exception {
|
||||
//A key should be inserted manually on start packet
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
//online mode is only changeable after a restart so check it here
|
||||
if (getServer().getOnlineMode()) {
|
||||
getLogger().severe("Server have to be in offline mode");
|
||||
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (!isEnabled() || !registerHooks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//register packet listeners on success
|
||||
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
|
||||
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
|
||||
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
|
||||
|
||||
//register commands
|
||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
//clean up
|
||||
session.clear();
|
||||
enabledPremium.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
|
||||
* account)
|
||||
*
|
||||
* @return a thread-safe session map
|
||||
*/
|
||||
public ConcurrentMap<String, PlayerSession> getSessions() {
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server KeyPair
|
||||
*
|
||||
* @return the server KeyPair
|
||||
*/
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of user who activated premium logins
|
||||
*
|
||||
* @return user who activated premium logins
|
||||
*/
|
||||
public Set<String> getEnabledPremium() {
|
||||
return enabledPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a Mojang API connection. The connection is not started in this method
|
||||
*
|
||||
* @param url the url connecting to
|
||||
* @return the prepared connection
|
||||
*
|
||||
* @throws IOException on invalid url format or on {@link java.net.URL#openConnection() }
|
||||
*/
|
||||
public HttpURLConnection getConnection(String url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
connection.setReadTimeout(TIMEOUT);
|
||||
//the new Mojang API just uses json as response
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private boolean registerHooks() {
|
||||
AuthPlugin authPluginHook = null;
|
||||
try {
|
||||
String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
|
||||
//Look through all classes in the hooks package and look for supporting plugins on the server
|
||||
for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
|
||||
//remove the hook suffix
|
||||
String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
|
||||
Class<?> clazz = clazzInfo.load();
|
||||
//uses only member classes which uses AuthPlugin interface (skip interfaces)
|
||||
if (AuthPlugin.class.isAssignableFrom(clazz)
|
||||
&& getServer().getPluginManager().isPluginEnabled(pluginName)) {
|
||||
authPluginHook = (AuthPlugin) clazz.newInstance();
|
||||
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException | IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
|
||||
}
|
||||
|
||||
if (authPluginHook == null) {
|
||||
//run this check for exceptions and not found plugins
|
||||
getLogger().warning("No support offline Auth plugin found. ");
|
||||
getLogger().warning("Disabling this plugin...");
|
||||
|
||||
setEnabled(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
//We found a supporting plugin - we can now register a forwarding listener
|
||||
getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
/**
|
||||
* Represents a client connecting to the server.
|
||||
*
|
||||
* This session is invalid if the player disconnects or the login was successful
|
||||
*/
|
||||
public class PlayerSession {
|
||||
|
||||
private final byte[] verifyToken;
|
||||
private final String username;
|
||||
private boolean verified;
|
||||
|
||||
public PlayerSession(byte[] verifyToken, String username) {
|
||||
this.username = username;
|
||||
this.verifyToken = verifyToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the verify token the server sent to the client.
|
||||
*
|
||||
* @return the verify token from the server
|
||||
*/
|
||||
public byte[] getVerifyToken() {
|
||||
return verifyToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the username the player sent to the server
|
||||
*
|
||||
* @return the client sent username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @param verified whether the player has valid session
|
||||
*/
|
||||
public synchronized void setVerified(boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @return whether the player has a valid session
|
||||
*/
|
||||
public synchronized boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PremiumCommand implements CommandExecutor {
|
||||
|
||||
private final FastLogin plugin;
|
||||
|
||||
public PremiumCommand(FastLogin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Only players can add themselves as premium");
|
||||
return true;
|
||||
}
|
||||
|
||||
String playerName = sender.getName();
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sender.hasPermission(plugin.getName() + ".command." + command.getName() + ".others")) {
|
||||
String playerName = args[0];
|
||||
//todo check if valid username
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added "
|
||||
+ ChatColor.DARK_BLUE + ChatColor.BOLD + playerName
|
||||
+ ChatColor.RESET + ChatColor.DARK_GREEN + " to the list of premium players");
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Not enough permissions");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
import fr.xephi.authme.cache.limbo.LimboCache;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/Xephi/AuthMeReloaded/
|
||||
* Project page: dev.bukkit.org/bukkit-plugins/authme-reloaded/
|
||||
*/
|
||||
public class AuthMeHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
//here is the gamemode, inventory ... saved
|
||||
if (!LimboCache.getInstance().hasLimboPlayer(player.getName().toLowerCase())) {
|
||||
//add cache entry - otherwise logging in wouldn't work
|
||||
LimboCache.getInstance().addLimboPlayer(player);
|
||||
}
|
||||
|
||||
//skips registration and login
|
||||
NewAPI.getInstance().forceLogin(player);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin
|
||||
*/
|
||||
public interface AuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
*/
|
||||
void forceLogin(Player player);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import de.st_ddt.crazylogin.CrazyLogin;
|
||||
import de.st_ddt.crazylogin.data.LoginPlayerData;
|
||||
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/ST-DDT/CrazyLogin
|
||||
* Project page: http://dev.bukkit.org/server-mods/crazylogin/
|
||||
*/
|
||||
public class CrazyLoginHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
|
||||
CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
|
||||
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData == null) {
|
||||
//create a fake account - this will be saved to the database with the password=FAILEDLOADING
|
||||
//user cannot login with that password unless the admin uses plain text
|
||||
playerData = new LoginPlayerData(player);
|
||||
crazyDatabase.save(playerData);
|
||||
} else {
|
||||
//mark the account as logged in
|
||||
playerData.setLoggedIn(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import com.lenis0012.bukkit.ls.LoginSecurity;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
|
||||
* Project page: https://github.com/lenis0012/LoginSecurity-2
|
||||
*
|
||||
* on join: https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282
|
||||
*/
|
||||
public class LoginSecurityHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
//Login command of this plugin: (How the plugin logs the player in)
|
||||
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39
|
||||
LoginSecurity securityPlugin = LoginSecurity.instance;
|
||||
String name = player.getName().toLowerCase();
|
||||
|
||||
//mark the user as logged in
|
||||
securityPlugin.authList.remove(name);
|
||||
//cancel timeout timer
|
||||
securityPlugin.thread.timeout.remove(name);
|
||||
//remove effects
|
||||
securityPlugin.rehabPlayer(player, name);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import de.luricos.bukkit.xAuth.xAuth;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer.Status;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/LycanDevelopment/xAuth/
|
||||
* Project page: http://dev.bukkit.org/bukkit-plugins/xauth/
|
||||
*/
|
||||
public class xAuthHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
xAuth xAuthPlugin = xAuth.getPlugin();
|
||||
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
//we checked that the player is premium (paid account)
|
||||
xAuthPlayer.setPremium(true);
|
||||
//mark the player online
|
||||
xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName());
|
||||
|
||||
//update last login time
|
||||
xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis()));
|
||||
|
||||
//mark the player as logged in
|
||||
xAuthPlayer.setStatus(Status.AUTHENTICATED);
|
||||
|
||||
//restore inventory
|
||||
xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
import com.github.games647.fastlogin.hooks.AuthPlugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
public class PlayerListener implements Listener {
|
||||
|
||||
private final FastLogin plugin;
|
||||
private final AuthPlugin authPlugin;
|
||||
|
||||
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.authPlugin = authPlugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onJoin(PlayerJoinEvent joinEvent) {
|
||||
final Player player = joinEvent.getPlayer();
|
||||
String address = player.getAddress().toString();
|
||||
|
||||
//removing the session because we now use it
|
||||
PlayerSession session = plugin.getSessions().remove(address);
|
||||
//check if it's the same player as we checked before
|
||||
if (session != null && session.getUsername().equals(player.getName())
|
||||
&& session.isVerified()) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName());
|
||||
authPlugin.forceLogin(player);
|
||||
}
|
||||
//Wait before auth plugin initializes the player
|
||||
}, 1 * 20L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Login_Start
|
||||
*
|
||||
* String=Username
|
||||
*/
|
||||
public class StartPacketListener extends PacketAdapter {
|
||||
|
||||
//only premium (paid account) users have a uuid from there
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
|
||||
private final FastLogin plugin;
|
||||
|
||||
//just create a new once on plugin enable
|
||||
private final Random random = new Random();
|
||||
//compile the pattern on plugin enable
|
||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||
|
||||
public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
||||
//run async in order to not block the server, because we make api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//this includes ip and port. Should be unique for 2 Minutes
|
||||
String sessionKey = player.getAddress().toString();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(sessionKey);
|
||||
|
||||
String username = packet.getGameProfiles().read(0).getName();
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
||||
, new Object[]{sessionKey, username});
|
||||
//do premium login process
|
||||
if (isPremium(username)) {
|
||||
//minecraft server implementation
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
||||
sentEncryptionRequest(sessionKey, username, player, packetEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPremium(String playerName) {
|
||||
//check if it's a valid playername and the user activated fast logins
|
||||
if (playernameMatcher.matcher(playerName).matches() && plugin.getEnabledPremium().contains(playerName)) {
|
||||
//only make a API call if the name is valid existing mojang account
|
||||
try {
|
||||
HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName);
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
return responseCode == HttpURLConnection.HTTP_OK;
|
||||
//204 - no content for not found
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sentEncryptionRequest(String sessionKey, String username, Player player, PacketEvent packetEvent) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
try {
|
||||
/**
|
||||
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
|
||||
*
|
||||
* ServerID="" (String)
|
||||
* key=public server key
|
||||
* verifyToken=random 4 byte array
|
||||
*/
|
||||
PacketContainer newPacket = protocolManager
|
||||
.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
|
||||
|
||||
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic());
|
||||
byte[] verifyToken = new byte[4];
|
||||
random.nextBytes(verifyToken);
|
||||
newPacket.getByteArrays().write(0, verifyToken);
|
||||
|
||||
protocolManager.sendServerPacket(player, newPacket, false);
|
||||
|
||||
//cancel only if the player has a paid account otherwise login as normal offline player
|
||||
packetEvent.setCancelled(true);
|
||||
plugin.getSessions().put(sessionKey, new PlayerSession(verifyToken, username));
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# project informations for Bukkit in order to register our plugin with all it components
|
||||
# ${project.name} are variables from Maven (pom.xml) which will be replaced after the build
|
||||
name: ${project.name}
|
||||
version: ${project.version}
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
# meta informations for plugin managers
|
||||
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
|
||||
description: |
|
||||
${project.description}
|
||||
website: ${project.url}
|
||||
dev-url: ${project.url}
|
||||
|
||||
depend: [ProtocolLib]
|
||||
softdepend:
|
||||
- xAuth
|
||||
- AuthMe
|
||||
- CrazyLogin
|
||||
- LoginSecurity
|
||||
|
||||
commands:
|
||||
premium:
|
||||
description: 'Marks the invoker or the player specified as premium'
|
||||
aliases: [prem, fastlogin, loginfast]
|
||||
usage: /<command> [player]
|
||||
|
||||
permissions:
|
||||
${project.artifactId}.command.premium:
|
||||
description: 'Mark themselves as premium using a command'
|
||||
default: true
|
||||
${project.artifactId}.command.premium.others:
|
||||
description: 'Mark other people as premium'
|
||||
62
universal/pom.xml
Normal file
62
universal/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.0</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>fastlogin-universal</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLoginUniversal</name>
|
||||
|
||||
<build>
|
||||
<defaultGoal>package</defaultGoal>
|
||||
<finalName>${project.parent.name}</finalName>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.4.3</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>${project.groupId}:*</include>
|
||||
<include>com.zaxxer:HikariCP</include>
|
||||
<include>org.slf4j:*</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.bukkit</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.bungee</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Reference in New Issue
Block a user