Compare commits

..

6 Commits
1.1 ... 0.2.2

58 changed files with 853 additions and 5284 deletions

6
.gitignore vendored
View File

@@ -6,7 +6,6 @@
# netbeans
/nbproject
nb-configuration.xml
/bukkit/nbproject/
# maven
/target
@@ -41,8 +40,3 @@ gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Project module targets
bukkit/target
universal/target
bungee/target

View File

@@ -1,15 +0,0 @@
# 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

View File

@@ -1,105 +0,0 @@
######1.1
* Make the configuration options also work under BungeeCord (premiumUUID, forwardSkin)
* Catch configuration loading exception if it's not spigot build
* Fix config loading for older Spigot builds
######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
View File

@@ -1,156 +1,13 @@
# FastLogin
[![Build Status](https://travis-ci.org/games647/FastLogin.svg?branch=master)](https://travis-ci.org/games647/FastLogin)
[![Donate Button](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8ZBULMAPN7MZC)
Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentification.
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)
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/)

Binary file not shown.

Binary file not shown.

View File

@@ -1,155 +0,0 @@
<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.1</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>

View File

@@ -1,250 +0,0 @@
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.reflect.FuzzyReflection;
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.lang.reflect.Method;
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.configuration.file.YamlConfiguration;
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;
}
try {
if (Bukkit.spigot().getConfig().isBoolean("settings.bungeecord")) {
bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord");
} else {
Method getConfigMethod = FuzzyReflection.fromObject(getServer().spigot(), true)
.getMethodByName("getSpigotConfig");
getConfigMethod.setAccessible(true);
YamlConfiguration spigotConfig = (YamlConfiguration) getConfigMethod.invoke(getServer().spigot());
bungeeCord = spigotConfig.getBoolean("settings.bungeecord");
}
} catch (Exception | NoSuchMethodError ex) {
getLogger().warning("Cannot check bungeecord support. You use a non-spigot build");
}
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;
}
}

View File

@@ -1,124 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
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.ChatColor;
import org.bukkit.entity.Player;
public class ForceLoginTask implements Runnable {
protected final FastLoginBukkit plugin;
protected final Player player;
public ForceLoginTask(FastLoginBukkit plugin, Player player) {
this.plugin = plugin;
this.player = player;
}
@Override
public void run() {
if (!isOnlineThreadSafe()) {
return;
}
//remove the bungeecord identifier if there is ones
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
PlayerSession session = plugin.getSessions().get(id);
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
Storage storage = plugin.getStorage();
PlayerProfile playerProfile = null;
if (storage != null) {
playerProfile = storage.getProfile(player.getName(), false);
}
if (session == null) {
//cracked player
if (playerProfile != null) {
playerProfile.setUuid(null);
playerProfile.setPremium(false);
storage.save(playerProfile);
}
//check if it's the same player as we checked before
} else if (player.getName().equals(session.getUsername())) {
//premium player
if (authPlugin == null) {
//maybe only bungeecord plugin
sendSuccessNotification();
} else {
boolean success = false;
if (isOnlineThreadSafe() && session.isVerified()) {
if (session.needsRegistration()) {
success = forceRegister(authPlugin, player);
} else {
success = forceLogin(authPlugin, player);
}
}
if (success) {
//update only on success to prevent corrupt data
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
//save cracked players too
playerProfile.setPremium(session.isVerified());
storage.save(playerProfile);
}
sendSuccessNotification();
}
}
}
}
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;
}
private void sendSuccessNotification() {
if (plugin.isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("SUCCESS");
player.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
}
}
private boolean isOnlineThreadSafe() {
//the playerlist isn't thread-safe
Future<Boolean> onlineFuture = Bukkit.getScheduler().callSyncMethod(plugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return player.isOnline();
}
});
try {
return onlineFuture.get();
} catch (InterruptedException | ExecutionException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to perform thread-safe online check", ex);
return false;
}
}
}

View File

@@ -1,124 +0,0 @@
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;
}
}

View File

@@ -1,78 +0,0 @@
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 synchronized long getLastLogin() {
return lastLogin;
}
public synchronized void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
}

View File

@@ -1,144 +0,0 @@
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;
}
}

