Compare commits

..

110 Commits
1.3 ... 1.9

Author SHA1 Message Date
games647
87ca00d75d [SwitchMode] Kick the player only if the player is unknown to us 2016-09-21 09:16:19 +02:00
games647
62ffb1a904 [Bukkit] Fix adding to premium whitelist 2016-09-20 13:55:03 +02:00
games647
5075a71843 A few code styling things 2016-09-20 13:32:06 +02:00
games647
da266c7e91 Fix loading of settings 2016-09-19 17:59:45 +02:00
games647
acab4766b1 Remove database migration logging 2016-09-19 15:59:57 +02:00
games647
bef90d11cd Deploy only the universal jar to the target folder 2016-09-18 11:40:21 +02:00
games647
a02acd2d63 Remove the nasty UltraAuth fakeplayer workaround 2016-09-18 10:38:05 +02:00
games647
ca42a7c19e Refactor more code for more Java 8 and Guava usage 2016-09-17 15:19:07 +02:00
games647
b533197f05 Fix config loading in BungeeCord 2016-09-17 15:19:07 +02:00
Maxetto
c94711f315 Fix verb (#79) 2016-09-17 08:02:33 +02:00
games647
ee7af80bf0 Fix travis 2016-09-16 17:41:23 +02:00
games647
17c2099bf1 Make use of the awesome Java 8 features 2016-09-16 17:40:42 +02:00
games647
31d6b67381 Try to upgrade to Java 8. I hope enough people are using it. 2016-09-16 16:31:47 +02:00
games647
4b423c9ccb Update ProtocolSupport and use a maven repository for it now 2016-09-16 10:45:05 +02:00
games647
4292e9aaa0 Less deprecated warnings + Clean up 2016-09-15 11:10:52 +02:00
games647
07d0aededa Fix loading with unloaded configuration values 2016-09-15 10:33:17 +02:00
games647
218bc50c96 Drop support for LoginSecurity 1.X since 2.X seems to be stable 2016-09-14 17:44:32 +02:00
games647
a3b2e33aad Switch to vik1395 repository for BungeeAuth 2016-09-13 09:53:46 +02:00
games647
76f5ba7ed1 Refactor a lot of code + Add Guava v10 as shared library 2016-09-11 21:26:03 +02:00
games647
2cd50d23ad Revert converting auth hooks to the new format
-> backwards compatibility
2016-09-11 20:07:21 +02:00
games647
9f5f61f1c2 Do the same for the password generator 2016-09-11 19:57:27 +02:00
games647
3e9c8e3a7e More shared project code for less errors and less duplication 2016-09-11 18:59:42 +02:00
games647
8e5da01be0 Added configuration to disable auto logins for 2Factor authentication
(Fixes #65)
2016-09-09 16:52:44 +02:00
games647
5022c9aa7b Add cracked whitelist (Fixes #42)
(switch-mode -> switching to online-mode from offlinemode)
2016-09-09 16:40:24 +02:00
games647
ad1ab22586 Test another locale sqlite fix 2016-09-08 11:48:13 +02:00
games647
99ef5ce726 Fix correct cracked permission for bukkit 2016-09-08 10:06:43 +02:00
games647
9b7634a9f3 Fix LogIt repository 2016-09-04 16:35:57 +02:00
games647
115fc2e7ba A try to fix SQLite timestamp parsing 2016-09-04 12:14:28 +02:00
games647
b660951e1e Fix compatibility with older ProtocolLib versions (for 1.7)
because of the missing getMethodAcccessorOrNull method
2016-09-03 10:21:42 +02:00
games647
e495f70ccd Fix logging exceptions on encryption enabling 2016-09-01 20:09:34 +02:00
games647
b35d67b5c0 Add missing add-premium-other message 2016-09-01 19:41:58 +02:00
games647
58ac73a5a9 Fix update username in FastLogin database after nameChange (Fixes #67) 2016-08-31 13:29:06 +02:00
games647
ebe768f7a2 Fix ProtocolSupport autoRegister 2016-08-30 12:27:02 +02:00
games647
d20db79f46 Add second attemp login -> cracked (Fixes #51) 2016-08-29 17:38:46 +02:00
games647
c28d889c1b Remove complex nameChange convert since we now allow duplicate uuids 2016-08-26 13:32:28 +02:00
games647
ad60397851 Added auto login importers 2016-08-25 17:45:09 +02:00
games647
88fdeff3f1 Update documentation 2016-08-25 17:44:51 +02:00
games647
558ee1c92c Fix console support for cracked/premium commands (Fixes# 62) 2016-08-20 17:38:47 +02:00
games647
3e84ebd787 [BungeeAuth] Do not login the player if it's already logged in using
sessions
2016-08-19 22:00:07 +02:00
games647
36d7564c3a Fix race condition when waiting for bukkit message while
bungee redirects player
2016-08-19 21:07:29 +02:00
games647
596caa0573 Invoke forcelogin in BungeeCord only once 2016-08-19 20:52:51 +02:00
games647
fe4331298f Send a message on BungeeCord if there is only an auth plugin 2016-08-18 20:22:04 +02:00
games647
a67d84ef3f Fix race condition in bungee<->bukkit 2016-08-18 20:15:43 +02:00
games647
71362dfd7d Log bungeeauth exceptions 2016-08-18 09:16:06 +02:00
Maxetto
fcd98fce43 Fix Color Code (#54)
Got it wrong the first time.
2016-08-09 19:17:48 +02:00
games647
6c1c4e7286 Fix third-party not premium player detection 2016-08-09 14:24:57 +02:00
games647
164fb735d6 Fix ProtocolSupport BungeeCord 2016-08-07 11:41:03 +02:00
games647
fa1b0970a5 Dump to 1.7.1 2016-08-01 12:59:04 +02:00
games647
974bf498fc Fix protocollsupport autoregister 2016-07-31 11:56:20 +02:00
games647
27c04ff08f Fix BungeeCord autoRegister (Fixes #46) 2016-07-31 09:57:50 +02:00
games647
fb357424e6 Run the plugin message reader async to prevent the timeout event
warning from BungeeCord
2016-07-26 14:30:11 +02:00
games647
c73bb70256 FIx autoregister bug 2016-07-25 19:29:22 +02:00
games647
dc395cdc3f Remove debug code 2016-07-20 20:03:17 +02:00
games647
f7626ab969 Read the fully input from mcapi.ca instead of just one line 2016-07-20 11:54:00 +02:00
games647
5f9802d589 Fix third party profile parsing 2016-07-19 10:47:47 +02:00
games647
642c1621ad Fine tune timeout length 2016-07-13 13:00:25 +02:00
games647
eb965d5a48 Fix importing 2016-07-13 10:05:22 +02:00
games647
457bc9cf47 Fix SQLite drop index 2016-07-12 13:06:27 +02:00
games647
2ab3c6b77c Update AutoIn importer 2016-07-12 12:39:06 +02:00
games647
f27bad02d3 Remove the uuid index for name change conflicts 2016-07-12 12:23:32 +02:00
games647
9334296beb Fix saving on name change 2016-07-10 19:25:37 +02:00
games647
fd9940e6f0 Fix setting skin on Cauldron (Fixes #36) 2016-07-10 13:34:08 +02:00
games647
0745957e79 Fix BungeeCord not setting an premium uuid (Fixes #35) 2016-07-09 13:30:43 +02:00
games647
bb2e60f6e1 State why choose the loginEvent and fix it 2016-07-08 16:47:50 +02:00
games647
d15861b8e5 Use the correct message key 2016-07-08 16:47:50 +02:00
Maxetto
b84b340a77 Consistency language update (#33)
Messages that aren't targeted to the player invoking the command shouldn't start with "You are", that could be confusing.

Changed Warning message to be more eye-catching to the player. Assuming also that next line doesn't take the color code from previous line (otherwise change "&r" with "&6").
2016-07-07 14:51:14 +02:00
games647
c50249edea Switch to mcapi.ca and add configurable number of requests 2016-07-07 12:20:39 +02:00
games647
757ddb905a Make it buildable again 2016-07-04 21:40:42 +02:00
games647
9914b7f358 Fix player entry is not saved if namechangecheck is enabled 2016-07-04 21:26:03 +02:00
games647
bba4eb4eec Ignore all canceled events 2016-07-03 21:28:44 +02:00
games647
2b16f3341f Use the loginevent to send the client the offline uuid
-> skin applies on deactivated premium uuid
2016-07-03 16:58:23 +02:00
games647
167ce66057 Added note about skin forwarding if premium uuids are disabled 2016-07-03 14:18:27 +02:00
games647
8d1021e44c Update fake player methods 2016-06-29 19:04:12 +02:00
games647
a811a741f5 Change to lenis repository 2016-06-29 19:03:52 +02:00
games647
a6348766b3 Reduce the number of lookups if a cracked player already exists 2016-06-20 16:29:39 +02:00
games647
22dcc50950 I'm stupid (Related #27) 2016-06-20 16:16:31 +02:00
games647
bd3494eed0 Fix recursive method invocation (Related #27) 2016-06-20 15:27:43 +02:00
games647
1aba9a0f3b Added us.mcapi.com as third-party APIs to workaround rate-limits
(Fixes #27)
2016-06-20 14:12:29 +02:00
games647
6faf00e1bf Support for making requests to Mojang from different IPv4 addresses
(Related #27)
2016-06-20 13:52:37 +02:00
games647
0d89614f3c Add support for the new LoginSecurity version 2016-06-18 14:39:47 +02:00
games647
b009658eea Fix typo in BungeeCord message key 2016-06-16 15:10:25 +02:00
games647
2881689f09 Fixed default message copying 2016-06-15 17:35:10 +02:00
games647
6d1a97fd32 Added premium command warning 2016-06-15 13:55:57 +02:00
games647
b74faa2fd5 Fix missing translation 2016-06-15 13:26:20 +02:00
games647
4800a88886 Perform protocollib checks async/non-blocking 2016-06-14 19:36:34 +02:00
games647
92c9ab5b76 Use ProtocolLib as a soft dependency 2016-06-14 17:21:46 +02:00
games647
d90e3fdb44 Load the embed message as default 2016-06-14 17:02:12 +02:00
games647
8abbb8f07c Fix bungeecord support (Fixes #26) 2016-06-13 08:51:47 +02:00
games647
f04a44b1d2 Applies skin earlier to make it visible for other
plugins listening on login events
2016-06-11 17:16:03 +02:00
games647
1a66121977 Do not save players multiple times on server switch 2016-06-11 15:08:44 +02:00
games647
413a0325f8 Fixed BungeeCord force logins if there is a lobby server 2016-06-11 13:24:35 +02:00
games647
9fc7e0bf43 Fixed BungeeCord support by correctly saving the proxy ids (Fixes #22) 2016-06-11 10:32:53 +02:00
games647
ac8bcb1758 Fix default config deploy 2016-06-11 09:29:55 +02:00
games647
bebcb3e9de Make locale messages thread-safe 2016-06-10 10:37:04 +02:00
games647
0b899f61a8 Fixed message removal 2016-06-10 09:23:15 +02:00
games647
7733135ce4 Fixed NPE in BungeeCord on cracked login for existing players (#22) 2016-06-10 08:57:08 +02:00
games647
be89eec23b Fix NPE on premium name check if it's pure cracked player
(Fixes #21)
2016-06-10 08:53:06 +02:00
games647
679060d4e9 Fixed support for empty messages 2016-06-09 14:33:14 +02:00
games647
f6aa064835 Added localization messages (Fixes #20) 2016-06-09 12:43:04 +02:00
games647
de4b73c3bd Upgrade maven plugin version 2016-06-09 10:30:42 +02:00
games647
ac15829dcc Fixes insert (new player) for cracked players (Fixes #18) 2016-06-07 17:32:45 +02:00
games647
0b709997a4 Fix duplicate premium uuid check in BungeeCord 2016-06-07 16:46:18 +02:00
games647
8809875ca4 Fixed setting auth hook 2016-06-04 14:56:04 +02:00
games647
aa30c070b9 Add database importers (planned) 2016-06-02 15:02:15 +02:00
games647
51d0aefbf3 Added support of detecting name changes (Fixes #18) 2016-06-01 14:42:48 +02:00
games647
cb876a52bd Add support for multiple bungeecords (Fixes #19) 2016-05-28 17:21:08 +02:00
games647
3e844be65d Clean up project structure 2016-05-26 11:04:13 +02:00
games647
dce95cf0d0 Prevent thread create violation in BungeeCord 2016-05-25 09:40:43 +02:00
games647
81eeaeae83 Fix recursive call for bungeecord 2016-05-23 21:11:34 +02:00
games647
6b1542de88 Now bungeeCord detection should work for all server versions 2016-05-23 17:02:32 +02:00
89 changed files with 4209 additions and 3839 deletions

3
.gitignore vendored
View File

@@ -45,4 +45,5 @@ gradle-app.setting
# Project module targets
bukkit/target
universal/target
bungee/target
bungee/target
core/target

View File

@@ -8,8 +8,5 @@ language: java
script: mvn compile test
# We run on 7+
jdk:
- openjdk7
- oraclejdk7
- oraclejdk8
# We run on 8
jdk: [oraclejdk8]

View File

@@ -1,3 +1,95 @@
######1.9
* Added second attempt login -> cracked login
* Added cracked whitelist (switch-mode -> switching to online-mode from offlinemode)
* Added configuration to disable auto logins for 2Factor authentication
* Added missing add-premium-other message
* Upgrade to Java 8 -> Minimize file size
* Refactored/Cleaned up a lot of code
* [API] Deprecated platform specific authplugin. Please use AuthPlugin< platform specific player type >
* [API] Deprecated bukkit's password generator. Please use PasswordGenerator< platform specific player type >
* Fix ProtocolSupport autoRegister
* Fix update username in FastLogin database after nameChange
* Fix logging exceptions on encryption enabling
* Fix compatibility with older ProtocolLib versions (for 1.7) because of the missing getMethodAcccessorOrNull method
* Fix correct cracked permission for bukkit
* A try to fix SQLite timestamp parsing
* Drop support for LoginSecurity 1.X since 2.X seems to be stable
* Remove the nasty UltraAuth fakeplayer workaround by using a new api method. You should UltraAuth if you have it
######1.8
* Added autoIn importer
* Added BFA importer
* Added ElDziAuth importer
* Fix third-party not premium player detection
* Fix ProtocolSupport BungeeCord
* Fix duplicate logins for BungeeAuth users
######1.7.1
* Fix BungeeCord autoRegister (Fixes #46)
* Fix protocollsupport autoregister
######1.7
* Added support for making requests to Mojang from different IPv4 addresses
* Added us.mcapi.com as third-party APIs to workaround rate-limits
* Fixed NPE in BungeeCord on cracked session
* Fixed skin applies if premium uuid is deactivated
* Fix player entry is not saved if namechangecheck is enabled
* Fix skin applies for third-party plugins
* Switch to mcapi.ca for uuid lookups
* Fix BungeeCord not setting an premium uuid
* Fix setting skin on Cauldron
* Fix saving on name change
######1.6.2
* Fixed support for new LoginSecurity version
######1.6.1
* Fix message typo in BungeeCord which created a NPE if premium-warning is activated
######1.6
* Add a warning message if the user tries to invoke the premium command
* Added missing translation if the server isn't fully started
* Removed ProtocolLib as required dependency. You can use ProtocolSupport or BungeeCord as alternative
* Reduce the number of worker threads from 5 to 3 in ProtocolLib
* Process packets in ProtocolLib async/non-blocking -> better performance
* Fixed missing translation in commands
* Fixed cracked command not working on BungeeCord
* Fix error if forward skins is disabled
######1.5.2
* Fixed BungeeCord force logins if there is a lobby server
* Removed cache expire in BungeeCord
* Applies skin earlier to make it visible for other plugins listening on login events
######1.5.1
* Fixed BungeeCord support by correctly saving the proxy ids
######1.5
* Added localization
* Fixed NPE on premium name check if it's pure cracked player
* Fixed NPE in BungeeCord on cracked login for existing players
* Fixed saving of existing cracked players
######1.4
* Added Bungee setAuthPlugin method
* Added nameChangeCheck
* Multiple BungeeCord support
######1.3.1
* Prevent thread create violation in BungeeCord
######1.3
* Added support for AuthMe 3.X
@@ -123,4 +215,4 @@
* Added better error handling
#####0.1
* First release
* First release

View File

@@ -15,11 +15,14 @@ So they don't need to enter passwords. This is also called auto login (auto-logi
* Forge/Sponge message support
* Premium UUID support
* Forwards Skins
* Detect user name changed and will update the existing database record
* 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
* Locale messages
* Import the database from similar plugins
* Free
* Open source
@@ -28,10 +31,14 @@ So they don't need to enter passwords. This is also called auto login (auto-logi
###Commands:
* /premium [player] Label the invoker or the argument as paid account
* /cracked [player] Label the invoker or the argument as cracked account
* /importdb <autoIn/bpa/eldzi> <mysql/sqlite> [host:port] [database] [username] [password] - Imports the database from another plugin
###Permissions:
* fastlogin.bukkit.command.premium
* fastlogin.bukkit.command.cracked
* fastlogin.command.premium.other
* fastlogin.command.cracked.other
* fastlogin.command.import
###Requirements:
* Plugin: [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/) or [ProtocolSupport](http://www.spigotmc.org/resources/protocolsupport.7201/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.3</version>
<version>1.9</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -22,6 +22,12 @@
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<!--LoginSecurity-->
<repository>
<id>lenis0012-repo</id>
<url>http://ci.lenis0012.com/plugin/repository/everything/</url>
</repository>
<!--ProtocolLib-->
<repository>
<id>dmulloy2-repo</id>
@@ -40,11 +46,6 @@
<url>http://repo.luricos.de/bukkit-plugins/</url>
</repository>
<repository>
<id>logit-only-repo</id>
<url>http://ci.ac3-servers.eu/job/LogIt-Classic/2/maven-repository/repository/</url>
</repository>
<!--Github automatic maven builds-->
<repository>
<id>jitpack.io</id>
@@ -53,11 +54,18 @@
</repositories>
<dependencies>
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<!--Server API-->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.9-R0.1-SNAPSHOT</version>
<version>1.10-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -65,17 +73,15 @@
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>3.6.5-SNAPSHOT</version>
<optional>true</optional>
<version>3.6.5</version>
</dependency>
<dependency>
<groupId>protcolsupport</groupId>
<artifactId>ProtocolSupport</artifactId>
<version>Build-337</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/ProtocolSupport b337.jar</systemPath>
</dependency>
<dependency>
<groupId>com.github.ProtocolSupport</groupId>
<artifactId>ProtocolSupport</artifactId>
<!--4.25.dev-->
<version>5554413b51</version>
</dependency>
<!--Login Plugins-->
<dependency>
@@ -92,9 +98,10 @@
</dependency>
<dependency>
<groupId>com.github.lenis0012</groupId>
<artifactId>LoginSecurity-2</artifactId>
<version>-9c09e73b7f-1</version>
<groupId>com.lenis0012.bukkit</groupId>
<artifactId>loginsecurity</artifactId>
<version>2.1.3-SNAPSHOT</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>*</groupId>
@@ -103,22 +110,25 @@
</exclusions>
</dependency>
<dependency>
<groupId>io.github.lucaseasedup.logit</groupId>
<artifactId>LogIt</artifactId>
<version>SNAPSHOT</version>
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>LogIt</artifactId>
<version>9e3581db27</version>
<optional>true</optional>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependency>
<dependency>
<groupId>com.github.RoyalDev</groupId>
<artifactId>RoyalAuth</artifactId>
<version>-e21354a9b7-1</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>*</groupId>
@@ -166,7 +176,7 @@
<version>2.0.2</version>
<optional>true</optional>
<scope>system</scope>
<systemPath>${project.basedir}/lib/UltraAuth v2.0.2.jar</systemPath>
<systemPath>${project.basedir}/lib/UltraAuth v2.1.2.jar</systemPath>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,72 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.File;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
public class BukkitCore extends FastLoginCore<Player> {
private final FastLoginBukkit plugin;
public BukkitCore(FastLoginBukkit plugin) {
super(plugin.getConfig().getValues(false));
this.plugin = plugin;
}
@Override
public File getDataFolder() {
return plugin.getDataFolder();
}
@Override
public Logger getLogger() {
return plugin.getLogger();
}
@Override
public ThreadFactory getThreadFactory() {
String pluginName = plugin.getName();
return new ThreadFactoryBuilder()
.setNameFormat(pluginName + " Database Pool Thread #%1$d")
//Hikari create daemons by default
.setDaemon(true)
.build();
}
@Override
public void loadMessages() {
plugin.saveResource("messages.yml", false);
File messageFile = new File(plugin.getDataFolder(), "messages.yml");
YamlConfiguration messageConfig = YamlConfiguration.loadConfiguration(messageFile);
InputStreamReader defaultReader = new InputStreamReader(plugin.getResource("messages.yml"), Charsets.UTF_8);
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(defaultReader);
messageConfig.setDefaults(defaults);
messageConfig.getKeys(false).forEach((key) -> {
String message = ChatColor.translateAlternateColorCodes('&', messageConfig.getString(key));
if (!message.isEmpty()) {
localeMessages.put(key, message);
}
});
}
@Override
public MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests) {
return new MojangApiBukkit(logger, addresses, requests);
}
}

View File

@@ -0,0 +1,101 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
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 BukkitLoginSession extends LoginSession {
private final String serverId;
private final byte[] verifyToken;
private boolean verified;
private String encodedSkinData;
private String skinSignature;
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
, PlayerProfile profile) {
super(username, registered, profile);
this.serverId = serverId;
this.verifyToken = ArrayUtils.clone(verifyToken);
}
//available for bungeecord
public BukkitLoginSession(String username, boolean registered) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, registered, null);
}
//cracked player
public BukkitLoginSession(String username, PlayerProfile profile) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
}
/**
* 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);
}
public synchronized String getEncodedSkinData() {
return encodedSkinData;
}
public synchronized String getSkinSignature() {
return skinSignature;
}
/**
* Sets the premium skin property which was retrieved by the session server
*
* @param encodedData
* @param skinSignature
*/
public synchronized void setSkin(String encodedData, String skinSignature) {
this.encodedSkinData = encodedData;
this.skinSignature = skinSignature;
}
/**
* 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

@@ -1,12 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import org.apache.commons.lang.RandomStringUtils;
import org.bukkit.entity.Player;
public class DefaultPasswordGenerator implements PasswordGenerator {
@Override
public String getRandomPassword(Player player) {
return RandomStringUtils.random(8, true, true);
}
}

View File

@@ -10,6 +10,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.stream.Stream;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -48,9 +49,7 @@ public class EncryptionUtil {
private static byte[] digestOperation(String algo, byte[]... content) {
try {
MessageDigest messagedigest = MessageDigest.getInstance(algo);
for (byte[] data : content) {
messagedigest.update(data);
}
Stream.of(content).forEach(messagedigest::update);
return messagedigest.digest();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {

View File

@@ -3,35 +3,27 @@ package com.github.games647.fastlogin.bukkit;
import com.avaje.ebeaninternal.api.ClassUtil;
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.ImportCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
import com.github.games647.fastlogin.bukkit.hooks.AuthMeHook;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.bukkit.hooks.CrazyLoginHook;
import com.github.games647.fastlogin.bukkit.hooks.LogItHook;
import com.github.games647.fastlogin.bukkit.hooks.LoginSecurityHook;
import com.github.games647.fastlogin.bukkit.hooks.UltraAuthHook;
import com.github.games647.fastlogin.bukkit.hooks.xAuthHook;
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.collect.Lists;
import com.github.games647.fastlogin.bukkit.listener.protocollib.EncryptionPacketListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.LoginSkinApplyListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.StartPacketListener;
import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.security.KeyPair;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@@ -40,56 +32,33 @@ import org.bukkit.plugin.java.JavaPlugin;
*/
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));
}
private static final int WORKER_THREADS = 3;
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
protected boolean bungeeCord;
private Storage storage;
private boolean bungeeCord;
private BukkitCore core;
private boolean serverStarted;
//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>() {
//1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
private final ConcurrentMap<String, BukkitLoginSession> session = FastLoginCore.buildCache(1, -1);
@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);
private PasswordGenerator passwordGenerator = new DefaultPasswordGenerator();
@Override
public void onEnable() {
saveDefaultConfig();
core = new BukkitCore(this);
core.loadMessages();
core.setApiConnector();
try {
if (ClassUtil.isPresent("org.spigotmc.SpigotConfig")) {
bungeeCord = (boolean) FuzzyReflection.fromClass(Class.forName("org.spigotmc.SpigotConfig"))
.getFieldByType("bungee", Boolean.TYPE).get(null);
bungeeCord = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
}
} catch (Exception | NoSuchMethodError ex) {
getLogger().warning("Cannot check bungeecord support. You use a non-spigot build");
ex.printStackTrace();
} catch (Exception ex) {
getLogger().log(Level.WARNING, "Cannot check bungeecord support. You use a non-spigot build", ex);
}
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");
@@ -99,89 +68,79 @@ public class FastLoginBukkit extends JavaPlugin {
if (bungeeCord) {
setServerStarted();
//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);
if (!core.setupDatabase()) {
setEnabled(false);
return;
}
if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) {
getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this);
} else {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
} else if (getServer().getPluginManager().isPluginEnabled("ProtocolLib")) {
//we are performing HTTP request on these so run it async (seperate from the Netty IO threads)
AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager();
AsynchronousManager asynchronousManager = ProtocolLibrary.getProtocolManager().getAsynchronousManager();
StartPacketListener startPacketListener = new StartPacketListener(this, protocolManager);
EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this, protocolManager);
StartPacketListener startPacketListener = new StartPacketListener(this);
EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this);
asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS);
asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS);
getServer().getPluginManager().registerEvents(new LoginSkinApplyListener(this), this);
} else {
getLogger().warning("Either ProtocolLib or ProtocolSupport have to be installed "
+ "if you don't use BungeeCord");
}
}
//delay dependency setup because we load the plugin very early where plugins are initialized yet
getServer().getScheduler().runTask(this, new Runnable() {
@Override
public void run() {
boolean hookFound = registerHooks();
if (bungeeCord) {
getLogger().info("BungeeCord setting detected. No auth plugin is required");
} else if (!hookFound) {
getLogger().warning("No auth plugin were found by this plugin "
+ "(other plugins could hook into this after the intialization of this plugin)"
+ "and bungeecord is deactivated. "
+ "Either one or both of the checks have to pass in order to use this plugin");
}
}
});
getServer().getScheduler().runTask(this, new DelayedAuthHook(this));
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));
getCommand("import-auth").setExecutor(new ImportCommand(core));
}
@Override
public void onDisable() {
//clean up
session.clear();
if (core != null) {
core.close();
}
//remove old blacklists
for (Player player : getServer().getOnlinePlayers()) {
player.removeMetadata(getName(), this);
}
getServer().getOnlinePlayers().forEach(player -> player.removeMetadata(getName(), this));
}
if (storage != null) {
storage.close();
public BukkitCore getCore() {
return core;
}
public void sendBungeeActivateMessage(CommandSender sender, String target, boolean activate) {
if (sender instanceof Player) {
notifiyBungeeCord((Player) sender, target, activate);
} else {
Player firstPlayer = Iterables.getFirst(getServer().getOnlinePlayers(), null);
if (firstPlayer == null) {
getLogger().info("No player online to send a plugin message to the proxy");
return;
}
notifiyBungeeCord(firstPlayer, target, activate);
}
}
public String generateStringPassword(Player player) {
return passwordGenerator.getRandomPassword(player);
}
@Deprecated
public void setPasswordGenerator(PasswordGenerator passwordGenerator) {
this.passwordGenerator = passwordGenerator;
core.setPasswordGenerator(passwordGenerator);
}
/**
@@ -190,7 +149,7 @@ public class FastLoginBukkit extends JavaPlugin {
*
* @return a thread-safe session map
*/
public ConcurrentMap<String, PlayerSession> getSessions() {
public ConcurrentMap<String, BukkitLoginSession> getSessions() {
return session;
}
@@ -203,69 +162,20 @@ public class FastLoginBukkit extends JavaPlugin {
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
*/
@Deprecated
public BukkitAuthPlugin getAuthPlugin() {
if (authPlugin == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Logger.getLogger(FastLoginBukkit.class.getName()).log(Level.SEVERE, null, ex);
}
}
return authPlugin;
return (BukkitAuthPlugin) core.getAuthPluginHook();
}
@Deprecated
public void setAuthPluginHook(BukkitAuthPlugin authPlugin) {
this.authPlugin = 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 {
List<Class<? extends BukkitAuthPlugin>> supportedHooks = Lists.newArrayList(AuthMeHook.class
, CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class
, xAuthHook.class);
for (Class<? extends BukkitAuthPlugin> clazz : supportedHooks) {
String pluginName = clazz.getSimpleName().replace("Hook", "");
//uses only member classes which uses AuthPlugin interface (skip interfaces)
if (getServer().getPluginManager().getPlugin(pluginName) != null) {
//check only for enabled plugins. A single plugin could be disabled by plugin managers
authPluginHook = clazz.newInstance();
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
break;
}
}
} catch (InstantiationException | IllegalAccessException 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;
core.setAuthPluginHook(authPlugin);
}
public boolean isBungeeCord() {
@@ -287,4 +197,16 @@ public class FastLoginBukkit extends JavaPlugin {
this.serverStarted = true;
}
}
private void notifiyBungeeCord(Player sender, String target, boolean activate) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
if (activate) {
dataOutput.writeUTF("ON");
} else {
dataOutput.writeUTF("OFF");
}
dataOutput.writeUTF(target);
sender.sendPluginMessage(this, getName(), dataOutput.toByteArray());
}
}

View File

@@ -0,0 +1,83 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
public class MojangApiBukkit extends MojangApiConnector {
//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?";
public MojangApiBukkit(Logger logger, List<String> localAddresses, int rateLimit) {
super(logger, localAddresses, rateLimit);
}
@Override
public boolean hasJoinedServer(LoginSession session, String serverId) {
BukkitLoginSession playerSession = (BukkitLoginSession) session;
try {
String url = HAS_JOINED_URL + "username=" + playerSession.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");
playerSession.setUuid(FastLoginCore.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");
playerSession.setSkin(skinValue, signature);
}
return true;
}
} catch (Exception ex) {
//catch not only ioexceptions also parse and NPE on unexpected json format
logger.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;
}
@Override
protected String getUUIDFromJson(String json) {
boolean isArray = json.startsWith("[");
JSONObject mojangPlayer;
if (isArray) {
JSONArray array = (JSONArray) JSONValue.parse(json);
mojangPlayer = (JSONObject) array.get(0);
} else {
mojangPlayer = (JSONObject) JSONValue.parse(json);
}
String uuid = (String) mojangPlayer.get("id");
if ("null".equals(uuid)) {
return null;
}
return uuid;
}
}

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

@@ -2,7 +2,13 @@ package com.github.games647.fastlogin.bukkit;
import org.bukkit.entity.Player;
public interface PasswordGenerator {
/**
*
* @deprecated please use com.github.games647.fastlogin.core.hooks.PasswordGenerator<org.bukkit.entity.Player>
*/
@Deprecated
public interface PasswordGenerator extends com.github.games647.fastlogin.core.hooks.PasswordGenerator<Player> {
@Override
String getRandomPassword(Player player);
}

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,12 +1,9 @@
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 com.github.games647.fastlogin.core.PlayerProfile;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -14,7 +11,7 @@ import org.bukkit.entity.Player;
public class CrackedCommand implements CommandExecutor {
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
public CrackedCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
@@ -25,85 +22,70 @@ public class CrackedCommand implements CommandExecutor {
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");
sender.sendMessage(plugin.getCore().getMessage("no-console"));
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, sender.getName());
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
plugin.sendBungeeActivateMessage(sender, sender.getName(), false);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
//todo: load async if it's not in the cache anymore
final PlayerProfile profile = plugin.getStorage().getProfile(sender.getName(), true);
//todo: load async if
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players");
sender.sendMessage(plugin.getCore().getMessage("remove-premium"));
profile.setPremium(false);
profile.setUuid(null);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
plugin.getStorage().save(profile);
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
} else {
sender.sendMessage(ChatColor.DARK_RED + "You are not in the premium list");
sender.sendMessage(plugin.getCore().getMessage("not-premium"));
}
}
return true;
} else {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(ChatColor.DARK_RED + "Not enough permissions");
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
sender.sendMessage(ChatColor.YELLOW + "Sending request for player " + args[0] + "...");
} else {
//todo: load async if it's not in the cache anymore
final PlayerProfile profile = plugin.getStorage().getProfile(args[0], true);
if (profile == null) {
sender.sendMessage(ChatColor.DARK_RED + "Player not in the database");
return 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 + "Player is not in the premium list");
}
}
onCrackedOther(sender, command, args);
}
return true;
}
private void notifiyBungeeCord(CommandSender sender, String target) {
if (sender instanceof Player) {
notifiyBungeeCord(sender, target);
} else {
//todo: add console support
// Player firstPlayer = Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
// notifiyBungeeCord(firstPlayer, target);
private void onCrackedOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(plugin.getCore().getMessage("no-permission"));
return;
}
}
private void notifiyBungeeCord(Player sender, String target) {
if (plugin.isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("OFF");
dataOutput.writeUTF(target);
plugin.sendBungeeActivateMessage(sender, args[0], false);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage("Error occured");
return;
}
plugin.getLogger().info("No player online to send a plugin message to the proxy");
//existing player is already cracked
if (profile.getUserId() != -1 && !profile.isPremium()) {
sender.sendMessage(plugin.getCore().getMessage("not-premium-other"));
} else {
sender.sendMessage(plugin.getCore().getMessage("remove-premium"));
profile.setPremium(false);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
}
}
}
}

View File

@@ -0,0 +1,83 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.BukkitCore;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.importer.ImportPlugin;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
public class ImportCommand implements CommandExecutor {
private final BukkitCore core;
public ImportCommand(BukkitCore core) {
this.core = core;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 2) {
sender.sendMessage(ChatColor.DARK_RED + "You need to specify the import plugin and database type");
return true;
}
ImportPlugin importPlugin;
switch (args[0].toLowerCase()) {
case "autoin":
importPlugin = ImportPlugin.AUTO_IN;
break;
case "bpa":
importPlugin = ImportPlugin.BPA;
break;
case "eldzi":
importPlugin = ImportPlugin.ELDZI;
break;
default:
sender.sendMessage(ChatColor.DARK_RED + "Unknown auto login plugin");
return true;
}
boolean sqlite;
switch (args[1].toLowerCase()) {
case "sqlite":
sqlite = true;
break;
case "mysql":
sqlite = false;
break;
default:
sender.sendMessage(ChatColor.DARK_RED + "Unknown storage type to import from. Either SQLite or MySQL");
return true;
}
String host = "";
String database = "";
String username = "";
String password = "";
if (!sqlite) {
if (args.length <= 5) {
sender.sendMessage(ChatColor.DARK_RED + "If importing from MySQL, you need to specify host database "
+ "and username passowrd too");
return true;
}
host = args[2];
database = args[3];
username = args[4];
password = args[5];
}
AuthStorage storage = core.getStorage();
boolean success = core.importDatabase(importPlugin, true, storage, host, database, username, password);
if (success) {
sender.sendMessage(ChatColor.DARK_GREEN + "Successful imported the data");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Failed to import the data. Check out the logs");
}
return true;
}
}

View File

@@ -1,12 +1,11 @@
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 com.github.games647.fastlogin.core.PlayerProfile;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -19,7 +18,7 @@ import org.bukkit.entity.Player;
*/
public class PremiumCommand implements CommandExecutor {
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
public PremiumCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
@@ -30,87 +29,80 @@ public class PremiumCommand implements CommandExecutor {
if (args.length == 0) {
if (!(sender instanceof Player)) {
//console or command block
sender.sendMessage(ChatColor.DARK_RED + "Only players can add themselves as premium");
sender.sendMessage(plugin.getCore().getMessage("no-console"));
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, sender.getName());
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
plugin.sendBungeeActivateMessage(sender, sender.getName(), true);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
// //todo: load async if it's not in the cache anymore
final PlayerProfile profile = plugin.getStorage().getProfile(sender.getName(), true);
UUID id = ((Player) sender).getUniqueId();
if (plugin.getConfig().getBoolean("premium-warning")
&& !plugin.getCore().getPendingConfirms().contains(id)) {
sender.sendMessage(plugin.getCore().getMessage("premium-warning"));
plugin.getCore().getPendingConfirms().add(id);
return true;
}
plugin.getCore().getPendingConfirms().remove(id);
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list");
sender.sendMessage(plugin.getCore().getMessage("already-exists"));
} else {
//todo: resolve uuid
profile.setPremium(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
plugin.getStorage().save(profile);
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
sender.sendMessage(plugin.getCore().getMessage("add-premium"));
}
}
return true;
} else {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(ChatColor.DARK_RED + "Not enough permissions");
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
} else {
//todo: load async if it's not in the cache anymore
final PlayerProfile profile = plugin.getStorage().getProfile(args[0], true);
if (profile == null) {
sender.sendMessage(ChatColor.DARK_RED + "Player not in the database");
return true;
}
if (profile.isPremium()) {
sender.sendMessage(ChatColor.DARK_RED + "Player is 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");
}
}
onPremiumOther(sender, command, args);
}
return true;
}
private void notifiyBungeeCord(CommandSender sender, String target) {
if (sender instanceof Player) {
notifiyBungeeCord(sender, target);
} else {
//todo: add console support
// Player firstPlayer = Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
// notifiyBungeeCord(firstPlayer, target);
private void onPremiumOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(plugin.getCore().getMessage("no-permission"));
return ;
}
}
private void notifiyBungeeCord(Player sender, String target) {
if (plugin.isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("ON");
dataOutput.writeUTF(target);
plugin.sendBungeeActivateMessage(sender, args[0], true);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage(plugin.getCore().getMessage("player-unknown"));
return;
}
if (profile.isPremium()) {
sender.sendMessage(plugin.getCore().getMessage("already-exists-other"));
} else {
//todo: resolve uuid
profile.setPremium(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
sender.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
sender.sendMessage(plugin.getCore().getMessage("add-premium-other"));
}
}
}
}

View File

@@ -1,6 +1,7 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import fr.xephi.authme.api.API;
import fr.xephi.authme.api.NewAPI;
@@ -14,7 +15,7 @@ import org.bukkit.entity.Player;
* Bukkit: http://dev.bukkit.org/bukkit-plugins/authme-reloaded/
* Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
*/
public class AuthMeHook implements BukkitAuthPlugin {
public class AuthMeHook implements AuthPlugin<Player> {
private final boolean isNewAPIAvailable;
@@ -23,6 +24,7 @@ public class AuthMeHook implements BukkitAuthPlugin {
}
@Override
@SuppressWarnings("deprecation")
public boolean forceLogin(Player player) {
//skips registration and login
if (isNewAPIAvailable) {
@@ -30,13 +32,12 @@ public class AuthMeHook implements BukkitAuthPlugin {
} else {
API.forceLogin(player);
}
//commented because the operation above is performed async -> race conditions
// return NewAPI.getInstance().isAuthenticated(player);
return true;
}
@Override
@SuppressWarnings("deprecation")
public boolean isRegistered(String playerName) throws Exception {
if (isNewAPIAvailable) {
return NewAPI.getInstance().isRegistered(playerName);
@@ -46,6 +47,7 @@ public class AuthMeHook implements BukkitAuthPlugin {
}
@Override
@SuppressWarnings("deprecation")
public boolean forceRegister(Player player, String password) {
if (isNewAPIAvailable) {
NewAPI.getInstance().forceRegister(player, password);

View File

@@ -1,60 +1,21 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import org.bukkit.entity.Player;
/**
* Represents a supporting authentication plugin in Bukkit/Spigot/... servers
* @deprecated please use com.github.games647.fastlogin.core.hooks.AuthPlugin<org.bukkit.entity.Player>
*/
public interface BukkitAuthPlugin {
@Deprecated
public interface BukkitAuthPlugin extends AuthPlugin<Player> {
/**
* 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
*/
@Override
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
*/
@Override
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
*/
@Override
boolean forceRegister(Player player, String password);
}

View File

@@ -1,18 +1,18 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
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.apache.commons.lang.reflect.FieldUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -23,47 +23,43 @@ import org.bukkit.entity.Player;
*
* Bukkit: http://dev.bukkit.org/server-mods/crazylogin/
*/
public class CrazyLoginHook implements BukkitAuthPlugin {
public class CrazyLoginHook implements AuthPlugin<Player> {
protected final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
private final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
private final PlayerListener playerListener = getListener();
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(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);
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin, () -> {
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player);
if (playerData != null) {
//mark the account as logged in
playerData.setLoggedIn(true);
String ip = player.getAddress().getAddress().getHostAddress();
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);
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;
if (playerListener != null) {
playerListener.removeMovementBlocker(player);
playerListener.disableHidenInventory(player);
playerListener.disableSaveLogin(player);
playerListener.unhidePlayer(player);
}
return null;
//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 {
@@ -87,7 +83,7 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
}
@Override
public boolean forceRegister(final Player player, String password) {
public boolean forceRegister(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
@@ -108,8 +104,8 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
private PlayerListener getListener() {
PlayerListener listener;
try {
listener = FuzzyReflection.getFieldValue(crazyLoginPlugin, PlayerListener.class, true);
} catch (Exception ex) {
listener = (PlayerListener) FieldUtils.readField(crazyLoginPlugin, "playerListener", true);
} catch (IllegalAccessException ex) {
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to get the listener instance for auto login", ex);
listener = null;
}

View File

@@ -1,5 +1,7 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import io.github.lucaseasedup.logit.CancelledState;
import io.github.lucaseasedup.logit.LogItCore;
import io.github.lucaseasedup.logit.account.Account;
@@ -13,7 +15,7 @@ import org.bukkit.entity.Player;
* Bukkit: Unknown
* Spigot: Unknown
*/
public class LogItHook implements BukkitAuthPlugin {
public class LogItHook implements AuthPlugin<Player> {
@Override
public boolean forceLogin(Player player) {

View File

@@ -1,92 +1,41 @@
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 com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.lenis0012.bukkit.loginsecurity.LoginSecurity;
import com.lenis0012.bukkit.loginsecurity.session.AuthService;
import com.lenis0012.bukkit.loginsecurity.session.PlayerSession;
import com.lenis0012.bukkit.loginsecurity.session.action.LoginAction;
import com.lenis0012.bukkit.loginsecurity.session.action.RegisterAction;
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
* Bukkit: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
* Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
*/
public class LoginSecurityHook implements BukkitAuthPlugin {
public class LoginSecurityHook implements AuthPlugin<Player> {
protected final LoginSecurity securityPlugin = LoginSecurity.instance;
private final FastLoginBukkit plugin = (FastLoginBukkit) Bukkit.getPluginManager().getPlugin("FastLogin");
@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;
}
public boolean forceLogin(Player player) {
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
return session.performAction(new LoginAction(AuthService.PLUGIN, plugin)).isSuccess();
}
@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)) {
PlayerSession session = LoginSecurity.getSessionManager().getOfflineSession(playerName);
return session.isRegistered();
}
@Override
public boolean forceRegister(final Player player, final String password) {
DataManager dataManager = securityPlugin.data;
UUID playerUUID = player.getUniqueId();
String uuidString = playerUUID.toString().replace("-", "");
InetAddress ipAddress = player.getAddress().getAddress();
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;
public boolean forceRegister(Player player, String password) {
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
return session.performAction(new RegisterAction(AuthService.PLUGIN, plugin, password)).isSuccess();
}
}

View File

@@ -1,6 +1,7 @@
package com.github.games647.fastlogin.bukkit.hooks;
import java.util.concurrent.Callable;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
@@ -18,24 +19,20 @@ import org.royaldev.royalauth.RoyalAuth;
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/royalauth/
*/
public class RoyalAuthHook implements BukkitAuthPlugin {
public class RoyalAuthHook implements AuthPlugin<Player> {
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);
public boolean forceLogin(Player player) {
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(royalAuthPlugin, () -> {
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdLogin.java#L62
//not thread-safe
authPlayer.login();
//not thread-safe
authPlayer.login();
return authPlayer.isLoggedIn();
}
return authPlayer.isLoggedIn();
});
try {
@@ -58,11 +55,8 @@ public class RoyalAuthHook implements BukkitAuthPlugin {
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;
//login in the player after registration
return registerSuccess && forceLogin(player);
}
}

View File

@@ -1,12 +1,12 @@
package com.github.games647.fastlogin.bukkit.hooks;
import java.util.concurrent.Callable;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
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;
@@ -20,19 +20,16 @@ import ultraauth.managers.PlayerManager;
* Bukkit: http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
* Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
*/
public class UltraAuthHook implements BukkitAuthPlugin {
public class UltraAuthHook implements AuthPlugin<Player> {
protected final Plugin ultraAuthPlugin = Main.main;
private final Plugin ultraAuthPlugin = Main.main;
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(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);
}
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, () -> {
UltraAuthAPI.authenticatedPlayer(player);
return UltraAuthAPI.isAuthenticated(player);
});
try {
@@ -45,7 +42,7 @@ public class UltraAuthHook implements BukkitAuthPlugin {
@Override
public boolean isRegistered(String playerName) throws Exception {
return UltraAuthAPI.isRegisterd(new FakePlayer(playerName));
return UltraAuthAPI.isRegisterd(playerName);
}
@Override

View File

@@ -1,9 +1,10 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
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;
@@ -19,27 +20,24 @@ import org.bukkit.entity.Player;
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/xauth/
*/
public class xAuthHook implements BukkitAuthPlugin {
public class xAuthHook implements AuthPlugin<Player> {
protected final xAuth xAuthPlugin = xAuth.getPlugin();
private final xAuth xAuthPlugin = xAuth.getPlugin();
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(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);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, () -> {
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;
//unprotect the inventory, op status...
return xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
}
return false;
});
try {
@@ -58,33 +56,25 @@ public class xAuthHook implements BukkitAuthPlugin {
}
@Override
public boolean forceRegister(final Player player, final String password) {
public boolean forceRegister(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);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, () -> {
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);
return registerSuccess;
}
return false;
}
});
try {
boolean success = future.get();
if (success) {
//login in the player after registration
return forceLogin(player);
return registerSuccess;
}
return false;
});
try {
//login in the player after registration
return future.get() && forceLogin(player);
} catch (InterruptedException | ExecutionException ex) {
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
return false;

View File

@@ -1,10 +1,7 @@
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 com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -24,7 +21,7 @@ public class BukkitJoinListener implements Listener {
private static final long DELAY_LOGIN = 20L / 2;
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
public BukkitJoinListener(FastLoginBukkit plugin) {
this.plugin = plugin;
@@ -33,7 +30,7 @@ public class BukkitJoinListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
if (loginEvent.getResult() == Result.ALLOWED && !plugin.isServerFullyStarted()) {
loginEvent.disallow(Result.KICK_OTHER, "§cServer is not fully started yet. Please retry");
loginEvent.disallow(Result.KICK_OTHER, plugin.getCore().getMessage("not-started"));
}
}
@@ -41,16 +38,6 @@ public class BukkitJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
//removing the session because we now use it
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);
@@ -60,8 +47,7 @@ public class BukkitJoinListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
Player player = quitEvent.getPlayer();
//prevent memory leaks
player.removeMetadata(plugin.getName(), plugin);
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -1,18 +1,21 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
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.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
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.net.InetSocketAddress;
import java.nio.file.Files;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -29,13 +32,13 @@ public class BungeeCordListener implements PluginMessageListener {
private static final String FILE_NAME = "proxy-whitelist.txt";
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
//null if whitelist is empty so bungeecord support is disabled
private final UUID proxyId;
private final Set<UUID> proxyIds;
public BungeeCordListener(FastLoginBukkit plugin) {
this.plugin = plugin;
this.proxyId = loadBungeeCordId();
this.proxyIds = loadBungeeCordIds();
}
@Override
@@ -49,10 +52,10 @@ public class BungeeCordListener implements PluginMessageListener {
plugin.getLogger().log(Level.FINEST, "Received plugin message for subchannel {0} from {1}"
, new Object[]{subchannel, player});
final String playerName = dataInput.readUTF();
String playerName = dataInput.readUTF();
//check if the player is still online or disconnected
final Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
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
@@ -65,51 +68,47 @@ public class BungeeCordListener implements PluginMessageListener {
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));
if (proxyIds.contains(sourceId)) {
readMessage(checkedPlayer, subchannel, playerName, player);
}
}
}
public UUID loadBungeeCordId() {
private void readMessage(Player checkedPlayer, String subchannel, String playerName, Player player) {
InetSocketAddress address = checkedPlayer.getAddress();
String id = '/' + address.getAddress().getHostAddress() + ':' + address.getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, true);
playerSession.setVerified(true);
plugin.getSessions().put(id, playerSession);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin == null || !authPlugin.isRegistered(playerName)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, false);
playerSession.setVerified(true);
plugin.getSessions().put(id, playerSession);
new ForceLoginTask(plugin, player).run();
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
});
}
}
public Set<UUID> loadBungeeCordIds() {
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());
}
List<String> lines = Files.readAllLines(whitelistFile.toPath());
return lines.stream().map(String::trim).map(UUID::fromString).collect(Collectors.toSet());
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
} catch (Exception ex) {

View File

@@ -1,229 +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.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 java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.UUID;
import java.util.logging.Level;
import javax.crypto.SecretKey;
import org.bukkit.entity.Player;
/**
* 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
*
* sharedSecret=encrypted byte array
* verify token=encrypted byte array
*/
public class EncryptionPacketListener extends PacketAdapter {
private final ProtocolManager protocolManager;
//hides the inherit Plugin plugin field, but we need this type
private final FastLoginBukkit plugin;
public EncryptionPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we make api calls to Mojang
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
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
*
* Minecraft Server implementation
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
System.out.println("ENCRYPTION REQUEST");
Player player = packetEvent.getPlayer();
//the player name is unknown to ProtocolLib (so getName() doesn't work) - now uses ip:port as key
String uniqueSessionKey = player.getAddress().toString();
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
if (session == null) {
disconnect(packetEvent, "Invalid request", Level.FINE
, "Player {0} tried to send encryption response at invalid 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);
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
//check if the verify token are equal to the server sent one
disconnect(packetEvent, "Invalid token", Level.FINE
, "Player {0} ({1}) tried to login with an invalid verify token. "
+ "Server: {2} Client: {3}"
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
}
return true;
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager(Player player)
throws IllegalAccessException, NoSuchFieldException {
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
Object rawInjector = injectorField.get(socketInjector);
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
injectorField.setAccessible(true);
return injectorField.get(rawInjector);
}
private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent)
throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager(player);
//try to detect the method by parameters
Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager)
.getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptConnectionMethod.invoke(networkManager, loginKey);
} catch (ReflectiveOperationException ex) {
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
return false;
}
return true;
}
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
, Object... arguments) {
plugin.getLogger().log(logLevel, logMessage, arguments);
kickPlayer(packetEvent.getPlayer(), kickReason);
//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));
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);
}
}
//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);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
protocolManager.recieveClientPacket(from, startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
//cancel the event in order to prevent the server receiving an invalid packet
kickPlayer(from, "Error occured");
}
}
}

View File

@@ -1,89 +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) {
plugin.setServerStarted();
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);
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
if (authPlugin == null) {
return;
}
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
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,156 +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) {
plugin.setServerStarted();
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});
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
if (authPlugin == null) {
return;
}
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
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

@@ -0,0 +1,65 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* 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
*
* sharedSecret=encrypted byte array
* verify token=encrypted byte array
*/
public class EncryptionPacketListener extends PacketAdapter {
//hides the inherit Plugin plugin field, but we need this type
private final FastLoginBukkit plugin;
public EncryptionPacketListener(FastLoginBukkit plugin) {
//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());
this.plugin = plugin;
}
/**
* 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
*
* Minecraft Server implementation
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
if (packetEvent.isCancelled()) {
return;
}
Player sender = packetEvent.getPlayer();
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
packetEvent.getAsyncMarker().incrementProcessingDelay();
VerifyResponseTask verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret);
Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask);
}
}

View File

@@ -0,0 +1,74 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.reflect.MethodUtils;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
public class LoginSkinApplyListener implements Listener {
private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
private static final MethodAccessor GET_PROPERTIES = Accessors.getMethodAccessor(GAME_PROFILE, "getProperties");
private final FastLoginBukkit plugin;
public LoginSkinApplyListener(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOW)
//run this on the loginEvent to let skins plugins see the skin like in normal minecraft behaviour
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
if (loginEvent.getResult() != Result.ALLOWED) {
return;
}
Player player = loginEvent.getPlayer();
if (plugin.getConfig().getBoolean("forwardSkin")) {
//go through every session, because player.getAddress is null
//loginEvent.getAddress is just a InetAddress not InetSocketAddres, so not unique enough
for (BukkitLoginSession session : plugin.getSessions().values()) {
if (session.getUsername().equals(player.getName())) {
String signature = session.getSkinSignature();
String skinData = session.getEncodedSkinData();
applySkin(player, skinData, signature);
break;
}
}
}
}
private void applySkin(Player player, String skinData, String signature) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
if (skinData != null && signature != null) {
WrappedSignedProperty skin = WrappedSignedProperty.fromValues("textures", skinData, signature);
try {
gameProfile.getProperties().put("textures", skin);
} catch (ClassCastException castException) {
Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
try {
MethodUtils.invokeMethod(map, "put", new Object[]{"textures", skin.getHandle()});
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium skin", ex);
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import java.util.Random;
import java.util.logging.Level;
import org.bukkit.entity.Player;
public class NameCheckTask extends JoinManagement<Player, ProtocolLibLoginSource> implements Runnable {
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final Random random;
private final Player player;
private final String username;
public NameCheckTask(FastLoginBukkit plugin, PacketEvent packetEvent, Random random, Player player, String username) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
this.packetEvent = packetEvent;
this.random = random;
this.player = player;
this.username = username;
}
@Override
public void run() {
try {
super.onLogin(username, new ProtocolLibLoginSource(plugin, packetEvent, player, random));
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
@Override
public void requestPremiumLogin(ProtocolLibLoginSource source, PlayerProfile profile, String username, boolean registered) {
try {
source.setOnlineMode();
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to cracked login", ex);
return;
}
String ip = player.getAddress().getAddress().getHostAddress();
core.getPendingLogins().put(ip + username, new Object());
String serverId = source.getServerId();
byte[] verify = source.getVerifyToken();
BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId, verify, registered, profile);
plugin.getSessions().put(player.getAddress().toString(), playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
}
@Override
public void startCrackedSession(ProtocolLibLoginSource source, PlayerProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(player.getAddress().toString(), loginSession);
}
}

View File

@@ -0,0 +1,99 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.util.Random;
import org.bukkit.entity.Player;
public class ProtocolLibLoginSource implements LoginSource {
private static final int VERIFY_TOKEN_LENGTH = 4;
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final Player player;
private final Random random;
private String serverId;
private final byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
public ProtocolLibLoginSource(FastLoginBukkit plugin, PacketEvent packetEvent, Player player, Random random) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.player = player;
this.random = random;
}
@Override
public void setOnlineMode() throws Exception {
//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/
serverId = Long.toString(random.nextLong(), 16);
//generate a random token which should be the same when we receive it from the client
random.nextBytes(verifyToken);
sentEncryptionRequest();
}
@Override
public void kick(String message) throws Exception {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(message));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
protocolManager.sendServerPacket(player, kickPacket);
} finally {
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
}
}
@Override
public InetSocketAddress getAddress() {
return packetEvent.getPlayer().getAddress();
}
private void sentEncryptionRequest() throws InvocationTargetException {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
/**
* 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);
}
public String getServerId() {
return serverId;
}
public byte[] getVerifyToken() {
return verifyToken;
}
}

View File

@@ -0,0 +1,77 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
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 java.util.Random;
import java.util.logging.Level;
import org.bukkit.Bukkit;
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 {
//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) {
//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;
}
/**
* 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) {
if (packetEvent.isCancelled()
|| plugin.getCore().getAuthPluginHook()== null || !plugin.isServerFullyStarted()) {
return;
}
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", new Object[]{sessionKey, username});
packetEvent.getAsyncMarker().incrementProcessingDelay();
NameCheckTask nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username);
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
}
}

View File

@@ -0,0 +1,206 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.netty.Injector;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.UUID;
import java.util.logging.Level;
import javax.crypto.SecretKey;
import org.bukkit.entity.Player;
public class VerifyResponseTask implements Runnable {
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final Player fromPlayer;
private final byte[] sharedSecret;
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player fromPlayer, byte[] sharedSecret) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.fromPlayer = fromPlayer;
this.sharedSecret = sharedSecret;
}
@Override
public void run() {
try {
BukkitLoginSession session = plugin.getSessions().get(fromPlayer.getAddress().toString());
if (session == null) {
disconnect(plugin.getCore().getMessage("invalid-requst"), true
, "Player {0} tried to send encryption response at invalid state", fromPlayer.getAddress());
} else {
String ip = fromPlayer.getAddress().getAddress().getHostAddress();
plugin.getCore().getPendingLogins().remove(ip + session.getUsername());
verifyResponse(session);
}
} finally {
//this is a fake packet; it shouldn't be send to the server
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
private void verifyResponse(BukkitLoginSession session) {
PrivateKey privateKey = plugin.getServerKey().getPrivate();
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) {
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.getCore().getApiConnector().hasJoinedServer(session, serverId)) {
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), fromPlayer.getAddress(), serverId);
}
}
private void setPremiumUUID(UUID premiumUUID) {
if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) {
try {
Object networkManager = getNetworkManager();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
FieldUtils.writeField(networkManager, "spoofedUUID", premiumUUID, true);
} catch (Exception exc) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", exc);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey) {
byte[] requestVerify = session.getVerifyToken();
//encrypted verify token
byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1);
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
//check if the verify token are equal to the server sent one
disconnect(plugin.getCore().getMessage("invalid-verify-token"), true
, "Player {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}"
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
}
return true;
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager() throws IllegalAccessException, NoSuchFieldException {
Object injectorContainer = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer);
//ChannelInjector
Injector rawInjector = FuzzyReflection.getFieldValue(injectorContainer, Injector.class, true);
return FieldUtils.readField(rawInjector, "networkManager", true);
}
private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager();
//try to detect the method by parameters
Method encryptMethod = FuzzyReflection
.fromObject(networkManager).getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptMethod.invoke(networkManager, loginKey);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Couldn't enable encryption", ex);
disconnect(plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption");
return false;
}
return true;
}
private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) {
if (debug) {
plugin.getLogger().log(Level.FINE, logMessage, arguments);
} else {
plugin.getLogger().log(Level.SEVERE, logMessage, arguments);
}
kickPlayer(packetEvent.getPlayer(), kickReason);
}
private void kickPlayer(Player player, String reason) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
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);
}
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
//see StartPacketListener for packet information
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
protocolManager.recieveClientPacket(fromPlayer, 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(fromPlayer, plugin.getCore().getMessage("error-kick"));
}
}
}

View File

@@ -0,0 +1,35 @@
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.net.InetSocketAddress;
import protocolsupport.api.events.PlayerLoginStartEvent;
public class ProtocolLoginSource implements LoginSource {
private final PlayerLoginStartEvent loginStartEvent;
public ProtocolLoginSource(PlayerLoginStartEvent loginStartEvent) {
this.loginStartEvent = loginStartEvent;
}
@Override
public void setOnlineMode() {
loginStartEvent.setOnlineMode(true);
}
@Override
public void kick(String message) {
loginStartEvent.denyLogin(message);
}
@Override
public InetSocketAddress getAddress() {
return loginStartEvent.getAddress();
}
public PlayerLoginStartEvent getLoginStartEvent() {
return loginStartEvent;
}
}

View File

@@ -0,0 +1,76 @@
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.net.InetSocketAddress;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import protocolsupport.api.events.PlayerLoginStartEvent;
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
public class ProtocolSupportListener extends JoinManagement<Player, ProtocolLoginSource> implements Listener {
private final FastLoginBukkit plugin;
public ProtocolSupportListener(FastLoginBukkit plugin) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
}
@EventHandler
public void onLoginStart(PlayerLoginStartEvent loginStartEvent) {
plugin.setServerStarted();
if (loginStartEvent.isLoginDenied() || plugin.getCore().getAuthPluginHook() == null) {
return;
}
String username = loginStartEvent.getName();
InetSocketAddress address = loginStartEvent.getAddress();
//remove old data every time on a new login in order to keep the session only for one person
plugin.getSessions().remove(address.toString());
super.onLogin(username, new ProtocolLoginSource(loginStartEvent));
}
@EventHandler
public void onPropertiesResolve(PlayerPropertiesResolveEvent propertiesResolveEvent) {
InetSocketAddress address = propertiesResolveEvent.getAddress();
BukkitLoginSession session = plugin.getSessions().get(address.toString());
//skin was resolved -> premium player
if (propertiesResolveEvent.hasProperty("textures") && session != null) {
String ip = address.getAddress().getHostAddress();
plugin.getCore().getPendingLogins().remove(ip + session.getUsername());
session.setVerified(true);
}
}
@Override
public void requestPremiumLogin(ProtocolLoginSource source, PlayerProfile profile, String username, boolean registered) {
source.setOnlineMode();
String ip = source.getAddress().getAddress().getHostAddress();
plugin.getCore().getPendingLogins().put(ip + username, new Object());
BukkitLoginSession playerSession = new BukkitLoginSession(username, null, null, registered, profile);
plugin.getSessions().put(source.getAddress().toString(), playerSession);
if (plugin.getConfig().getBoolean("premiumUuid")) {
source.getLoginStartEvent().setUseOnlineModeUUID(true);
}
}
@Override
public void startCrackedSession(ProtocolLoginSource source, PlayerProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(source.getAddress().toString(), loginSession);
}
}

View File

@@ -0,0 +1,75 @@
package com.github.games647.fastlogin.bukkit.tasks;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.hooks.AuthMeHook;
import com.github.games647.fastlogin.bukkit.hooks.CrazyLoginHook;
import com.github.games647.fastlogin.bukkit.hooks.LogItHook;
import com.github.games647.fastlogin.bukkit.hooks.LoginSecurityHook;
import com.github.games647.fastlogin.bukkit.hooks.UltraAuthHook;
import com.github.games647.fastlogin.bukkit.hooks.xAuthHook;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class DelayedAuthHook implements Runnable {
private final FastLoginBukkit plugin;
public DelayedAuthHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public void run() {
boolean hookFound = registerHooks();
if (plugin.isBungeeCord()) {
plugin.getLogger().info("BungeeCord setting detected. No auth plugin is required");
} else if (!hookFound) {
plugin.getLogger().warning("No auth plugin were found by this plugin "
+ "(other plugins could hook into this after the intialization of this plugin)"
+ "and bungeecord is deactivated. "
+ "Either one or both of the checks have to pass in order to use this plugin");
}
}
private boolean registerHooks() {
AuthPlugin<Player> authPluginHook = null;
try {
@SuppressWarnings("unchecked")
List<Class<? extends AuthPlugin<Player>>> supportedHooks = Lists.newArrayList(AuthMeHook.class
, CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class
, xAuthHook.class);
for (Class<? extends AuthPlugin<Player>> clazz : supportedHooks) {
String pluginName = clazz.getSimpleName().replace("Hook", "");
//uses only member classes which uses AuthPlugin interface (skip interfaces)
if (Bukkit.getServer().getPluginManager().getPlugin(pluginName) != null) {
//check only for enabled plugins. A single plugin could be disabled by plugin managers
authPluginHook = clazz.newInstance();
plugin.getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
break;
}
}
} catch (InstantiationException | IllegalAccessException ex) {
plugin.getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
}
if (authPluginHook == null) {
//run this check for exceptions (errors) and not found plugins
plugin.getLogger().warning("No support offline Auth plugin found. ");
return false;
}
if (plugin.getCore().getAuthPluginHook() == null) {
plugin.getCore().setAuthPluginHook(authPluginHook);
plugin.setServerStarted();
}
return true;
}
}

View File

@@ -1,22 +1,24 @@
package com.github.games647.fastlogin.bukkit;
package com.github.games647.fastlogin.bukkit.tasks;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
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;
private final FastLoginBukkit plugin;
private final Player player;
public ForceLoginTask(FastLoginBukkit plugin, Player player) {
this.plugin = plugin;
@@ -31,36 +33,32 @@ public class ForceLoginTask implements Runnable {
//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);
BukkitLoginSession session = plugin.getSessions().remove(id);
if (session == null) {
return;
}
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())) {
AuthStorage storage = plugin.getCore().getStorage();
PlayerProfile playerProfile = session.getProfile();
//check if it's the same player as we checked before
if (session.isVerified() && player.getName().equals(session.getUsername())) {
//premium player
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
if (authPlugin == null) {
//maybe only bungeecord plugin
sendSuccessNotification();
} else {
boolean success = false;
if (isOnlineThreadSafe() && session.isVerified()) {
if (session.needsRegistration()) {
success = forceRegister(authPlugin, player);
if (isOnlineThreadSafe()) {
if (plugin.getConfig().getBoolean("autoLogin")) {
if (session.needsRegistration()) {
success = forceRegister(authPlugin, player);
} else {
success = forceLogin(authPlugin, player);
}
} else {
success = forceLogin(authPlugin, player);
success = true;
}
}
@@ -69,30 +67,46 @@ public class ForceLoginTask implements Runnable {
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
//save cracked players too
playerProfile.setPremium(session.isVerified());
playerProfile.setPremium(true);
storage.save(playerProfile);
}
sendSuccessNotification();
}
}
} else {
//cracked player
if (playerProfile != null) {
playerProfile.setUuid(null);
playerProfile.setPremium(false);
storage.save(playerProfile);
}
}
}
private boolean forceRegister(BukkitAuthPlugin authPlugin, Player player) {
private boolean forceRegister(AuthPlugin<Player> authPlugin, Player player) {
plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName());
String generatedPassword = plugin.generateStringPassword(player);
String generatedPassword = plugin.getCore().getPasswordGenerator().getRandomPassword(player);
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?");
String message = plugin.getCore().getMessage("auto-register");
if (success && message != null) {
message = message.replace("%password", generatedPassword);
player.sendMessage(message);
}
return success;
}
private boolean forceLogin(BukkitAuthPlugin authPlugin, Player player) {
private boolean forceLogin(AuthPlugin<Player> 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");
String message = plugin.getCore().getMessage("auto-login");
if (success && message != null) {
player.sendMessage(message);
}
return success;
}
@@ -107,12 +121,7 @@ public class ForceLoginTask implements Runnable {
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();
}
});
Future<Boolean> onlineFuture = Bukkit.getScheduler().callSyncMethod(plugin, player::isOnline);
try {
return onlineFuture.get();

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

@@ -14,19 +14,10 @@ dev-url: ${project.url}
# Load the plugin as early as possible to inject it for all players
load: STARTUP
# Without Protocollib the plugin does not work at all
depend: [ProtocolLib]
softdepend:
# We depend either ProtocolLib or ProtocolSupport
- ProtocolSupport
# Auth plugins
# - xAuth
# - AuthMe
# - LogIt
# - CrazyLogin
# - LoginSecurity
# - RoyalAuth
# - UltraAuth
- ProtocolLib
commands:
${project.parent.name}:
@@ -39,7 +30,12 @@ commands:
description: 'Label the invoker or the player specified as cracked if he was marked premium before'
aliases: [unpremium]
usage: /<command> [player]
permission: ${project.artifactId}.command.unpremium
permission: ${project.artifactId}.command.cracked
import-auth:
description: 'Imports the auth data from another auto login'
usage: /<command> [player]
permission: ${project.artifactId}.command.import
permissions:
${project.artifactId}.command.premium:
@@ -55,7 +51,7 @@ permissions:
description: 'Label themselves as cracked'
default: true
${project.artifactId}.command..cracked.other:
${project.artifactId}.command.cracked.other:
description: 'Label others as cracked'
children:
${project.artifactId}.command.cracked: true

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.3</version>
<version>1.9</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -17,44 +17,37 @@
<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>
<id>myplayplanet-REPO</id>
<url>http://maven.myplayplanet.net/</url>
</repository>
<!--Github automatic maven builds-->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<id>vik1395-repo</id>
<url>http://repo.vik1395.me/repositories</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.9-SNAPSHOT</version>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</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>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.8-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.vik1395</groupId>
<artifactId>BungeeAuth</artifactId>
<version>-1.2.1-gc367d92-8</version>
<version>1.3.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,87 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.scheduler.GroupedThreadFactory;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
public class BungeeCore extends FastLoginCore<ProxiedPlayer> {
private static Map<String, Object> generateConfigMap(Configuration config) {
return config.getKeys().stream()
.filter(key -> config.get(key) != null)
.collect(Collectors.toMap(key -> key, config::get));
}
private final FastLoginBungee plugin;
public BungeeCore(FastLoginBungee plugin, Configuration config) {
super(generateConfigMap(config));
this.plugin = plugin;
}
@Override
public File getDataFolder() {
return plugin.getDataFolder();
}
@Override
public Logger getLogger() {
return plugin.getLogger();
}
@Override
@SuppressWarnings("deprecation")
public ThreadFactory getThreadFactory() {
String pluginName = plugin.getDescription().getName();
return new ThreadFactoryBuilder()
.setNameFormat(pluginName + " Database Pool Thread #%1$d")
//Hikari create daemons by default
.setDaemon(true)
.setThreadFactory(new GroupedThreadFactory(plugin, pluginName))
.build();
}
@Override
public void loadMessages() {
try {
plugin.saveDefaultFile("messages.yml");
ConfigurationProvider configProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);
Configuration defaults = configProvider.load(getClass().getResourceAsStream("/messages.yml"));
File messageFile = new File(getDataFolder(), "messages.yml");
Configuration messageConfig = configProvider.load(messageFile, defaults);
messageConfig.getKeys().forEach(key -> {
String message = ChatColor.translateAlternateColorCodes('&', messageConfig.getString(key));
if (!message.isEmpty()) {
localeMessages.put(key, message);
}
});
} catch (IOException ex) {
getLogger().log(Level.SEVERE, "Failed to load messages", ex);
}
}
@Override
public MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests) {
return new MojangApiBungee(logger, addresses, requests);
}
}

View File

@@ -0,0 +1,34 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
public class BungeeLoginSession extends LoginSession {
private boolean alreadySaved;
private boolean alreadyLogged;
public BungeeLoginSession(String username, boolean registered, PlayerProfile profile) {
super(username, registered, profile);
}
public void setRegistered(boolean registered) {
this.registered = registered;
}
public boolean isAlreadySaved() {
return alreadySaved;
}
public void setAlreadySaved(boolean alreadySaved) {
this.alreadySaved = alreadySaved;
}
public boolean isAlreadyLogged() {
return alreadyLogged;
}
public void setAlreadyLogged(boolean alreadyLogged) {
this.alreadyLogged = alreadyLogged;
}
}

View File

@@ -0,0 +1,36 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.net.InetSocketAddress;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
public class BungeeLoginSource implements LoginSource {
private final PendingConnection connection;
public BungeeLoginSource(PendingConnection connection) {
this.connection = connection;
}
@Override
public void setOnlineMode() {
connection.setOnlineMode(true);
}
@Override
public void kick(String message) {
connection.disconnect(TextComponent.fromLegacyText(message));
}
@Override
public InetSocketAddress getAddress() {
return connection.getAddress();
}
public PendingConnection getConnection() {
return connection;
}
}

View File

@@ -1,22 +1,18 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.listener.PlayerConnectionListener;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthHook;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.github.games647.fastlogin.bungee.listener.PlayerConnectionListener;
import com.github.games647.fastlogin.bungee.listener.PluginMessageListener;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
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;
@@ -28,103 +24,90 @@ import net.md_5.bungee.config.YamlConfiguration;
*/
public class FastLoginBungee extends Plugin {
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = Maps.newConcurrentMap();
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();
private BungeeCore core;
private Configuration config;
@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);
}
}
saveDefaultFile("config.yml");
try {
configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
File configFile = new File(getDataFolder(), "config.yml");
ConfigurationProvider provider = ConfigurationProvider.getProvider(YamlConfiguration.class);
Configuration defaults = provider.load(getResourceAsStream("config.yml"));
config = provider.load(configFile, defaults);
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);
core = new BungeeCore(this, config);
if (!core.setupDatabase()) {
return;
}
} catch (IOException ioExc) {
getLogger().log(Level.SEVERE, "Error loading config. Disabling plugin...", ioExc);
return;
}
core.setApiConnector();
core.loadMessages();
//events
getProxy().getPluginManager().registerListener(this, new PlayerConnectionListener(this));
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
//bungee only commands
getProxy().getPluginManager().registerCommand(this, new ImportCommand(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();
if (core != null) {
core.close();
}
}
public Configuration getConfiguration() {
return configuration;
public void saveDefaultFile(String fileName) {
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
File configFile = new File(getDataFolder(), fileName);
if (!configFile.exists()) {
InputStream in = getResourceAsStream(fileName);
try {
Files.copy(in, configFile.toPath());
} catch (IOException ioExc) {
getLogger().log(Level.SEVERE, "Error saving default " + fileName, ioExc);
} finally {
try {
in.close();
} catch (IOException ex) {
getLogger().log(Level.SEVERE, null, ex);
}
}
}
}
public Storage getStorage() {
return storage;
public BungeeCore getCore() {
return core;
}
public MojangApiConnector getMojangApiConnector() {
return mojangApiConnector;
@Deprecated
public void setAuthPluginHook(BungeeAuthPlugin authPlugin) {
core.setAuthPluginHook(authPlugin);
}
public ConcurrentMap<PendingConnection, Object> getPendingAutoRegister() {
return pendingAutoRegister;
public Configuration getConfig() {
return config;
}
public ConcurrentMap<PendingConnection, BungeeLoginSession> getSession() {
return session;
}
/**
@@ -132,14 +115,15 @@ public class FastLoginBungee extends Plugin {
*
* @return the auth hook for BungeeCord. null if none found
*/
@Deprecated
public BungeeAuthPlugin getBungeeAuthPlugin() {
return bungeeAuthPlugin;
return (BungeeAuthPlugin) core.getAuthPluginHook();
}
private void registerHook() {
Plugin plugin = getProxy().getPluginManager().getPlugin("BungeeAuth");
if (plugin != null) {
bungeeAuthPlugin = new BungeeAuthHook();
core.setAuthPluginHook(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

@@ -0,0 +1,91 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.importer.ImportPlugin;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
public class ImportCommand extends Command {
private final BungeeCore core;
public ImportCommand(FastLoginBungee plugin) {
super("import-db", plugin.getDescription().getName().toLowerCase() + ".import");
this.core = plugin.getCore();
}
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length < 2) {
String message = ChatColor.DARK_RED + "You need to specify the import plugin and database type";
sender.sendMessage(convertFromLegacy(message));
return;
}
ImportPlugin importPlugin;
switch (args[0].toLowerCase()) {
case "autoin":
importPlugin = ImportPlugin.AUTO_IN;
break;
case "bpa":
importPlugin = ImportPlugin.BPA;
break;
case "eldzi":
importPlugin = ImportPlugin.ELDZI;
break;
default:
String message = ChatColor.DARK_RED + "Unknown auto login plugin";
sender.sendMessage(convertFromLegacy(message));
return;
}
boolean sqlite;
switch (args[1].toLowerCase()) {
case "sqlite":
sqlite = true;
break;
case "mysql":
sqlite = false;
break;
default:
String message = ChatColor.DARK_RED + "Unknown storage type to import from. Either SQLite or MySQL";
sender.sendMessage(convertFromLegacy(message));
return;
}
String host = "";
String database = "";
String username = "";
String password = "";
if (!sqlite) {
if (args.length <= 5) {
String message = ChatColor.DARK_RED + "If importing from MySQL, you need to specify host database "
+ "and username passowrd too";
sender.sendMessage(convertFromLegacy(message));
return;
}
host = args[2];
database = args[3];
username = args[4];
password = args[5];
}
AuthStorage storage = core.getStorage();
boolean success = core.importDatabase(importPlugin, true, storage, host, database, username, password);
if (success) {
sender.sendMessage(convertFromLegacy(ChatColor.DARK_GREEN + "Successful imported the data"));
} else {
sender.sendMessage(convertFromLegacy(ChatColor.DARK_RED + "Failed to import the data. Check out the logs"));
}
}
private BaseComponent[] convertFromLegacy(String message) {
return TextComponent.fromLegacyText(message);
}
}

View File

@@ -0,0 +1,41 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import java.util.List;
import java.util.logging.Logger;
import net.md_5.bungee.BungeeCord;
public class MojangApiBungee extends MojangApiConnector {
public MojangApiBungee(Logger logger, List<String> localAddresses, int rateLimit) {
super(logger, localAddresses, rateLimit);
}
@Override
protected String getUUIDFromJson(String json) {
boolean isArray = json.startsWith("[");
MojangPlayer mojangPlayer;
if (isArray) {
mojangPlayer = BungeeCord.getInstance().gson.fromJson(json, MojangPlayer[].class)[0];
} else {
mojangPlayer = BungeeCord.getInstance().gson.fromJson(json, MojangPlayer.class);
}
String id = mojangPlayer.getId();
if ("null".equals(id)) {
return null;
}
return id;
}
@Override
public boolean hasJoinedServer(LoginSession session, String serverId) {
//this is not needed in Bungee
throw new UnsupportedOperationException("Not supported");
}
}

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,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,9 +1,12 @@
package com.github.games647.fastlogin.bungee.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.logging.Level;
import me.vik1395.BungeeAuth.ListenerClass;
import me.vik1395.BungeeAuth.Main;
@@ -19,29 +22,34 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
*
* Spigot: https://www.spigotmc.org/resources/bungeeauth.493/
*/
public class BungeeAuthHook implements BungeeAuthPlugin {
public class BungeeAuthHook implements AuthPlugin<ProxiedPlayer> {
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L32
private final Tables databaseConnection = new Tables();
@Override
public boolean forceLogin(ProxiedPlayer player) {
String playerName = player.getName();
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L92-95
Main.plonline.add(player.getName());
if (Main.plonline.contains(playerName)) {
return true;
}
Main.plonline.add(playerName);
//renamed from ct to databaseConnection
// databaseConnection.setStatus(player.getName(), "online");
Class<?>[] parameterTypes = new Class<?>[]{String.class, String.class};
Object[] arguments = new Object[]{player.getName(), "online"};
Object[] arguments = new Object[]{playerName, "online"};
try {
callProtected("setStatus", parameterTypes, arguments);
ListenerClass.movePlayer(player, false);
//proparly not thread-safe
ListenerClass.prelogin.get(player.getName()).cancel();
ListenerClass.prelogin.get(playerName).cancel();
} catch (Exception ex) {
Main.plugin.getLogger().severe("[BungeeAuth] Error force loging in player");
Main.plugin.getLogger().log(Level.SEVERE, "Error force loging in player", ex);
return false;
}
@@ -73,20 +81,17 @@ public class BungeeAuthHook implements BungeeAuthPlugin {
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);
Class<?>[] parameterTypes = new Class<?>[] {String.class, String.class, String.class, String.class
, String.class, String.class, String.class, String.class};
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");
Main.plugin.getLogger().log(Level.SEVERE, "[BungeeAuth] Error when creating a new player in the Database", ex);
return false;
}
@@ -95,10 +100,12 @@ public class BungeeAuthHook implements BungeeAuthPlugin {
//pail ;(
private void callProtected(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
Class<? extends Tables> tableClass = databaseConnection.getClass();
Class<Tables> tableClass = Tables.class;
Method method = tableClass.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
//renamed t to databaseConnection
//databaseConnection.newPlayerEntry(player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen);
method.invoke(databaseConnection, arguments);
}
}

View File

@@ -1,55 +1,20 @@
package com.github.games647.fastlogin.bungee.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* Represents a supporting authentication plugin in BungeeCord/Waterfall/... servers
* @deprecated please use com.github.games647.fastlogin.core.hooks.AuthPlugin<net.md_5.bungee.api.connection.ProxiedPlayer>
*/
public interface BungeeAuthPlugin {
@Deprecated
public interface BungeeAuthPlugin extends AuthPlugin<ProxiedPlayer> {
/**
* 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
*/
@Override
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
*/
@Override
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
*/
@Override
boolean forceRegister(ProxiedPlayer player, String password);
}

View File

@@ -1,9 +1,10 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.ForceLoginTask;
import com.github.games647.fastlogin.bungee.PlayerProfile;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.github.games647.fastlogin.bungee.tasks.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.tasks.ForceLoginTask;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.base.Charsets;
import java.lang.reflect.Field;
@@ -13,7 +14,8 @@ import java.util.logging.Level;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
@@ -21,6 +23,7 @@ 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;
import net.md_5.bungee.event.EventPriority;
/**
* Enables online mode logins for specified users and sends
@@ -29,81 +32,62 @@ import net.md_5.bungee.event.EventHandler;
*/
public class PlayerConnectionListener implements Listener {
protected final FastLoginBungee plugin;
private final FastLoginBungee plugin;
public PlayerConnectionListener(FastLoginBungee plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPreLogin(final PreLoginEvent preLoginEvent) {
public void onPreLogin(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);
}
}
});
PendingConnection connection = preLoginEvent.getConnection();
AsyncPremiumCheck asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
ProxyServer.getInstance().getScheduler().runAsync(plugin, asyncPremiumCheck);
}
@EventHandler
public void onLogin(PostLoginEvent loginEvent) {
ProxiedPlayer player = loginEvent.getPlayer();
PendingConnection connection = player.getPendingConnection();
@EventHandler(priority = EventPriority.LOW)
public void onLogin(LoginEvent loginEvent) {
if (loginEvent.isCancelled()) {
return;
}
//use the loginevent instead of the postlogin event in order to send the loginsuccess packet to the client
//with the offline uuid this makes it possible to set the skin then
PendingConnection connection = loginEvent.getConnection();
String username = connection.getName();
if (connection.isOnlineMode()) {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
playerProfile.setUuid(player.getUniqueId());
String ip = connection.getAddress().getAddress().getHostAddress();
plugin.getCore().getPendingLogins().remove(ip + username);
LoginSession session = plugin.getSession().get(connection);
session.setUuid(connection.getUniqueId());
PlayerProfile playerProfile = session.getProfile();
playerProfile.setUuid(connection.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
InitialHandler initialHandler = (InitialHandler) connection;
if (!plugin.getConfiguration().getBoolean("premiumUuid")) {
if (!plugin.getConfig().getBoolean("premiumUuid")) {
try {
UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(Charsets.UTF_8));
Field idField = initialHandler.getClass().getDeclaredField("uniqueId");
//bungeecord doesn't support overriding the premium uuid
//so we have to do it with reflection
Field idField = InitialHandler.class.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")) {
if (!plugin.getConfig().getBoolean("forwardSkin")) {
//this is null on offline mode
LoginResult loginProfile = initialHandler.getLoginProfile();
if (loginProfile != null) {
@@ -119,4 +103,11 @@ public class PlayerConnectionListener implements Listener {
ForceLoginTask loginTask = new ForceLoginTask(plugin, player, serverConnectedEvent.getServer());
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
ProxiedPlayer player = disconnectEvent.getPlayer();
plugin.getSession().remove(player.getPendingConnection());
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -1,11 +1,14 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.PlayerProfile;
import com.github.games647.fastlogin.bungee.tasks.AsyncToggleMessage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import net.md_5.bungee.api.ChatColor;
import java.util.Arrays;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@@ -16,7 +19,7 @@ import net.md_5.bungee.event.EventHandler;
public class PluginMessageListener implements Listener {
protected final FastLoginBungee plugin;
private final FastLoginBungee plugin;
public PluginMessageListener(FastLoginBungee plugin) {
this.plugin = plugin;
@@ -35,75 +38,53 @@ public class PluginMessageListener implements Listener {
//check if the message is sent from the server
if (Server.class.isAssignableFrom(pluginMessageEvent.getSender().getClass())) {
readMessage(pluginMessageEvent);
//so that we can safely process this in the background
byte[] data = Arrays.copyOf(pluginMessageEvent.getData(), pluginMessageEvent.getData().length);
ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> readMessage(forPlayer, data));
}
}
private void readMessage(PluginMessageEvent pluginMessageEvent) {
byte[] data = pluginMessageEvent.getData();
private void readMessage(ProxiedPlayer forPlayer, byte[] data) {
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
String subchannel = dataInput.readUTF();
final ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
if ("ON".equals(subchannel)) {
final String playerName = dataInput.readUTF();
String playerName = dataInput.readUTF();
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(playerName, 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);
}
if (playerName.equals(forPlayer.getName()) && plugin.getConfig().getBoolean("premium-warning")
&& !plugin.getCore().getPendingConfirms().contains(forPlayer.getUniqueId())) {
String message = plugin.getCore().getMessage("premium-warning");
forPlayer.sendMessage(TextComponent.fromLegacyText(message));
plugin.getCore().getPendingConfirms().add(forPlayer.getUniqueId());
return;
}
return;
}
playerProfile.setPremium(true);
//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);
}
});
plugin.getCore().getPendingConfirms().remove(forPlayer.getUniqueId());
AsyncToggleMessage task = new AsyncToggleMessage(plugin.getCore(), forPlayer, playerName, true);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else if ("OFF".equals(subchannel)) {
final String playerName = dataInput.readUTF();
String playerName = dataInput.readUTF();
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(playerName, 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);
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Removed to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
});
AsyncToggleMessage task = new AsyncToggleMessage(plugin.getCore(), forPlayer, playerName, false);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} 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);
onSuccessMessage(forPlayer);
}
}
private void onSuccessMessage(ProxiedPlayer forPlayer) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
BungeeLoginSession loginSession = plugin.getSession().get(forPlayer.getPendingConnection());
PlayerProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
//we override this in the loginevent
// playerProfile.setUuid(forPlayer.getUniqueId());
plugin.getStorage().save(playerProfile);
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
}
}
}

View File

@@ -0,0 +1,53 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.BungeeLoginSource;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.AsyncEvent;
public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, BungeeLoginSource> implements Runnable {
private final FastLoginBungee plugin;
private final AsyncEvent<?> preLoginEvent;
private final PendingConnection connection;
public AsyncPremiumCheck(FastLoginBungee plugin, AsyncEvent<?> preLoginEvent, PendingConnection connection) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
this.preLoginEvent = preLoginEvent;
this.connection = connection;
}
@Override
public void run() {
plugin.getSession().remove(connection);
String username = connection.getName();
try {
super.onLogin(username, new BungeeLoginSource(connection));
} finally {
preLoginEvent.completeIntent(plugin);
}
}
@Override
public void requestPremiumLogin(BungeeLoginSource source, PlayerProfile profile, String username, boolean registered) {
source.setOnlineMode();
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, registered, profile));
String ip = source.getAddress().getAddress().getHostAddress();
plugin.getCore().getPendingLogins().put(ip + username, new Object());
}
@Override
public void startCrackedSession(BungeeLoginSource source, PlayerProfile profile, String username) {
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, false, profile));
}
}

View File

@@ -0,0 +1,57 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.BungeeCore;
import com.github.games647.fastlogin.core.PlayerProfile;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
public class AsyncToggleMessage implements Runnable {
private final BungeeCore core;
private final ProxiedPlayer fromPlayer;
private final String targetPlayer;
private final boolean toPremium;
public AsyncToggleMessage(BungeeCore core, ProxiedPlayer fromPlayer, String targetPlayer, boolean toPremium) {
this.core = core;
this.fromPlayer = fromPlayer;
this.targetPlayer = targetPlayer;
this.toPremium = toPremium;
}
@Override
public void run() {
if (toPremium) {
activatePremium();
} else {
turnOffPremium();
}
}
private void turnOffPremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
//existing player is already cracked
if (playerProfile.getUserId() != -1 && !playerProfile.isPremium()) {
fromPlayer.sendMessage(TextComponent.fromLegacyText(core.getMessage("not-premium")));
return;
}
playerProfile.setPremium(false);
playerProfile.setUuid(null);
core.getStorage().save(playerProfile);
fromPlayer.sendMessage(TextComponent.fromLegacyText(core.getMessage("remove-premium")));
}
private void activatePremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
if (playerProfile.isPremium()) {
fromPlayer.sendMessage(TextComponent.fromLegacyText(core.getMessage("already-exists")));
return;
}
playerProfile.setPremium(true);
core.getStorage().save(playerProfile);
fromPlayer.sendMessage(TextComponent.fromLegacyText(core.getMessage("add-premium")));
}
}

View File

@@ -0,0 +1,135 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.UUID;
import java.util.logging.Level;
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;
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() {
try {
PendingConnection pendingConnection = player.getPendingConnection();
BungeeLoginSession session = plugin.getSession().get(pendingConnection);
if (session == null || !player.isConnected()) {
plugin.getLogger().log(Level.FINE, "Invalid session player {0} propaly left the server", player);
return;
}
PlayerProfile playerProfile = session.getProfile();
//force login only on success
if (pendingConnection.isOnlineMode()) {
boolean autoRegister = session.needsRegistration();
//2fa authentication - no need to send bukkit force login notification and so we also don't need
// to wait for a response -> save immediatly
if (!plugin.getConfig().getBoolean("autoLogin")) {
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
session.setAlreadySaved(true);
}
AuthPlugin<ProxiedPlayer> authPlugin = plugin.getCore().getAuthPluginHook();
if (authPlugin == null) {
//save will happen on success message from bukkit
sendBukkitLoginNotification(autoRegister);
} else if (session.needsRegistration()) {
forceRegister(session, authPlugin);
} else if (authPlugin.forceLogin(player)) {
forceLogin(session, authPlugin);
}
} else {
//cracked player
if (!session.isAlreadySaved()) {
playerProfile.setPremium(false);
plugin.getCore().getStorage().save(playerProfile);
session.setAlreadySaved(true);
}
}
} catch (Exception ex) {
plugin.getLogger().log(Level.INFO, "ERROR ON FORCE LOGIN", ex);
}
}
private void forceRegister(BungeeLoginSession session, AuthPlugin<ProxiedPlayer> authPlugin) {
if (session.isAlreadyLogged()) {
sendBukkitLoginNotification(true);
return;
}
session.setAlreadyLogged(true);
String password = plugin.getCore().getPasswordGenerator().getRandomPassword(player);
if (authPlugin.forceRegister(player, password)) {
//save will happen on success message from bukkit
sendBukkitLoginNotification(true);
String message = plugin.getCore().getMessage("auto-register");
if (message != null) {
message = message.replace("%password", password);
player.sendMessage(TextComponent.fromLegacyText(message));
}
}
}
private void forceLogin(BungeeLoginSession session, AuthPlugin<ProxiedPlayer> authPlugin) {
if (session.isAlreadyLogged()) {
sendBukkitLoginNotification(false);
return;
}
session.setAlreadyLogged(true);
if (authPlugin.forceLogin(player)) {
//save will happen on success message from bukkit
sendBukkitLoginNotification(false);
String message = plugin.getCore().getMessage("auto-login");
if (message != null) {
player.sendMessage(TextComponent.fromLegacyText(message));
}
}
}
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