View File

@@ -1,201 +0,0 @@
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);
}
}
}
}

View File

@@ -1,81 +0,0 @@
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());
}
}
}

View File

@@ -1,87 +0,0 @@
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());
}
}
}

View File

@@ -1,35 +0,0 @@
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;
}
}

View File

@@ -1,60 +0,0 @@
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);
}

View File

@@ -1,119 +0,0 @@
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 && result.isLoggedIn()) {
//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) {
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;
}
}

View File

@@ -1,92 +0,0 @@
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());
String storedPassword = dataManager.getPassword(uuidString);
if (storedPassword != null && storedPassword.equals(passwordHash)) {
//the register method silents any excpetion so check if our entry was saved
return forceLogin(player);
}
return false;
}
}

View File

@@ -1,68 +0,0 @@
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 authPlayer.isLoggedIn();
}
});
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;
}
}

View File

@@ -1,61 +0,0 @@
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.bukkit.plugin.Plugin;
import ultraauth.api.UltraAuthAPI;
import ultraauth.main.Main;
import ultraauth.managers.PlayerManager;
/**
* Project page:
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
* Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
*/
public class UltraAuthHook implements BukkitAuthPlugin {
protected final Plugin ultraAuthPlugin = Main.main;
@Override
public boolean forceLogin(final Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
UltraAuthAPI.authenticatedPlayer(player);
return UltraAuthAPI.isAuthenticated(player);
}
});
try {
return future.get();
} catch (InterruptedException | ExecutionException ex) {
ultraAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
return false;
}
}
@Override
public boolean isRegistered(String playerName) throws Exception {
return UltraAuthAPI.isRegisterd(new FakePlayer(playerName));
}
@Override
public boolean forceRegister(Player player, String password) {
UltraAuthAPI.setPlayerPasswordOnline(player, password);
if (PlayerManager.getInstance().checkPlayerPassword(player, password)) {
//the register method silents any excpetion so check if our entry was saved
return forceLogin(player);
}
return false;
}
}

View File

@@ -1,90 +0,0 @@
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)
xAuthPlayer.setPremium(true);
//unprotect the inventory, op status...
return xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
}
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;
}
}
}

View File

@@ -1,57 +0,0 @@
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 = 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);
}
}
if (!plugin.isBungeeCord()) {
//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);
}
}

View File

@@ -1,121 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.ForceLoginTask;
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())) {
//blacklist this target player for BungeeCord Id brute force attacks
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
//bungeecord UUID
long mostSignificantBits = dataInput.readLong();
long leastSignificantBits = dataInput.readLong();
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
plugin.getLogger().log(Level.FINEST, "Received proxy id {0} from {1}", new Object[]{sourceId, player});
//fail if BungeeCord support is disabled (id = null)
if (sourceId.equals(proxyId)) {
final PlayerSession playerSession = new PlayerSession(playerName);
final String id = '/' + checkedPlayer.getAddress().getAddress().getHostAddress() + ':'
+ checkedPlayer.getAddress().getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
playerSession.setVerified(true);
playerSession.setRegistered(true);
plugin.getSessions().put(id, 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(id, playerSession);
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
}
});
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
}
}
}
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;
}
}

View File

@@ -1,84 +0,0 @@
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);
}
}
}

View File

@@ -1,149 +0,0 @@
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;
}
}

View File

@@ -1,73 +0,0 @@
# 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

View File

@@ -1,57 +0,0 @@
# 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

View File

@@ -1,60 +0,0 @@
<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.1</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>

View File

@@ -1,143 +0,0 @@
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.Util;
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 {
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
public static UUID parseId(String withoutDashes) {
return Util.getUUID(withoutDashes);
}
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");
}
}
}

View File