@@ -8,7 +8,7 @@ main: ${project.groupId}.${project.artifactId}.${project.name}
version: ${project.version}
author: games647, http://github.com/games647/FastLogin/graphs/contributors
softdepends:
softdepend:
# BungeeCord auth plugins
- BungeeAuth

39
core/pom.xml Normal file
View File

@@ -0,0 +1,39 @@
<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.9</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>fastlogin.core</artifactId>
<packaging>jar</packaging>
<name>FastLoginCore</name>
<dependencies>
<!--Database pooling-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.5.0</version>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,253 @@
package com.github.games647.fastlogin.core;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
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.Calendar;
import java.util.Locale;
import java.util.UUID;
import java.util.logging.Level;
public class AuthStorage {
private static final String PREMIUM_TABLE = "premium";
private final FastLoginCore<?> core;
private final HikariDataSource dataSource;
//a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647
private final Calendar calendar = Calendar.getInstance(Locale.US);
public AuthStorage(FastLoginCore<?> core, String driver, String host, int port, String databasePath
, String user, String pass) {
this.core = core;
HikariConfig databaseConfig = new HikariConfig();
databaseConfig.setUsername(user);
databaseConfig.setPassword(pass);
databaseConfig.setDriverClassName(driver);
databaseConfig.setThreadFactory(core.getThreadFactory());
databasePath = databasePath.replace("{pluginDir}", core.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 HikariDataSource getDataSource() {
return dataSource;
}
public void createTables() throws SQLException {
Connection con = null;
Statement createStmt = null;
try {
con = dataSource.getConnection();
createStmt = 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, "
//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");
}
createStmt.executeUpdate(createDataStmt);
//drop the old unique uuid index
try {
if (dataSource.getJdbcUrl().contains("sqlite")) {
String tempTableCreate = createDataStmt.replace(PREMIUM_TABLE, PREMIUM_TABLE + "_TEMP")
//if we already imported the table fail here
.replace("IF NOT EXISTS", "");
//create a temp table insert it there and then back
createStmt.executeUpdate(tempTableCreate);
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + "_TEMP SELECT * FROM " + PREMIUM_TABLE);
createStmt.executeUpdate("DROP TABLE " + PREMIUM_TABLE);
createStmt.executeUpdate(createDataStmt);
//insert it back into the new table
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + " SELECT * FROM " + PREMIUM_TABLE + "_TEMP");
} else {
createStmt.executeUpdate("ALTER TABLE premium DROP INDEX UUID");
}
} catch (SQLException sqlEx) {
//silent - we already migrated
}
try {
createStmt.executeUpdate("CREATE INDEX uuid_idx on premium (UUID)");
} catch (SQLException sqlEx) {
//silent - we already migrated
}
} finally {
closeQuietly(con);
closeQuietly(createStmt);
}
}
public PlayerProfile loadProfile(String name) {
Connection con = null;
PreparedStatement loadStmt = null;
ResultSet resultSet = null;
try {
con = dataSource.getConnection();
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE Name=? LIMIT 1");
loadStmt.setString(1, name);
resultSet = loadStmt.executeQuery();
if (resultSet.next()) {
long userId = resultSet.getInt(1);
UUID uuid = FastLoginCore.parseId(resultSet.getString(2));
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
long lastLogin = resultSet.getTimestamp(6, calendar).getTime();
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
return playerProfile;
} else {
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
return crackedProfile;
}
} catch (SQLException sqlEx) {
core.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
closeQuietly(loadStmt);
closeQuietly(resultSet);
}
return null;
}
public PlayerProfile loadProfile(UUID uuid) {
Connection con = null;
PreparedStatement loadStmt = null;
ResultSet resultSet = null;
try {
con = dataSource.getConnection();
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE UUID=? LIMIT 1");
loadStmt.setString(1, uuid.toString().replace("-", ""));
resultSet = loadStmt.executeQuery();
if (resultSet.next()) {
long userId = resultSet.getInt(1);
String name = resultSet.getString(3);
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
long lastLogin = resultSet.getTimestamp(6, calendar).getTime();
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
return playerProfile;
}
} catch (SQLException sqlEx) {
core.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
closeQuietly(loadStmt);
closeQuietly(resultSet);
}
return null;
}
public boolean save(PlayerProfile playerProfile) {
Connection con = null;
PreparedStatement updateStmt = null;
PreparedStatement saveStmt = null;
ResultSet generatedKeys = null;
try {
con = dataSource.getConnection();
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
saveStmt = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) ", Statement.RETURN_GENERATED_KEYS);
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
}
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
saveStmt.execute();
generatedKeys = saveStmt.getGeneratedKeys();
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setUserId(generatedKeys.getInt(1));
}
} else {
saveStmt = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
}
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
saveStmt.setLong(5, playerProfile.getUserId());
saveStmt.execute();
}
return true;
} catch (SQLException ex) {
core.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
} finally {
closeQuietly(con);
closeQuietly(updateStmt);
closeQuietly(saveStmt);
closeQuietly(generatedKeys);
}
return false;
}
public void close() {
dataSource.close();
}
private void closeQuietly(AutoCloseable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception closeEx) {
core.getLogger().log(Level.SEVERE, "Failed to close connection", closeEx);
}
}
}
}

View File

@@ -0,0 +1,70 @@
package com.github.games647.fastlogin.core;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLSocketFactory;
public class BalancedSSLFactory extends SSLSocketFactory {
private final SSLSocketFactory oldFactory;
//in order to be thread-safe
private final List<InetAddress> localAddresses;
private AtomicInteger id;
public BalancedSSLFactory(SSLSocketFactory oldFactory, Iterable<InetAddress> localAddresses) {
this.oldFactory = oldFactory;
this.localAddresses = ImmutableList.copyOf(localAddresses);
}
@Override
public String[] getDefaultCipherSuites() {
return oldFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return oldFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoclose) throws IOException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort)
throws IOException, UnknownHostException {
//default
return oldFactory.createSocket(host, port, localAddress, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(InetAddress host, int port, InetAddress local, int localPort) throws IOException {
//Default
return oldFactory.createSocket(host, port, local, localPort);
}
private InetAddress getNextLocalAddress() {
int index = id.incrementAndGet() % localAddresses.size();
return localAddresses.get(index);
}
}

View File

@@ -0,0 +1,321 @@
package com.github.games647.fastlogin.core;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.RemovalListener;
/**
* Represents a Guava CacheBuilder that is compatible with both Guava 10 (Minecraft 1.7.X) and 13
*/
public class CompatibleCacheBuilder<K, V> {
private static Method BUILD_METHOD;
private static Method AS_MAP_METHOD;
/**
* Construct a new safe cache builder.
*
* @param <K> Key type
* @param <V> Value type
*
* @return A new cache builder.
*/
public static <K, V> CompatibleCacheBuilder<K, V> newBuilder() {
return new CompatibleCacheBuilder<>();
}
private final CacheBuilder<K, V> builder;
@SuppressWarnings("unchecked")
private CompatibleCacheBuilder() {
builder = (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
/**
* Guides the allowed concurrency among update operations. Used as a hint for internal sizing. The table is
* internally partitioned to try to permit the indicated number of concurrent updates without contention. Because
* assignment of entries to these partitions is not necessarily uniform, the actual concurrency observed may vary.
* Ideally, you should choose a value to accommodate as many threads as will ever concurrently modify the table.
* Using a significantly higher value than you need can waste space and time, and a significantly lower value can
* lead to thread contention. But overestimates and underestimates within an order of magnitude do not usually have
* much noticeable impact. A value of one permits only one thread to modify the cache at a time, but since read
* operations can proceed concurrently, this still yields higher concurrency than full synchronization. Defaults to
* 4.
*
* <p>
* <b>Note:</b>The default may change in the future. If you care about this value, you should always choose it
* explicitly.
*
* @param concurrencyLevel New concurrency level
* @return This for chaining
*
* @throws IllegalArgumentException if {@code concurrencyLevel} is nonpositive
* @throws IllegalStateException if a concurrency level was already set
*/
public CompatibleCacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) {
builder.concurrencyLevel(concurrencyLevel);
return this;
}
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, or last access. Access time is reset by
* {@link com.google.common.cache.Cache#get Cache.get()}, but not by operations on the view returned by
* {@link com.google.common.cache.Cache#asMap() Cache.asMap()}.
*
* <p>
* When {@code duration} is zero, elements will be evicted immediately after being loaded into the cache. This has
* the same effect as invoking {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, or to
* disable caching temporarily without a code change.
*
* <p>
* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be
* visible to read or write operations. Expired entries are currently cleaned up during write operations, or during
* occasional read operations in the absense of writes; though this behavior may change in the future.
*
* @param duration the length of time after an entry is last accessed that it should be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return This for chaining
*
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to idle or time to live was already set
*/
public CompatibleCacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
builder.expireAfterAccess(duration, unit);
return this;
}
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, or the most recent replacement of its value.
*
* <p>
* When {@code duration} is zero, elements will be evicted immediately after being loaded into the cache. This has
* the same effect as invoking {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, or to
* disable caching temporarily without a code change.
*
* <p>
* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be
* visible to read or write operations. Expired entries are currently cleaned up during write operations, or during
* occasional read operations in the absense of writes; though this behavior may change in the future.
*
* @param duration the length of time after an entry is created that it should be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return This for chaining
*
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle was already set
*/
public CompatibleCacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
builder.expireAfterWrite(duration, unit);
return this;
}
/**
* Sets the minimum total size for the internal hash tables. For example, if the initial capacity is {@code 60}, and
* the concurrency level is {@code 8}, then eight segments are created, each having a hash table of size eight.
* Providing a large enough estimate at construction time avoids the need for expensive resizing operations later,
* but setting this value unnecessarily high wastes memory.
*
* @param initialCapacity - initial capacity
* @return This for chaining
*
* @throws IllegalArgumentException if {@code initialCapacity} is negative
* @throws IllegalStateException if an initial capacity was already set
*/
public CompatibleCacheBuilder<K, V> initialCapacity(int initialCapacity) {
builder.initialCapacity(initialCapacity);
return this;
}
/**
* Specifies the maximum number of entries the cache may contain. Note that the cache <b>may evict an entry before
* this limit is exceeded</b>. As the cache size grows close to the maximum, the cache evicts entries that are less
* likely to be used again. For example, the cache may evict an entry because it hasn't been used recently or very
* often.
*
* <p>
* When {@code size} is zero, elements will be evicted immediately after being loaded into the cache. This has the
* same effect as invoking {@link #expireAfterWrite expireAfterWrite}{@code (0, unit)} or {@link #expireAfterAccess expireAfterAccess}{@code (0,
* unit)}. It can be useful in testing, or to disable caching temporarily without a code change.
*
* @param size the maximum size of the cache
* @return This for chaining
*
* @throws IllegalArgumentException if {@code size} is negative
* @throws IllegalStateException if a maximum size was already set
*/
public CompatibleCacheBuilder<K, V> maximumSize(int size) {
builder.maximumSize(size);
return this;
}
/**
* Specifies a listener instance, which all caches built using this {@code CacheBuilder} will notify each time an
* entry is removed from the cache by any means.
*
* <p>
* Each cache built by this {@code CacheBuilder} after this method is called invokes the supplied listener after
* removing an element for any reason (see removal causes in
* {@link com.google.common.cache.RemovalCause RemovalCause}). It will invoke the listener during invocations of any
* of that cache's public methods (even read-only methods).
*
* <p>
* <b>Important note:</b> Instead of returning <em>this</em> as a {@code CacheBuilder} instance, this method returns
* {@code CacheBuilder<K1, V1>}. From this point on, either the original reference or the returned reference may be
* used to complete configuration and build the cache, but only the "generic" one is type-safe. That is, it will
* properly prevent you from building caches whose key or value types are incompatible with the types accepted by
* the listener already provided; the {@code CacheBuilder} type cannot do this. For best results, simply use the
* standard method-chaining idiom, as illustrated in the documentation at top, configuring a {@code CacheBuilder}
* and building your {@link com.google.common.cache.Cache Cache} all in a single statement.
*
* <p>
* <b>Warning:</b> if you ignore the above advice, and use this {@code CacheBuilder} to build a cache whose key or
* value type is incompatible with the listener, you will likely experience a {@link ClassCastException} at some
* <i>undefined</i> point in the future.
*
* @param <K1> Key type
* @param <V1> Value type
* @param listener - removal listener
* @return This for chaining
*
* @throws IllegalStateException if a removal listener was already set
*/
@SuppressWarnings("unchecked")
public <K1 extends K, V1 extends V> CompatibleCacheBuilder<K1, V1> removalListener(RemovalListener<? super K1, ? super V1> listener) {
builder.removalListener(listener);
return (CompatibleCacheBuilder<K1, V1>) this;
}
/**
* Specifies a nanosecond-precision time source for use in determining when entries should be expired. By default,
* {@link System#nanoTime} is used.
*
* <p>
* The primary intent of this method is to facilitate testing of caches which have been configured with
* {@link #expireAfterWrite} or {@link #expireAfterAccess}.
*
* @param ticker - ticker
* @return This for chaining
*
* @throws IllegalStateException if a ticker was already set
*/
public CompatibleCacheBuilder<K, V> ticker(Ticker ticker) {
builder.ticker(ticker);
return this;
}
/**
* Specifies that each value (not key) stored in the cache should be wrapped in a
* {@link java.lang.ref.SoftReference SoftReference} (by default, strong references are used). Softly-referenced
* objects will be garbage-collected in a <i>globally</i>
* least-recently-used manner, in response to memory demand.
*
* <p>
* <b>Warning:</b> in most circumstances it is better to set a per-cache {@linkplain #maximumSize maximum size}
* instead of using soft references. You should only use this method if you are well familiar with the practical
* consequences of soft references.
*
* <p>
* <b>Note:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to determine
* equality of values.
*
* @return This for chaining
*
* @throws IllegalStateException if the value strength was already set
*/
public CompatibleCacheBuilder<K, V> softValues() {
builder.softValues();
return this;
}
/**
* Specifies that each key (not value) stored in the cache should be wrapped in a
* {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used).
*
* <p>
* <b>Warning:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to
* determine equality of keys.
*
* @return This for chaining
*
* @throws IllegalStateException if the key strength was already set
*/
public CompatibleCacheBuilder<K, V> weakKeys() {
builder.weakKeys();
return this;
}
/**
* Specifies that each value (not key) stored in the cache should be wrapped in a
* {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used).
*
* <p>
* Weak values will be garbage collected once they are weakly reachable. This makes them a poor candidate for
* caching; consider {@link #softValues} instead.
*
* <p>
* <b>Note:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to determine
* equality of values.
*
* @return This for chaining
*
* @throws IllegalStateException if the value strength was already set
*/
public CompatibleCacheBuilder<K, V> weakValues() {
builder.weakValues();
return this;
}
/**
* Returns the cache wrapped as a ConcurrentMap.
* <p>
* We can't return the direct Cache instance as it changed in Guava 13.
*
* @param <K1> Key type
* @param <V1> Value type
* @param loader - cache loader
* @return The cache as a a map.
*/
@SuppressWarnings("unchecked")
public <K1 extends K, V1 extends V> ConcurrentMap<K1, V1> build(CacheLoader<? super K1, V1> loader) {
Object cache = null;
if (BUILD_METHOD == null) {
try {
BUILD_METHOD = builder.getClass().getDeclaredMethod("build", CacheLoader.class);
BUILD_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find CacheBuilder.build(CacheLoader)", e);
}
}
// Attempt to build the Cache
try {
cache = BUILD_METHOD.invoke(builder, loader);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + BUILD_METHOD + " on " + builder, e);
}
if (AS_MAP_METHOD == null) {
try {
AS_MAP_METHOD = cache.getClass().getMethod("asMap");
AS_MAP_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find Cache.asMap() in " + cache, e);
}
}
// Retrieve it as a map
try {
return (ConcurrentMap<K1, V1>) AS_MAP_METHOD.invoke(cache);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + AS_MAP_METHOD + " on " + cache, e);
}
}
}

View File

@@ -1,10 +1,10 @@
package com.github.games647.fastlogin.bukkit;
package com.github.games647.fastlogin.core;
import java.util.UUID;
public class PlayerProfile {
private final String playerName;
private String playerName;
private long userId;
@@ -13,8 +13,7 @@ public class PlayerProfile {
private String lastIp;
private long lastLogin;
public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium
, String lastIp, 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;
@@ -24,23 +23,22 @@ public class PlayerProfile {
}
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;
this(-1, uuid, playerName, premium, lastIp, System.currentTimeMillis());
}
public String getPlayerName() {
public synchronized String getPlayerName() {
return playerName;
}
public synchronized void setPlayerName(String playerName) {
this.playerName = playerName;
}
public synchronized long getUserId() {
return userId;
}
protected synchronized void setUserId(long generatedId) {
public synchronized void setUserId(long generatedId) {
this.userId = generatedId;
}

View File

@@ -0,0 +1,26 @@
package com.github.games647.fastlogin.core;
import java.util.Map;
public class SharedConfig {
private final Map<String, Object> configValues;
public SharedConfig(Map<String, Object> configValues) {
this.configValues = configValues;
}
@SuppressWarnings("unchecked")
public <T> T get(String path, T def) {
Object val = configValues.get(path);
return ( val != null ) ? (T) val : def;
}
public <T> T get(String path) {
return get(path, null);
}
public Map<String, Object> getConfigValues() {
return configValues;
}
}

View File

@@ -0,0 +1,59 @@
package com.github.games647.fastlogin.core.hooks;
/**
* Represents a supporting authentication plugin in BungeeCord and Bukkit/Spigot/... servers
*
* @param <P> either org.bukkit.entity.Player for Bukkit or net.md_5.bungee.api.connection.ProxiedPlayer for BungeeCord
*/
public interface AuthPlugin<P> {
/**
* 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(P player);
/**
* 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(P player, String password);
/**
* 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;
}

View File

@@ -0,0 +1,20 @@
package com.github.games647.fastlogin.core.hooks;
import java.util.Random;
public class DefaultPasswordGenerator<P> implements PasswordGenerator<P> {
private static final char[] PASSWORD_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
private final Random random = new Random();
@Override
public String getRandomPassword(P player) {
StringBuilder generatedPassword = new StringBuilder(8);
for (int i = 1; i <= 8; i++) {
generatedPassword.append(PASSWORD_CHARACTERS[random.nextInt(PASSWORD_CHARACTERS.length - 1)]);
}
return generatedPassword.toString();
}
}

View File

@@ -0,0 +1,6 @@
package com.github.games647.fastlogin.core.hooks;
public interface PasswordGenerator<P> {
String getRandomPassword(P player);
}

View File

@@ -0,0 +1,73 @@
package com.github.games647.fastlogin.core.importer;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID;
import javax.sql.DataSource;
public class AutoInImporter extends Importer {
private static final String PLUGIN_NAME = "AutoIn";
private static final String SQLITE_FILE = "plugins/" + PLUGIN_NAME + "/AutoIn_PlayerOptions.db";
private static final String USER_TABLE = "nicknames";
private static final String UUID_TABLE = "uuids";
private static final String SESSION_TABLE = "sessions";
public static String getSQLitePath() {
return SQLITE_FILE;
}
@Override
public int importData(Connection source, DataSource target, AuthStorage storage) throws SQLException {
Statement stmt = null;
ResultSet resultSet = null;
try {
stmt = source.createStatement();
resultSet = stmt.executeQuery("SELECT name, protection, premium, puuid FROM " + USER_TABLE
+ " LEFT JOIN " + " ("
/* Prevent duplicates */
+ "SELECT * FROM " + UUID_TABLE + " GROUP BY nickname_id"
+ ") uuids"
+ " ON " + USER_TABLE + ".id = uuids.nickname_id");
int rows = 0;
while (resultSet.next()) {
String name = resultSet.getString(1);
boolean protection = resultSet.getBoolean(2);
/* Enable premium authentication only for those who want to be auto logged in,
so they have their cracked protection disabled */
boolean premium = !protection && resultSet.getBoolean(3);
String puuid = resultSet.getString(4);
/* FastLogin will also make lookups on the uuid column for name changes
the old 1.6.2 version won't check if those user have premium enabled
so it could happen that a premium could steal the account if we don't do this
It seems the uuid is saved on autoin too if the player is cracked */
PlayerProfile profile;
if (premium) {
profile = new PlayerProfile(UUID.fromString(puuid), name, premium, "");
} else {
profile = new PlayerProfile(null, name, premium, "");
}
storage.save(profile);
rows++;
}
return rows;
} finally {
closeQuietly(stmt);
closeQuietly(resultSet);
}
}
}

View File

@@ -0,0 +1,50 @@
package com.github.games647.fastlogin.core.importer;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import javax.sql.DataSource;
public class BPAImporter extends Importer {
private static final String DEFAULT_TABLE_NAME = "users";
@Override
public int importData(Connection source, DataSource target, AuthStorage storage) throws SQLException {
Statement stmt = null;
ResultSet resultSet = null;
try {
stmt = source.createStatement();
resultSet = stmt.executeQuery("SELECT "
+ "nick, "
+ "checked, "
+ "lastIP, "
+ "FROM_UNIXTIME(lastJoined * 0.001) AS LastLogin "
+ "FROM " + DEFAULT_TABLE_NAME);
int rows = 0;
while (resultSet.next()) {
String name = resultSet.getString(1);
boolean premium = resultSet.getBoolean(2);
String lastIP = resultSet.getString(3);
Timestamp lastLogin = resultSet.getTimestamp(4);
//uuid doesn't exist here
PlayerProfile profile = new PlayerProfile(null, name, premium, lastIP);
storage.save(profile);
rows++;
}
return rows;
} finally {
closeQuietly(stmt);
closeQuietly(resultSet);
}
}
}

View File

@@ -0,0 +1,58 @@
package com.github.games647.fastlogin.core.importer;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.UUID;
import javax.sql.DataSource;
public class ElDziAuthImporter extends Importer {
private static final String TABLE_NAME = "accounts";
@Override
public int importData(Connection source, DataSource target, AuthStorage storage) throws SQLException {
Statement stmt = null;
ResultSet resultSet = null;
try {
stmt = source.createStatement();
resultSet = stmt.executeQuery("SELECT "
+ "nick, "
+ "premium, "
+ "lastIP, "
+ "FROM_UNIXTIME(lastPlayed * 0.001) AS LastLogin "
+ "FROM " + TABLE_NAME);
int rows = 0;
while (resultSet.next()) {
String name = resultSet.getString(1);
boolean premium = resultSet.getBoolean(2);
String lastIP = resultSet.getString(3);
Timestamp lastLogin = resultSet.getTimestamp(4);
String uuid = resultSet.getString(5);
PlayerProfile profile;
if (premium) {
profile = new PlayerProfile(UUID.fromString(uuid), name, premium, lastIP);
} else {
profile = new PlayerProfile(null, name, premium, "");
}
storage.save(profile);
rows++;
}
return rows;
} finally {
closeQuietly(stmt);
closeQuietly(resultSet);
}
}
}

View File

@@ -0,0 +1,20 @@
package com.github.games647.fastlogin.core.importer;
public enum ImportPlugin {
AUTO_IN(AutoInImporter.class),
BPA(BPAImporter.class),
ELDZI(ElDziAuthImporter.class);
private final Class<? extends Importer> importerClass;
ImportPlugin(Class<? extends Importer> importer) {
this.importerClass = importer;
}
public Class<? extends Importer> getImporter() {
return importerClass;
}
}

View File

@@ -0,0 +1,21 @@
package com.github.games647.fastlogin.core.importer;
import com.github.games647.fastlogin.core.AuthStorage;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
public abstract class Importer {
public abstract int importData(Connection source, DataSource target, AuthStorage storage) throws SQLException;
protected void closeQuietly(AutoCloseable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignore) {
//ignore
}
}
}
}