@@ -1,74 +0,0 @@
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;
private final Server server;
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player, Server server) {
this.plugin = plugin;
this.player = player;
this.server = server;
}
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
//force login only on success
if (player.getPendingConnection().isOnlineMode()) {
boolean autoRegister = plugin.getPendingAutoRegister().remove(player.getPendingConnection()) != null;
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
if (authPlugin == null) {
sendBukkitLoginNotification(autoRegister);
} else if (player.isConnected()) {
if (autoRegister) {
String password = plugin.generateStringPassword();
if (authPlugin.forceRegister(player, password)) {
sendBukkitLoginNotification(autoRegister);
}
} else if (authPlugin.forceLogin(player)) {
sendBukkitLoginNotification(autoRegister);
}
}
} else {
//cracked player
//update only on success to prevent corrupt data
playerProfile.setPremium(false);
plugin.getStorage().save(playerProfile);
}
}
private void sendBukkitLoginNotification(boolean autoRegister) {
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());
if (server != null) {
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
}
}
}

View File

@@ -1,80 +0,0 @@
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;
}
}

View File

@@ -1,15 +0,0 @@
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;
}
}

View File

@@ -1,204 +0,0 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import java.lang.reflect.Field;
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.PostLoginEvent;
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.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.connection.LoginResult.Property;
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 onLogin(PostLoginEvent loginEvent) {
ProxiedPlayer player = loginEvent.getPlayer();
PendingConnection connection = player.getPendingConnection();
String username = connection.getName();
if (connection.isOnlineMode()) {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
playerProfile.setUuid(player.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
InitialHandler initialHandler = (InitialHandler) connection;
if (!plugin.getConfiguration().getBoolean("premiumUuid")) {
try {
UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(Charsets.UTF_8));
Field idField = initialHandler.getClass().getDeclaredField("uniqueId");
idField.setAccessible(true);
idField.set(connection, offlineUUID);
//bungeecord doesn't support overriding the premium uuid
// connection.setUniqueId(offlineUUID);
} catch (NoSuchFieldException | IllegalAccessException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to set offline uuid", ex);
}
}
if (!plugin.getConfiguration().getBoolean("forwardSkin")) {
//this is null on offline mode
LoginResult loginProfile = initialHandler.getLoginProfile();
if (loginProfile != null) {
loginProfile.setProperties(new Property[]{});
}
}
}
}
@EventHandler
public void onServerConnected(ServerConnectedEvent serverConnectedEvent) {
ProxiedPlayer player = serverConnectedEvent.getPlayer();
ForceLoginTask loginTask = new ForceLoginTask(plugin, player, serverConnectedEvent.getServer());
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
}
@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);
TextComponent textComponent = new TextComponent("Added to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
});
} else if ("SUCCESS".equals(subchannel)) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), false);
playerProfile.setPremium(true);
//we override this in the loginevent
// playerProfile.setUuid(forPlayer.getUniqueId());
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Removed to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
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 synchronized long getLastLogin() {
return lastLogin;
}
public synchronized void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
}

View File

@@ -1,201 +0,0 @@
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);
}
}
}
}

View File

@@ -1,104 +0,0 @@
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);
}
}

View File

@@ -1,55 +0,0 @@
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);
}

View File

@@ -1,16 +0,0 @@
# 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.

111
pom.xml
View File

@@ -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>pom</packaging>
<packaging>jar</packaging>
<name>FastLogin</name>
<version>1.1</version>
<version>0.2.2</version>
<inceptionYear>2015</inceptionYear>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<url>https://github.com/games647/FastLogin</url>
<description>
Automatically logins premium (paid accounts) player on a offline mode server
</description>
@@ -21,12 +21,6 @@
<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>
@@ -47,12 +41,14 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<version>3.2</version>
<configuration>
<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>
@@ -75,8 +71,7 @@
<!--Add the license to jar in order to see it in the final jar-->
<resource>
<!--Parent folder-->
<directory>${basedir}/..</directory>
<directory>${basedir}</directory>
<includes>
<include>LICENSE</include>
</includes>
@@ -84,19 +79,95 @@
</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>
<!--Database pooling-->
<!--Server API-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.4.6</version>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.8.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->
<!--Library for listening and sending Minecraft packets-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.20</version>
<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>
</dependency>
</dependencies>
</project>

View File