View File

@@ -0,0 +1,198 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.CompatibleCacheBuilder;
import com.github.games647.fastlogin.core.SharedConfig;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
import com.github.games647.fastlogin.core.importer.AutoInImporter;
import com.github.games647.fastlogin.core.importer.ImportPlugin;
import com.github.games647.fastlogin.core.importer.Importer;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.Sets;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @param <P> Player class
*/
public abstract class FastLoginCore<P> {
public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
CompatibleCacheBuilder<Object, Object> builder = CompatibleCacheBuilder.newBuilder();
if (expireAfterWrite > 0) {
builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);
}
if (maxSize > 0) {
builder.maximumSize(maxSize);
}
return builder.build(CacheLoader.from(() -> {
throw new UnsupportedOperationException();
}));
}
public static UUID parseId(String withoutDashes) {
if (withoutDashes == null) {
return null;
}
return UUID.fromString(withoutDashes.substring(0, 8)
+ "-" + withoutDashes.substring(8, 12)
+ "-" + withoutDashes.substring(12, 16)
+ "-" + withoutDashes.substring(16, 20)
+ "-" + withoutDashes.substring(20, 32));
}
protected final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> pendingLogins = FastLoginCore.buildCache(5, 0);
private final Set<UUID> pendingConfirms = Sets.newHashSet();
private final SharedConfig sharedConfig;
private MojangApiConnector apiConnector;
private AuthStorage storage;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
public FastLoginCore(Map<String, Object> config) {
this.sharedConfig = new SharedConfig(config);
}
public void setApiConnector() {
List<String> ipAddresses = sharedConfig.get("ip-addresses");
int requestLimit = sharedConfig.get("mojang-request-limit");
this.apiConnector = makeApiConnector(getLogger(), ipAddresses, requestLimit);
}
public MojangApiConnector getApiConnector() {
return apiConnector;
}
public AuthStorage getStorage() {
return storage;
}
public abstract File getDataFolder();
public abstract Logger getLogger();
public abstract ThreadFactory getThreadFactory();
public String getMessage(String key) {
return localeMessages.get(key);
}
public abstract void loadMessages();
public abstract MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests);
public boolean setupDatabase() {
String driver = sharedConfig.get("driver");
String host = sharedConfig.get("host", "");
int port = sharedConfig.get("port", 3306);
String database = sharedConfig.get("database");
String user = sharedConfig.get("username", "");
String password = sharedConfig.get("password", "");
storage = new AuthStorage(this, driver, host, port, database, user, password);
try {
storage.createTables();
return true;
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
return false;
}
}
public boolean importDatabase(ImportPlugin plugin, boolean sqlite, AuthStorage storage, String host, String database
, String username, String pass) {
if (sqlite && (plugin == ImportPlugin.BPA || plugin == ImportPlugin.ELDZI)) {
throw new IllegalArgumentException("These plugins doesn't support flat file databases");
}
Importer importer;
try {
importer = plugin.getImporter().newInstance();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Couldn't not setup importer class", ex);
return false;
}
try {
if (sqlite && plugin == ImportPlugin.AUTO_IN) {
//load sqlite driver
Class.forName("org.sqlite.JDBC");
String jdbcUrl = "jdbc:sqlite:" + AutoInImporter.getSQLitePath();
Connection con = DriverManager.getConnection(jdbcUrl);
importer.importData(con, storage.getDataSource(), storage);
return true;
} else {
Class.forName("com.mysql.jdbc.Driver");
String jdbcUrl = "jdbc:mysql://" + host + "/" + database;
Connection con = DriverManager.getConnection(jdbcUrl, username, pass);
importer.importData(con, storage.getDataSource(), storage);
return true;
}
} catch (ClassNotFoundException ex) {
getLogger().log(Level.SEVERE, "Cannot find SQL driver. Do you removed it?", ex);
} catch (SQLException ex) {
getLogger().log(Level.SEVERE, "Couldn't import data. Aborting...", ex);
}
return false;
}
public SharedConfig getConfig() {
return sharedConfig;
}
public PasswordGenerator<P> getPasswordGenerator() {
return passwordGenerator;
}
public void setPasswordGenerator(PasswordGenerator<P> passwordGenerator) {
this.passwordGenerator = passwordGenerator;
}
public ConcurrentMap<String, Object> getPendingLogins() {
return pendingLogins;
}
public Set<UUID> getPendingConfirms() {
return pendingConfirms;
}
public AuthPlugin<P> getAuthPluginHook() {
return authPlugin;
}
public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
this.authPlugin = authPlugin;
}
public void close() {
if (storage != null) {
storage.close();
}
}
}

View File

@@ -0,0 +1,98 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.SharedConfig;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.UUID;
import java.util.logging.Level;
public abstract class JoinManagement<T, S extends LoginSource> {
protected final FastLoginCore<T> core;
protected final AuthPlugin<T> authHook;
public JoinManagement(FastLoginCore<T> core, AuthPlugin<T> authHook) {
this.core = core;
this.authHook = authHook;
}
public void onLogin(String username, S source) {
PlayerProfile profile = core.getStorage().loadProfile(username);
if (profile == null) {
return;
}
SharedConfig config = core.getConfig();
String ip = source.getAddress().getAddress().getHostAddress();
try {
if (profile.getUserId() == -1) {
if (core.getPendingLogins().containsKey(ip + username) && config.get("secondAttemptCracked", false)) {
core.getLogger().log(Level.INFO, "Second attempt login -> cracked {0}", username);
//first login request failed so make a cracked session
startCrackedSession(source, profile, username);
return;
}
UUID premiumUUID = null;
if (config.get("nameChangeCheck", false) || config.get("autoRegister", false)) {
core.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
premiumUUID = core.getApiConnector().getPremiumUUID(username);
}
if (premiumUUID == null
|| (!checkNameChange(source, username, premiumUUID)
&& !checkPremiumName(source, username, profile))) {
//nothing detected the player as premium -> start a cracked session
if (core.getConfig().get("switchMode", false)) {
source.kick(core.getMessage("switch-kick-message"));
return;
}
startCrackedSession(source, profile, username);
}
} else if (profile.isPremium()) {
requestPremiumLogin(source, profile, username, true);
} else {
startCrackedSession(source, profile, username);
}
} catch (Exception ex) {
core.getLogger().log(Level.SEVERE, "Failed to check premium state", ex);
}
}
private boolean checkPremiumName(S source, String username, PlayerProfile profile) throws Exception {
if (core.getConfig().get("autoRegister", false) && (authHook == null || !authHook.isRegistered(username))) {
requestPremiumLogin(source, profile, username, false);
return true;
}
return false;
}
private boolean checkNameChange(S source, String username, UUID premiumUUID) {
//user not exists in the db
if (core.getConfig().get("nameChangeCheck", false)) {
PlayerProfile profile = core.getStorage().loadProfile(premiumUUID);
if (profile != null) {
//uuid exists in the database
core.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
//update the username to the new one in the database
profile.setPlayerName(username);
requestPremiumLogin(source, profile, username, false);
return true;
}
}
return false;
}
public abstract void requestPremiumLogin(S source, PlayerProfile profile, String username, boolean registered);
public abstract void startCrackedSession(S source, PlayerProfile profile, String username);
}