@@ -1,48 +1,50 @@
package com.github.games647.fastlogin.bukkit;
package com.github.games647.fastlogin;
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 EncryptionUtil {
public class Encryption {
public static KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1_024);
return keyPairGenerator.generateKeyPair();
keypairgenerator.initialize(1024);
return keypairgenerator.generateKeyPair();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
//Should be existing in every vm
throw new ExceptionInInitializerError(nosuchalgorithmexception);
return null;
}
}
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) {
@@ -59,26 +61,26 @@ public class EncryptionUtil {
}
}
// 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;
// }
public static PublicKey decodePublicKey(byte[] encodedKey) {
try {
X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
KeyFactory keyfactory = KeyFactory.getInstance("RSA");
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] encryptedSharedKey) {
return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES");
return keyfactory.generatePublic(x509encodedkeyspec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
;
}
System.err.println("Public key reconstitute failed!");
return null;
}
public static byte[] decryptData(Key key, byte[] data) {
return cipherOperation(Cipher.DECRYPT_MODE, key, data);
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);
}
private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
@@ -105,19 +107,19 @@ public class EncryptionUtil {
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);
// }
// }
private EncryptionUtil() {
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() {
//utility
}
}

View File

@@ -0,0 +1,164 @@
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;
}
}

View File

@@ -0,0 +1,56 @@
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;
}
}

View File

@@ -0,0 +1,45 @@
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;
}
}

View File

@@ -0,0 +1,25 @@
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: http://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);
}
}

View File

@@ -0,0 +1,16 @@
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);
}

View File

@@ -0,0 +1,31 @@
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);
}
}
}

View File

@@ -0,0 +1,30 @@
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);
}
}

View File

@@ -0,0 +1,36 @@
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);
}
}

View File

@@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit.listener;
package com.github.games647.fastlogin.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolManager;
@@ -7,34 +7,30 @@ 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.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.Encryption;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.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
*
@@ -43,11 +39,13 @@ import org.bukkit.entity.Player;
*/
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 FastLoginBukkit plugin;
private final FastLogin plugin;
public EncryptionPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
public EncryptionPacketListener(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.ENCRYPTION_BEGIN).optionAsync());
@@ -71,101 +69,36 @@ 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 (so getName() doesn't work) - now uses ip:port as key
//the player name is unknown to ProtocolLib - 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 at invalid state"
, "Player {0} tried to send encryption response"
+ "on an invalid connection state"
, player.getAddress());
return;
}
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);
byte[] sharedSecret = packet.getByteArrays().read(0);
byte[] clientVerify = packet.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(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
//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(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
, session.getUsername(), player.getAddress(), serverVerify, clientVerify);
return;
}
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 {
SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager(player);
@@ -179,50 +112,90 @@ public class EncryptionPacketListener extends PacketAdapter {
encryptConnectionMethod.invoke(networkManager, loginKey);
} catch (ReflectiveOperationException ex) {
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
return false;
return;
}
return true;
//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);
}
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
, Object... arguments) {
plugin.getLogger().log(logLevel, logMessage, arguments);
kickPlayer(packetEvent.getPlayer(), kickReason);
packetEvent.getPlayer().kickPlayer(kickMessage);
//cancel the event in order to prevent the server receiving an invalid packet
packetEvent.setCancelled(true);
}
private void kickPlayer(Player player, String reason) {
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
private Object getNetworkManager(Player player)
throws SecurityException, IllegalAccessException, NoSuchFieldException {
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = injector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
try {
//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);
}
Object rawInjector = injectorField.get(injector);
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
injectorField.setAccessible(true);
return injectorField.get(rawInjector);
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username, Player from) {
//see StartPacketListener for packet information
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
private boolean hasJoinedServer(String username, String serverId) {
try {
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
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);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
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);
WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(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
kickPlayer(from, "Error occured");
from.kickPlayer("Error occurred");
}
}
}

View File

@@ -0,0 +1,53 @@
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);
// }
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
@Override
public void run() {
if (player.isOnline()) {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
authPlugin.forceLogin(player);
}
}
//Wait before auth plugin initializes the player
}, 1 * 20L);
}
}
}

View File

@@ -0,0 +1,131 @@
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/";
//this includes a-zA-Z1-9_
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);
}
}
}

View File

@@ -0,0 +1,33 @@
# 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]
permission: ${project.artifactId}.command.premium
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'

View File

@@ -1,62 +0,0 @@
<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.1</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>