View File

@@ -0,0 +1,56 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.util.UUID;
public abstract class LoginSession {
private final String username;
private final PlayerProfile profile;
private UUID uuid;
protected boolean registered;
public LoginSession(String username, boolean registered, PlayerProfile profile) {
this.username = username;
this.registered = registered;
this.profile = profile;
}
public String getUsername() {
return username;
}
/**
* This value is always false if we authenticate the player with a cracked authentication
*
* @return
*/
public boolean needsRegistration() {
return !registered;
}
public PlayerProfile getProfile() {
return profile;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,12 @@
package com.github.games647.fastlogin.core.shared;
import java.net.InetSocketAddress;
public interface LoginSource {
void setOnlineMode() throws Exception;
void kick(String message) throws Exception;
InetSocketAddress getAddress();
}

View File

@@ -0,0 +1,158 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.BalancedSSLFactory;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
public abstract class MojangApiConnector {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 3 * 1_000;
private static final String USER_AGENT = "Premium-Checker";
private static final String MCAPI_UUID_URL = "https://mcapi.ca/uuid/player/";
//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}$";
private static final int RATE_LIMIT_CODE = 429;
//compile the pattern only on plugin enable -> and this have to be threadsafe
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
private final ConcurrentMap<Object, Object> requests = FastLoginCore.buildCache(10, -1);
private final BalancedSSLFactory sslFactory;
private final int rateLimit;
private long lastRateLimit;
protected final Logger logger;
public MojangApiConnector(Logger logger, List<String> localAddresses, int rateLimit) {
this.logger = logger;
if (rateLimit > 600) {
this.rateLimit = 600;
} else {
this.rateLimit = rateLimit;
}
if (localAddresses.isEmpty()) {
this.sslFactory = null;
} else {
Set<InetAddress> addresses = Sets.newHashSet();
for (String localAddress : localAddresses) {
try {
InetAddress address = InetAddress.getByName(localAddress);
if (!address.isAnyLocalAddress()) {
logger.log(Level.WARNING, "Submitted IP-Address is not local {0}", address);
continue;
}
addresses.add(address);
} catch (UnknownHostException ex) {
logger.log(Level.SEVERE, "IP-Address is unknown to us", ex);
}
}
this.sslFactory = new BalancedSSLFactory(HttpsURLConnection.getDefaultSSLSocketFactory(), addresses);
}
}
/**
*
* @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
if (requests.size() >= rateLimit || System.currentTimeMillis() - lastRateLimit < 1_000 * 60 * 10) {
// plugin.getLogger().fine("STILL WAITING FOR RATE_LIMIT - TRYING Third-party API");
return getUUIDFromAPI(playerName);
}
requests.put(new Object(), new Object());
try {
HttpsURLConnection 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.equals("null")) {
return FastLoginCore.parseId(getUUIDFromJson(line));
}
} else if (connection.getResponseCode() == RATE_LIMIT_CODE) {
logger.info("RATE_LIMIT REACHED - TRYING THIRD-PARTY API");
lastRateLimit = System.currentTimeMillis();
return getUUIDFromAPI(playerName);
}
//204 - no content for not found
} catch (Exception ex) {
logger.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 UUID getUUIDFromAPI(String playerName) {
try {
HttpURLConnection httpConnection = getConnection(MCAPI_UUID_URL + playerName);
if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
//cracked
return null;
}
Reader reader = new InputStreamReader(httpConnection.getInputStream());
String json = CharStreams.toString(reader);
return FastLoginCore.parseId(getUUIDFromJson(json));
} catch (IOException iOException) {
logger.log(Level.SEVERE, "Tried converting name->uuid from third-party api", iOException);
}
return null;
}
public abstract boolean hasJoinedServer(LoginSession session, String serverId);
protected abstract String getUUIDFromJson(String json);
protected HttpsURLConnection getConnection(String url) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(2 * TIMEOUT);
//the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", USER_AGENT);
if (sslFactory != null) {
connection.setSSLSocketFactory(sslFactory);
}
return connection;
}
}

View File

@@ -0,0 +1,168 @@
# 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/core/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
# This is extra configuration option to the feature above. If we request a premium authentication from a player who
# isn't actual premium but used a premium username, the player will disconnect with the reason "invalid session" or
# "bad login".
#
# If you activate this, we are remembering this player and do not force another premium authentication if the player
# tries to join again, so the player could join as cracked player.
secondAttemptCracked: false
# New cracked players will be kicked from server. Good if you want switch from offline-mode to online-mode without
# losing players!
#
# Existing cracked and premium players could still join your server. Moreover you could add playernames to a whitelist.
# So that these cracked players could join too although they are new players.
switchMode: 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
# This will make an additional check (only for player names which are not in the database) against the mojang servers
# in order to get the premium UUID. If that premium UUID is in the database, we can assume on sucessful login that the
# player changed it's username and we just update the name in the database.
# Examples:
# #### Case 1
# nameChangeCheck = false ----- autoRegister = false
#
# Player logins as cracked until the player invoked the command /premium. Then we could override the existing database
# record.
#
# #### Case 2
#
# nameChangeCheck = true ----- autoRegister = false
#
# Connect the Mojang API and check what UUID the player has (UUID exists => Paid Minecraft account). If that UUID is in
# the database it's an **existing player** and FastLogin can **assume** the player is premium and changed the username.
# If it's not in the database, it's a new player and **could be a cracked player**. So we just use a offline mode
# authentication for this player.
#
# **Limitation**: Cracked players who uses the new username of a paid account cannot join the server if the database
# contains the old name. (Example: The owner of the paid account no longer plays on the server, but changed the username
# in the meanwhile).
#
# #### Case 3
#
# nameChangeCheck = false ----- autoRegister = true
#
# We will always request a premium authentication if the username is unknown to us, but is in use by a paid minecraft
# account. This means it's kind of a more aggressive check like nameChangeCheck = true and autoRegister = false, because
# it request a premium authentication which are completely new to us, that even the premium UUID is not in our database.
#
# **Limitation**: see below
#
# #### Case 4
#
# nameChangeCheck = true ----- autoRegister = true
#
# Based on autoRegister it checks if the player name is premium and login using a premium authentication. After that
# fastlogin receives the premium UUID and can update the database record.
#
# **Limitation from autoRegister**: New offline players who uses the username of an existing minecraft cannot join the
# server.
nameChangeCheck: 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
# Displays a warning message that this message SHOULD only be invoked by
# users who actually are the owner of this account. So not by cracked players
#
# If they still want to invoke the command, they have to invoke /premium again
premium-warning: true
# If you have autoRegister or nameChangeCheck enabled, you could be rate-limited by Mojang.
# The requests of the both options will be only made by FastLogin if the username is unknown to the server
# You are allowed to make 600 requests per 10-minutes (60 per minute)
# If you own a big server this value could be too low
# Once the limit is reached, new players are always logged in as cracked until the rate-limit is expired.
# (to the next ten minutes)
#
# The limit is IP-wide. If you have multiple IPv4-Addreses you specify them here. FastLogin will then use it in rotating
# order --> 5 different IP-addreses 5 * 600 per 10 minutes
# If this list is empty only the default one will be used
#
# Lists are created like this:
#ip-addresses:
# - 192-168-0-2
ip-addresses: []
# How many requests should be established until the plugin uses the third-party API https://mcapi.ca/
# Once this number is reached in a range of ten minutes it will start connecting to https://mcapi.ca/ for the next ten minutes
# This option exists in order to workaround the rate-limiting. Name -> UUID are fetched in the same way like heads
#
# If you want to join the discussion visit this: https://github.com/games647/FastLogin/issues/27#issuecomment-226954350
mojang-request-limit: 600
# This disables the auto login from fastlogin. So a premium (like a paid account) authentication is requested, but
# the player won't be auto logged into the account.
#
# This can be used as 2Factor authentication for better security of your accounts. A hacker then needs both passwords.
# The password of your minecraft and the password to login in with your auth plugin
autoLogin: 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
#driver: com.mysql.jdbc.Driver
#host: localhost
#port: 3306
#database: fastlogin
#username: myUser
#password: myPassword

View File

@@ -0,0 +1,98 @@
# FastLogin localization
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
# Source code: https://github.com/games647/FastLogin
#
# You can access the newest locale here:
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/messages.yml
#
# You want to have language template? Visit the Github Wiki here:
# https://github.com/games647/FastLogin/wiki/English
# In order to split a message into seperate lines you could just make a new line, but keep the '
# Example:
# bla: '&aFirst line
# Second line
# Third line'
# If you want to disable a message, you can just set it to a empty value.
# In this case no message will be sent
# Example:
# bla: ''
# ========= Shared (BungeeCord and Bukkit) ============
# Switch mode is activated and a new (non-whitelist) cracked player tries to join
switch-kick-message: '&4Only paid minecraft whitelisted accounts are allowed to join this server'
# Player activated premium logins in order to skip offline authentication
add-premium: '&2Added to the list of premium players'
# Player activated premium logins in order to skip offline authentication
add-premium-other: '&2Player has been added to the premium list'
# Player is already set be a paid account
already-exists: '&4You are already on the premium list'
# Player is already set be a paid account
already-exists-other: '&4Player is already on the premium list'
# Player was changed to be cracked
remove-premium: '&2Removed from the list of premium players'
# Player is already set to be cracked
not-premium: '&4You are not in the premium list'
# Player is already set to be cracked
not-premium-other: '&4Player is not in the premium list'
# Admin wanted to change the premium of a user that isn't known to the plugin
player-unknown: '&4Player not in the database'
# ========= Bukkit/Spigot/PaperSpigot/TacoSpigot only ================================
# The user skipped the authentication, because it was a premium player
auto-login: '&2Auto logged in'
# The user was auto registered on the first join. The user account will be registered to protect it from cracked players
# The password can be used if the mojang servers are down and you still want your premium users to login (PLANNED)
auto-register: '&2Auto registered with password: %password
You may want change it?'
# Player is not able to toggle the premium state of other players
no-permission: '&4Not enough permissions'
# Although the console can toggle the premium state, it's not possible for the console itself.
# Because the console is not a user. (obviously, isn't it?)
no-console: '&4You are not a player. You cannot toggle the premium state for YOURSELF as a console'
# The user wants to toggle premium state, but BungeeCord support is enabled. This means the server have to communicate
# with the BungeeCord first which will establish a connection with the database server.
wait-on-proxy: '&6Sending request...'
# When ProtocolLib is enabled and the plugin is unable to continue handling a login request after a requested premium
# authentication. In this state the client expects a success packet with a encrypted connection or disconnect packet.
# So we kick the player, if we cannot encrypt the connection. In other situation (example: premium name check),
# the player will be just authenticated as cracked
error-kick: '&4Error occured'
# The server sents a verify token within the premium authentication reqest. If this doesn't match on response,
# it could be another client sending malicious packets
invalid-verify-token: '&4Invalid token'
# The client sent no request join server request to the mojang servers which would proof that it's owner of that
# acciunt. Only modified clients would do this.
invalid-session: '&4Invalid session'
# The client sent a malicous packet without a login request packet
invalid-requst: '&4Invalid request'
# Message if the bukkit isn't fully started to inject the packets
not-started: '&cServer is not fully started yet. Please retry'
# Warning message if a user invoked /premium command
premium-warning: '&c&lWARNING: &6This command should &lonly&6 be invoked if you are the owner of this paid minecraft account
Type &a/premium&6 again to confirm'
# ========= Bungee/Waterfall only ================================

32
pom.xml
View File

@@ -8,7 +8,7 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.3</version>
<version>1.9</version>
<inceptionYear>2015</inceptionYear>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>
@@ -22,6 +22,7 @@
</properties>
<modules>
<module>core</module>
<module>bukkit</module>
<module>bungee</module>
<module>universal</module>
@@ -49,21 +50,12 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<source>1.8</source>
<target>1.8</target>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<outputDirectory>${outputDir}</outputDirectory>
</configuration>
</plugin>
</plugins>
<resources>
@@ -83,20 +75,4 @@
</resource>
</resources>
</build>
<dependencies>
<!--Database pooling-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.4.6</version>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
</project>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.3</version>
<version>1.9</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -43,10 +43,25 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<outputDirectory>${outputDir}</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.bukkit</artifactId>