Compare commits

...

49 Commits
1.5.2 ... 1.8

Author SHA1 Message Date
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
42 changed files with 1571 additions and 546 deletions

View File

@@ -1,7 +1,50 @@
######1.8
* Fix third-party not premium player detection
* Fix ProtocolSupport BungeeCord
######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

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.5.2</version>
<version>1.7.1</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>
@@ -64,7 +70,7 @@
<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>
@@ -99,9 +105,10 @@
</dependency>
<dependency>
<!--Oringal owner is lenis, but maven always uses the newest version-->
<groupId>com.github.lenis0012</groupId>
<artifactId>LoginSecurity-2</artifactId>
<!--Old version 2.0-->
<!--Old version 2.0 -->
<version>-9c09e73b7f-1</version>
<exclusions>
<exclusion>
@@ -111,6 +118,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.lenis0012.bukkit</groupId>
<artifactId>loginsecurity</artifactId>
<version>2.1.3-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.lucaseasedup.logit</groupId>
<artifactId>LogIt</artifactId>

View File

@@ -1,9 +1,11 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.FastLoginCore;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.File;
import java.io.InputStreamReader;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;
@@ -44,9 +46,21 @@ public class BukkitCore extends FastLoginCore {
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);
for (String key : defaults.getKeys(false)) {
String message = ChatColor.translateAlternateColorCodes('&', defaults.getString(key));
if (!message.isEmpty()) {
localeMessages.put(key, message);
}
}
for (String key : messageConfig.getKeys(false)) {
String message = ChatColor.translateAlternateColorCodes('&', messageConfig.getString(key));
if (!message.isEmpty()) {
if (message.isEmpty()) {
localeMessages.remove(key);
} else {
localeMessages.put(key, message);
}
}

View File

@@ -1,6 +1,5 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.fastlogin.core.LoginSession;
import com.github.games647.fastlogin.core.PlayerProfile;
@@ -19,9 +18,11 @@ public class BukkitLoginSession extends LoginSession {
private final byte[] verifyToken;
private UUID uuid;
private WrappedSignedProperty skinProperty;
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);
@@ -64,22 +65,23 @@ public class BukkitLoginSession extends LoginSession {
return ArrayUtils.clone(verifyToken);
}
/**
* 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;
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 skinProperty premium skin property
* @param encodedData
* @param skinSignature
*/
public synchronized void setSkin(WrappedSignedProperty skinProperty) {
this.skinProperty = skinProperty;
public synchronized void setSkin(String encodedData, String skinSignature) {
this.encodedSkinData = encodedData;
this.skinSignature = skinSignature;
}
/**

View File

@@ -0,0 +1,322 @@
package com.github.games647.fastlogin.bukkit;
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 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<K, V>();
}
private 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,23 +1,26 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
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.utility.SafeCacheBuilder;
import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.bukkit.listener.BukkitJoinListener;
import com.github.games647.fastlogin.bukkit.listener.BungeeCordListener;
import com.github.games647.fastlogin.bukkit.listener.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.listener.packet.EncryptionPacketListener;
import com.github.games647.fastlogin.bukkit.listener.packet.StartPacketListener;
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.FastLoginCore;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.Sets;
import java.security.KeyPair;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@@ -30,7 +33,7 @@ import org.bukkit.plugin.java.JavaPlugin;
*/
public class FastLoginBukkit extends JavaPlugin {
private static final int WORKER_THREADS = 5;
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();
@@ -39,31 +42,26 @@ public class FastLoginBukkit extends JavaPlugin {
private final FastLoginCore core = new BukkitCore(this);
private boolean serverStarted;
private final Set<UUID> pendingConfirms = Sets.newHashSet();
//this map is thread-safe for async access (Packet Listener)
//SafeCacheBuilder is used in order to be version independent
private final ConcurrentMap<String, BukkitLoginSession> session = SafeCacheBuilder.<String, BukkitLoginSession>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, BukkitLoginSession>() {
@Override
public BukkitLoginSession load(String key) throws Exception {
//A key should be inserted manually on start packet
throw new UnsupportedOperationException("Not supported");
}
});
private final ConcurrentMap<String, BukkitLoginSession> session = buildCache(1, -1);
//1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
private BukkitAuthPlugin authPlugin;
private PasswordGenerator passwordGenerator = new DefaultPasswordGenerator();
@Override
public void onEnable() {
core.setMojangApiConnector(new MojangApiBukkit(core));
core.loadConfig();
core.loadMessages();
List<String> ipAddresses = getConfig().getStringList("ip-addresses");
int requestLimit = getConfig().getInt("mojang-request-limit");
MojangApiBukkit mojangApi = new MojangApiBukkit(buildCache(10, -1), getLogger(), ipAddresses, requestLimit);
core.setMojangApiConnector(mojangApi);
try {
if (ClassUtil.isPresent("org.spigotmc.SpigotConfig")) {
bungeeCord = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
@@ -103,17 +101,19 @@ public class FastLoginBukkit extends JavaPlugin {
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");
}
}
@@ -132,14 +132,14 @@ public class FastLoginBukkit extends JavaPlugin {
//clean up
session.clear();
if (core != null) {
core.close();
}
//remove old blacklists
for (Player player : getServer().getOnlinePlayers()) {
player.removeMetadata(getName(), this);
}
if (core != null) {
core.close();
}
}
public FastLoginCore getCore() {
@@ -209,9 +209,32 @@ public class FastLoginBukkit extends JavaPlugin {
return serverStarted;
}
public Set<UUID> getPendingConfirms() {
return pendingConfirms;
}
public void setServerStarted() {
if (!this.serverStarted) {
this.serverStarted = true;
}
}
private <K, V> ConcurrentMap<K, V> buildCache(int minutes, int maxSize) {
CompatibleCacheBuilder<Object, Object> builder = CompatibleCacheBuilder.newBuilder();
if (minutes > 0) {
builder.expireAfterWrite(minutes, TimeUnit.MINUTES);
}
if (maxSize > 0) {
builder.maximumSize(maxSize);
}
return builder.build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
throw new UnsupportedOperationException("Not supported yet.");
}
});
}
}

View File

@@ -1,14 +1,16 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.fastlogin.core.FastLoginCore;
import com.github.games647.fastlogin.core.MojangApiConnector;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
@@ -19,8 +21,9 @@ 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(FastLoginCore plugin) {
super(plugin);
public MojangApiBukkit(ConcurrentMap<Object, Object> requests, Logger logger, List<String> localAddresses
, int rateLimit) {
super(requests, logger, localAddresses, rateLimit);
}
@Override
@@ -50,14 +53,14 @@ public class MojangApiBukkit extends MojangApiConnector {
if (propertyName.equals("textures")) {
String skinValue = (String) skinProperty.get("value");
String signature = (String) skinProperty.get("signature");
playerSession.setSkin(WrappedSignedProperty.fromValues(propertyName, skinValue, signature));
playerSession.setSkin(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);
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
@@ -66,8 +69,21 @@ public class MojangApiBukkit extends MojangApiConnector {
@Override
protected UUID getUUIDFromJson(String json) {
JSONObject userData = (JSONObject) JSONValue.parse(json);
String uuid = (String) userData.get("id");
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 (uuid == null || uuid.equals("null")) {
return null;
}
return FastLoginCore.parseId(uuid);
}
}

View File

@@ -6,7 +6,6 @@ import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -36,10 +35,10 @@ public class CrackedCommand implements CommandExecutor {
sender.sendMessage(message);
}
} else {
//todo: load async if it's not in the cache anymore
//todo: load async if
final 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() {
@@ -49,54 +48,58 @@ public class CrackedCommand implements CommandExecutor {
}
});
} 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(plugin.getCore().getMessage("no-permission"));
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
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.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage(plugin.getCore().getMessage("player-unknown"));
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.getCore().getStorage().save(profile);
}
});
} else {
sender.sendMessage(ChatColor.DARK_RED + "Player is not in the premium list");
}
}
onCrackedOther(sender, command, args);
}
return true;
}
private void onCrackedOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(plugin.getCore().getMessage("no-permission"));
return;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
//todo: load async
final 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("remove-premium"));
profile.setPremium(false);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
plugin.getCore().getStorage().save(profile);
}
});
} else {
sender.sendMessage(plugin.getCore().getMessage("not-premium-other"));
}
}
}
private void notifiyBungeeCord(CommandSender sender, String target) {
if (sender instanceof Player) {
notifiyBungeeCord((Player) sender, target);
} else {
plugin.getLogger().info("No player online to send a plugin message to the proxy");
//todo: add console support
// Player firstPlayer = Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
// notifiyBungeeCord(firstPlayer, target);
@@ -109,7 +112,7 @@ public class CrackedCommand implements CommandExecutor {
dataOutput.writeUTF("OFF");
dataOutput.writeUTF(target);
plugin.getLogger().info("No player online to send a plugin message to the proxy");
sender.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
}
}
}

View File

@@ -5,8 +5,9 @@ import com.github.games647.fastlogin.core.PlayerProfile;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
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;
@@ -41,10 +42,18 @@ public class PremiumCommand implements CommandExecutor {
sender.sendMessage(message);
}
} else {
// //todo: load async if it's not in the cache anymore
UUID id = ((Player) sender).getUniqueId();
if (plugin.getConfig().getBoolean("premium-warning") && !plugin.getPendingConfirms().contains(id)) {
sender.sendMessage(plugin.getCore().getMessage("premium-warning"));
plugin.getPendingConfirms().add(id);
return true;
}
plugin.getPendingConfirms().remove(id);
//todo: load async
final 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);
@@ -55,55 +64,60 @@ public class PremiumCommand implements CommandExecutor {
}
});
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(plugin.getCore().getMessage("no-permission"));
return true;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
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.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage(plugin.getCore().getMessage("player-unknown"));
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.getCore().getStorage().save(profile);
}
});
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
}
}
onPremiumOther(sender, command, args);
}
return true;
}
private void onPremiumOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
sender.sendMessage(plugin.getCore().getMessage("no-permission"));
return ;
}
if (plugin.isBungeeCord()) {
notifiyBungeeCord(sender, args[0]);
String message = plugin.getCore().getMessage("wait-on-proxy");
if (message != null) {
sender.sendMessage(message);
}
} else {
//todo: load async
final 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, new Runnable() {
@Override
public void run() {
plugin.getCore().getStorage().save(profile);
}
});
sender.sendMessage(plugin.getCore().getMessage("add-premium-other"));
}
}
}
private void notifiyBungeeCord(CommandSender sender, String target) {
if (sender instanceof Player) {
notifiyBungeeCord((Player) sender, target);
} else {
plugin.getLogger().info("No player online to send a plugin message to the proxy");
//todo: add console support
// Player firstPlayer = Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
// notifiyBungeeCord(firstPlayer, target);

View File

@@ -1,7 +1,5 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.comphenix.protocol.reflect.FuzzyReflection;
import de.st_ddt.crazylogin.CrazyLogin;
import de.st_ddt.crazylogin.data.LoginPlayerData;
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
@@ -13,6 +11,7 @@ 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;
@@ -108,7 +107,7 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
private PlayerListener getListener() {
PlayerListener listener;
try {
listener = FuzzyReflection.getFieldValue(crazyLoginPlugin, PlayerListener.class, true);
listener = (PlayerListener) FieldUtils.readField(crazyLoginPlugin, "playerListener", true);
} catch (Exception ex) {
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to get the listener instance for auto login", ex);
listener = null;

View File

@@ -1424,4 +1424,34 @@ public class FakePlayer implements Player {
public boolean isCollidable() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void stopSound(Sound sound) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void stopSound(String sound) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean isSilent() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setSilent(boolean flag) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean hasGravity() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setGravity(boolean gravity) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@@ -1,7 +1,13 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.google.common.base.Charsets;
import com.lenis0012.bukkit.ls.LoginSecurity;
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 com.lenis0012.bukkit.ls.data.DataManager;
import java.net.InetAddress;
@@ -17,24 +23,56 @@ 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 {
protected final LoginSecurity securityPlugin = LoginSecurity.instance;
// protected final boolean newVersion;
protected final com.lenis0012.bukkit.ls.LoginSecurity securityPlugin;
protected final FastLoginBukkit plugin = (FastLoginBukkit) Bukkit.getPluginManager().getPlugin("FastLogin");
protected final boolean newVersion;
public LoginSecurityHook() {
// this.newVersion = ClassUtil.isPresent("com.lenis0012.bukkit.loginsecurity.session.action.LoginAction");
this.newVersion = ClassUtil.isPresent("com.lenis0012.bukkit.loginsecurity.LoginSecurity", getClass());
if (newVersion) {
this.securityPlugin = null;
} else {
this.securityPlugin = com.lenis0012.bukkit.ls.LoginSecurity.instance;
}
}
@Override
public boolean forceLogin(final Player player) {
//Login command of this plugin: (How the plugin logs the player in)
public boolean forceLogin(Player player) {
if (!newVersion) {
return oldForceLogin(player);
}
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
return session.performAction(new LoginAction(AuthService.PLUGIN, plugin)).isSuccess();
}
@Override
public boolean isRegistered(String playerName) throws Exception {
if (!newVersion) {
return oldIsRegistred(playerName);
}
PlayerSession session = LoginSecurity.getSessionManager().getOfflineSession(playerName);
return session.isRegistered();
}
@Override
public boolean forceRegister(Player player, String password) {
if (!newVersion) {
return oldForceRegister(player, password);
}
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
return session.performAction(new RegisterAction(AuthService.PLUGIN, plugin, password)).isSuccess();
}
public boolean oldForceLogin(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
@@ -62,20 +100,16 @@ public class LoginSecurityHook implements BukkitAuthPlugin {
}
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean oldIsRegistred(String playerName) throws Exception {
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L296
DataManager dataManager = securityPlugin.data;
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L283
UUID offlineUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(Charsets.UTF_8));
return dataManager.isRegistered(offlineUuid.toString().replace("-", ""));
//check for loginsecurity sessions in order to prevent a sql query?
//sesUse && thread.getSession().containsKey(uuid) && checkLastIp(player)) {
}
@Override
public boolean forceRegister(Player player, String password) {
public boolean oldForceRegister(Player player, String password) {
DataManager dataManager = securityPlugin.data;
UUID playerUUID = player.getUniqueId();

View File

@@ -1,8 +1,5 @@
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.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
@@ -14,6 +11,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.player.PlayerQuitEvent;
/**
* This listener tells authentication plugins if the player has a premium account and we checked it successfully. So the
@@ -32,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"));
}
}
@@ -40,18 +38,16 @@ public class BukkitJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
BukkitLoginSession session = plugin.getSessions().get(player.getAddress().toString());
if (session != null && plugin.getConfig().getBoolean("forwardSkin")) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
WrappedSignedProperty skin = session.getSkin();
if (skin != null) {
gameProfile.getProperties().put("textures", skin);
}
}
if (!plugin.isBungeeCord()) {
//Wait before auth plugin and we received a message from BungeeCord initializes the player
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin, player), DELAY_LOGIN);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
Player player = quitEvent.getPlayer();
player.removeMetadata(plugin.getName(), plugin);
plugin.getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -42,7 +42,7 @@ public class BungeeCordListener implements PluginMessageListener {
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
public void onPluginMessageReceived(String channel, final Player player, byte[] message) {
if (!channel.equals(plugin.getName())) {
return;
}
@@ -75,6 +75,7 @@ public class BungeeCordListener implements PluginMessageListener {
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, new Runnable() {
@Override
@@ -82,10 +83,11 @@ public class BungeeCordListener implements PluginMessageListener {
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin != null && !authPlugin.isRegistered(playerName)) {
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);
@@ -93,8 +95,6 @@ public class BungeeCordListener implements PluginMessageListener {
}
});
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
}
}
}

View File

@@ -1,174 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.packet;
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.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.core.PlayerProfile;
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 profile = plugin.getCore().getStorage().loadProfile(username);
if (profile != null) {
if (profile.getUserId() == -1) {
UUID premiumUUID = null;
if (plugin.getConfig().getBoolean("nameChangeCheck") || plugin.getConfig().getBoolean("autoRegister")) {
premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username);
}
//user not exists in the db
try {
if (premiumUUID != null && plugin.getConfig().getBoolean("nameChangeCheck")) {
profile = plugin.getCore().getStorage().loadProfile(premiumUUID);
if (profile != null) {
plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
enablePremiumLogin(username, profile, sessionKey, player, packetEvent, false);
return;
}
}
if (premiumUUID != null
&& plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
enablePremiumLogin(username, profile, sessionKey, player, packetEvent, false);
return;
}
//no premium check passed so we save it as a cracked player
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(sessionKey, loginSession);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
} else if (profile.isPremium()) {
enablePremiumLogin(username, profile, sessionKey, player, packetEvent, true);
} else {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(sessionKey, loginSession);
}
}
}
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
private void enablePremiumLogin(String username, PlayerProfile profile, 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) {
BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId
, verifyToken, registered, profile);
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,72 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
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.lang.reflect.Method;
import java.util.Collection;
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;
public class LoginSkinApplyListener implements Listener {
private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
private static final MethodAccessor GET_PROPERTIES = Accessors.getMethodAcccessorOrNull(
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) {
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
Collection<BukkitLoginSession> sessions = plugin.getSessions().values();
for (BukkitLoginSession session : sessions) {
if (session.getUsername().equals(player.getName())) {
applySkin(player, session);
break;
}
}
}
}
private void applySkin(Player player, BukkitLoginSession session) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
String skinData = session.getEncodedSkinData();
String signature = session.getSkinSignature();
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 {
Method putMethod = map.getClass().getMethod("put", Object.class, Object.class);
putMethod.invoke(map, "textures", skin.getHandle());
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium skin", ex);
}
}
}
}
}

View File

@@ -0,0 +1,141 @@
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.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
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;
public class NameCheckTask implements Runnable {
private static final int VERIFY_TOKEN_LENGTH = 4;
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) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.random = random;
this.player = player;
this.username = username;
}
@Override
public void run() {
try {
nameCheck();
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
private void nameCheck() {
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(username);
if (profile == null) {
return;
}
if (profile.getUserId() == -1) {
UUID premiumUUID = null;
//user not exists in the db
try {
boolean isRegistered = plugin.getAuthPlugin().isRegistered(username);
if (plugin.getConfig().getBoolean("nameChangeCheck")
|| (plugin.getConfig().getBoolean("autoRegister") && !isRegistered)) {
premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username);
}
if (premiumUUID != null && plugin.getConfig().getBoolean("nameChangeCheck")) {
PlayerProfile uuidProfile = plugin.getCore().getStorage().loadProfile(premiumUUID);
if (uuidProfile != null) {
plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
enablePremiumLogin(uuidProfile, false);
return;
}
}
if (premiumUUID != null && plugin.getConfig().getBoolean("autoRegister") && !isRegistered) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
enablePremiumLogin(profile, false);
return;
}
//no premium check passed so we save it as a cracked player
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(player.getAddress().toString(), loginSession);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
} else if (profile.isPremium()) {
enablePremiumLogin(profile, true);
} else {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getSessions().put(player.getAddress().toString(), loginSession);
}
}
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
private void enablePremiumLogin(PlayerProfile profile, 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[] verify = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(verify);
boolean success = sentEncryptionRequest(player, serverId, verify);
if (success) {
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);
}
}
}
private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
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,84 @@
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 com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
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()) {
return;
}
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", new Object[]{sessionKey, username});
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
if (authPlugin == null) {
return;
}
packetEvent.getAsyncMarker().incrementProcessingDelay();
NameCheckTask nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username);
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
}
}

View File

@@ -1,8 +1,8 @@
package com.github.games647.fastlogin.bukkit.listener.packet;
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.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
@@ -26,65 +26,42 @@ 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 {
public class VerifyResponseTask implements Runnable {
private final ProtocolManager protocolManager;
//hides the inherit Plugin plugin field, but we need this type
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
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());
private final Player fromPlayer;
private final byte[] sharedSecret;
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player fromPlayer, byte[] sharedSecret) {
this.plugin = plugin;
this.protocolManager = protocolManger;
this.packetEvent = packetEvent;
this.fromPlayer = fromPlayer;
this.sharedSecret = sharedSecret;
}
/**
* 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) {
Player player = packetEvent.getPlayer();
BukkitLoginSession session = plugin.getSessions().get(player.getAddress().toString());
if (session == null) {
disconnect(packetEvent, plugin.getCore().getMessage("invalid-requst"), true
, "Player {0} tried to send encryption response at invalid state", player.getAddress());
return;
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 {
verifyResponse(session);
}
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
private void verifyResponse(BukkitLoginSession session) {
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)) {
if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) {
return;
}
@@ -102,34 +79,35 @@ public class EncryptionPacketListener extends PacketAdapter {
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
session.setVerified(true);
setPremiumUUID(session, player);
receiveFakeStartPacket(username, player);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(packetEvent, plugin.getCore().getMessage("invalid-session"), true
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), player.getAddress(), serverId);
, session.getUsername(), fromPlayer.getAddress(), serverId);
}
//this is a fake packet; it shouldn't be send to the server
packetEvent.setCancelled(true);
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
}
private void setPremiumUUID(BukkitLoginSession session, Player player) {
UUID uuid = session.getUuid();
if (plugin.getConfig().getBoolean("premiumUuid") && uuid != null) {
private void setPremiumUUID(UUID premiumUUID) {
if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) {
try {
Object networkManager = getNetworkManager(player);
Object networkManager = getNetworkManager();
//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);
spoofField.set(networkManager, premiumUUID);
} catch (ReflectiveOperationException reflectiveOperationException) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", reflectiveOperationException);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey, PacketEvent packetEvent) {
private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey) {
byte[] requestVerify = session.getVerifyToken();
//encrypted verify token
byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1);
@@ -137,7 +115,7 @@ public class EncryptionPacketListener extends PacketAdapter {
//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, plugin.getCore().getMessage("invalid-verify-token"), true
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;
@@ -147,9 +125,8 @@ public class EncryptionPacketListener extends PacketAdapter {
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager(Player player)
throws IllegalAccessException, NoSuchFieldException {
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
private Object getNetworkManager() throws IllegalAccessException, NoSuchFieldException {
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer);
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
@@ -160,11 +137,10 @@ public class EncryptionPacketListener extends PacketAdapter {
return injectorField.get(rawInjector);
}
private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent)
throws IllegalArgumentException {
private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager(player);
Object networkManager = getNetworkManager();
//try to detect the method by parameters
Method encryptConnectionMethod = FuzzyReflection
@@ -174,16 +150,15 @@ public class EncryptionPacketListener extends PacketAdapter {
//the client expects this behaviour
encryptConnectionMethod.invoke(networkManager, loginKey);
} catch (ReflectiveOperationException ex) {
disconnect(packetEvent, plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption", ex);
disconnect(plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption", ex);
return false;
}
return true;
}
private void disconnect(PacketEvent packetEvent, String kickReason, boolean debugLevel, String logMessage
, Object... arguments) {
if (debugLevel) {
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);
@@ -191,10 +166,14 @@ public class EncryptionPacketListener extends PacketAdapter {
kickPlayer(packetEvent.getPlayer(), kickReason);
//cancel the event in order to prevent the server receiving an invalid packet
packetEvent.setCancelled(true);
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
}
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));
@@ -210,7 +189,9 @@ public class EncryptionPacketListener extends PacketAdapter {
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username, Player from) {
private void receiveFakeStartPacket(String username) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
//see StartPacketListener for packet information
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
@@ -219,11 +200,11 @@ public class EncryptionPacketListener extends PacketAdapter {
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
protocolManager.recieveClientPacket(from, startPacket, false);
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(from, plugin.getCore().getMessage("error-kick"));
kickPlayer(fromPlayer, plugin.getCore().getMessage("error-kick"));
}
}
}

View File

@@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit.listener;
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
@@ -44,12 +44,15 @@ public class ProtocolSupportListener implements Listener {
if (profile != null) {
if (profile.getUserId() == -1) {
UUID premiumUUID = null;
if (plugin.getConfig().getBoolean("nameChangeCheck") || plugin.getConfig().getBoolean("autoRegister")) {
premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username);
}
//user not exists in the db
try {
boolean isRegistered = plugin.getAuthPlugin().isRegistered(username);
if (plugin.getConfig().getBoolean("nameChangeCheck")
|| (plugin.getConfig().getBoolean("autoRegister") && !isRegistered)) {
premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username);
}
if (premiumUUID != null && plugin.getConfig().getBoolean("nameChangeCheck")) {
profile = plugin.getCore().getStorage().loadProfile(premiumUUID);
if (profile != null) {
@@ -59,8 +62,7 @@ public class ProtocolSupportListener implements Listener {
}
}
if (premiumUUID != null
&& plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
if (premiumUUID != null && plugin.getConfig().getBoolean("autoRegister") && !isRegistered) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
startPremiumSession(username, loginStartEvent, false, profile);
return;

View File

@@ -3,8 +3,8 @@ package com.github.games647.fastlogin.bukkit.tasks;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.Storage;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
@@ -39,7 +39,7 @@ public class ForceLoginTask implements Runnable {
return;
}
Storage storage = plugin.getCore().getStorage();
AuthStorage storage = plugin.getCore().getStorage();
PlayerProfile playerProfile = session.getProfile();
//check if it's the same player as we checked before
@@ -64,7 +64,7 @@ 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);
}
@@ -87,7 +87,7 @@ public class ForceLoginTask implements Runnable {
String generatedPassword = plugin.generateStringPassword(player);
boolean success = authPlugin.forceRegister(player, generatedPassword);
String message = plugin.getCore().getMessage("auto-register");
if (message != null) {
if (success && message != null) {
message = message.replace("%password", generatedPassword);
player.sendMessage(message);
}
@@ -100,7 +100,7 @@ public class ForceLoginTask implements Runnable {
boolean success = authPlugin.forceLogin(player);
String message = plugin.getCore().getMessage("auto-login");
if (message != null) {
if (success && message != null) {
player.sendMessage(message);
}

View File

@@ -14,12 +14,11 @@ 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 require either ProtocolLib or ProtocolSupport
- ProtocolSupport
# Auth plugins
- ProtocolLib
# Auth plugins - we delay the hook
# - xAuth
# - AuthMe
# - LogIt

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.5.2</version>
<version>1.7.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -19,8 +19,8 @@
<repositories>
<!--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-->
@@ -41,10 +41,10 @@
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.9-SNAPSHOT</version>
<version>1.8-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- <dependency>
<!-- <dependency>
<groupId>io.github.waterfallmc</groupId>
<artifactId>waterfall-api</artifactId>
<version>1.9-SNAPSHOT</version>

View File

@@ -49,9 +49,13 @@ public class BungeeCore extends FastLoginCore {
public void loadMessages() {
try {
saveDefaultFile("messages.yml");
Configuration defaults = ConfigurationProvider.getProvider(YamlConfiguration.class)
.load(getClass().getResourceAsStream("/messages.yml"));
File messageFile = new File(getDataFolder(), "messages.yml");
Configuration messageConfig = ConfigurationProvider.getProvider(YamlConfiguration.class).load(messageFile);
Configuration messageConfig = ConfigurationProvider.getProvider(YamlConfiguration.class)
.load(messageFile, defaults);
for (String key : messageConfig.getKeys()) {
String message = ChatColor.translateAlternateColorCodes('&', messageConfig.getString(key));
if (!message.isEmpty()) {

View File

@@ -5,12 +5,18 @@ 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.github.games647.fastlogin.core.FastLoginCore;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import net.md_5.bungee.api.connection.PendingConnection;
@@ -29,30 +35,36 @@ public class FastLoginBungee extends Plugin {
private final FastLoginCore loginCore = new BungeeCore(this);
private BungeeAuthPlugin bungeeAuthPlugin;
private Configuration configuration;
private Configuration config;
private final Random random = new Random();
private final Set<UUID> pendingConfirms = Sets.newHashSet();
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = Maps.newConcurrentMap();
@Override
public void onEnable() {
loginCore.setMojangApiConnector(new MojangApiBungee(loginCore));
loginCore.loadConfig();
loginCore.loadMessages();
try {
File configFile = new File(getDataFolder(), "config.yml");
configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
String driver = configuration.getString("driver");
String host = configuration.getString("host", "");
int port = configuration.getInt("port", 3306);
String database = configuration.getString("database");
List<String> ipAddresses = getConfig().getStringList("ip-addresses");
int requestLimit = getConfig().getInt("mojang-request-limit");
ConcurrentMap<Object, Object> requestCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES).build().asMap();
MojangApiBungee mojangApi = new MojangApiBungee(requestCache, getLogger(), ipAddresses, requestLimit);
loginCore.setMojangApiConnector(mojangApi);
String username = configuration.getString("username", "");
String password = configuration.getString("password", "");
String driver = config.getString("driver");
String host = config.getString("host", "");
int port = config.getInt("port", 3306);
String database = config.getString("database");
String username = config.getString("username", "");
String password = config.getString("password", "");
if (!loginCore.setupDatabase(driver, host, port, database, username, password)) {
return;
}
@@ -94,13 +106,17 @@ public class FastLoginBungee extends Plugin {
}
public Configuration getConfig() {
return configuration;
return config;
}
public ConcurrentMap<PendingConnection, BungeeLoginSession> getSession() {
return session;
}
public Set<UUID> getPendingConfirms() {
return pendingConfirms;
}
/**
* Get the auth plugin hook for BungeeCord
*

View File

@@ -3,19 +3,35 @@ package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.FastLoginCore;
import com.github.games647.fastlogin.core.MojangApiConnector;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
import net.md_5.bungee.BungeeCord;
public class MojangApiBungee extends MojangApiConnector {
public MojangApiBungee(FastLoginCore plugin) {
super(plugin);
public MojangApiBungee(ConcurrentMap<Object, Object> requests, Logger logger, List<String> localAddresses
, int rateLimit) {
super(requests, logger, localAddresses, rateLimit);
}
@Override
protected UUID getUUIDFromJson(String json) {
MojangPlayer mojangPlayer = BungeeCord.getInstance().gson.fromJson(json, MojangPlayer.class);
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);
}
if (mojangPlayer.getId() == null || mojangPlayer.getId().equals("null")) {
return null;
}
return FastLoginCore.parseId(mojangPlayer.getId());
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -41,7 +42,7 @@ public class BungeeAuthHook implements BungeeAuthPlugin {
//proparly not thread-safe
ListenerClass.prelogin.get(player.getName()).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;
}
@@ -86,7 +87,7 @@ public class BungeeAuthHook implements BungeeAuthPlugin {
//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,7 +96,7 @@ 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);

View File

@@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.fastlogin.bungee.tasks.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.tasks.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.tasks.ForceLoginTask;
import com.github.games647.fastlogin.core.LoginSession;
import com.github.games647.fastlogin.core.PlayerProfile;
@@ -14,8 +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.LoginEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
@@ -23,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
@@ -47,15 +48,20 @@ public class PlayerConnectionListener implements Listener {
ProxyServer.getInstance().getScheduler().runAsync(plugin, new AsyncPremiumCheck(plugin, preLoginEvent));
}
@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()) {
LoginSession session = plugin.getSession().get(connection);
PlayerProfile playerProfile = session.getProfile();
playerProfile.setUuid(player.getUniqueId());
playerProfile.setUuid(connection.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
InitialHandler initialHandler = (InitialHandler) connection;
@@ -65,7 +71,7 @@ public class PlayerConnectionListener implements Listener {
//bungeecord doesn't support overriding the premium uuid
//so we have to do it with reflection
Field idField = initialHandler.getClass().getDeclaredField("uniqueId");
Field idField = InitialHandler.class.getDeclaredField("uniqueId");
idField.setAccessible(true);
idField.set(connection, offlineUUID);
} catch (NoSuchFieldException | IllegalAccessException ex) {
@@ -91,8 +97,9 @@ public class PlayerConnectionListener implements Listener {
}
@EventHandler
public void onServerConnected(PlayerDisconnectEvent disconnectEvent) {
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
ProxiedPlayer player = disconnectEvent.getPlayer();
plugin.getSession().remove(player.getPendingConnection());
plugin.getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -6,8 +6,10 @@ 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 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;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
@@ -40,36 +42,50 @@ public class PluginMessageListener implements Listener {
}
private void readMessage(PluginMessageEvent pluginMessageEvent) {
byte[] data = pluginMessageEvent.getData();
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
String subchannel = dataInput.readUTF();
//so that we can safely process this in the background
final byte[] data = Arrays.copyOf(pluginMessageEvent.getData(), pluginMessageEvent.getData().length);
final ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
ProxiedPlayer fromPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
if ("ON".equals(subchannel)) {
String playerName = dataInput.readUTF();
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
String subchannel = dataInput.readUTF();
if ("ON".equals(subchannel)) {
String playerName = dataInput.readUTF();
AsyncToggleMessage task = new AsyncToggleMessage(plugin, fromPlayer, playerName, true);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else if ("OFF".equals(subchannel)) {
String playerName = dataInput.readUTF();
if (playerName.equals(forPlayer.getName()) && plugin.getConfig().getBoolean("premium-warning")
&& !plugin.getPendingConfirms().contains(forPlayer.getUniqueId())) {
String message = plugin.getCore().getMessage("premium-warning");
forPlayer.sendMessage(TextComponent.fromLegacyText(message));
plugin.getPendingConfirms().add(forPlayer.getUniqueId());
return;
}
AsyncToggleMessage task = new AsyncToggleMessage(plugin, fromPlayer, playerName, false);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else if ("SUCCESS".equals(subchannel)) {
if (fromPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
PlayerProfile playerProfile = plugin.getCore().getStorage().loadProfile(fromPlayer.getName());
BungeeLoginSession loginSession = plugin.getSession().get(fromPlayer.getPendingConnection());
loginSession.setRegistered(true);
plugin.getPendingConfirms().remove(forPlayer.getUniqueId());
AsyncToggleMessage task = new AsyncToggleMessage(plugin, forPlayer, playerName, true);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else if ("OFF".equals(subchannel)) {
String playerName = dataInput.readUTF();
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
//we override this in the loginevent
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
AsyncToggleMessage task = new AsyncToggleMessage(plugin, 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
BungeeLoginSession loginSession = plugin.getSession().get(forPlayer.getPendingConnection());
PlayerProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
}
}
}
}
}
});
}
}

View File

@@ -40,14 +40,15 @@ public class AsyncPremiumCheck implements Runnable {
}
if (premiumUUID == null
|| checkNameChange(premiumUUID, connection, username)
|| checkPremiumName(username, connection, profile)) {
|| (!checkNameChange(premiumUUID, connection, username)
&& !checkPremiumName(username, connection, profile))) {
//nothing detected the player as premium -> start a cracked session
plugin.getSession().put(connection, new BungeeLoginSession(username, false, profile));
}
} else if (profile.isPremium()) {
requestPremiumLogin(connection, profile, username, true);
} else {
//Cracked session
plugin.getSession().put(connection, new BungeeLoginSession(username, false, profile));
}
} catch (Exception ex) {

View File

@@ -8,6 +8,7 @@ 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.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@@ -27,38 +28,63 @@ public class ForceLoginTask implements Runnable {
@Override
public void run() {
PendingConnection pendingConnection = player.getPendingConnection();
BungeeLoginSession session = plugin.getSession().get(pendingConnection);
PlayerProfile playerProfile = session.getProfile();
try {
PendingConnection pendingConnection = player.getPendingConnection();
BungeeLoginSession session = plugin.getSession().get(pendingConnection);
if (player.isConnected()) {
return;
}
if (session == null || !player.isConnected()) {
plugin.getLogger().log(Level.FINE, "Invalid session player {0} proparly left the server", player);
return;
}
//force login only on success
if (pendingConnection.isOnlineMode()) {
boolean autoRegister = session.needsRegistration();
PlayerProfile playerProfile = session.getProfile();
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
if (authPlugin == null) {
//save will happen on success message from bukkit
sendBukkitLoginNotification(autoRegister);
} else if (session.needsRegistration()) {
String password = plugin.generateStringPassword();
if (authPlugin.forceRegister(player, password)) {
//force login only on success
if (pendingConnection.isOnlineMode()) {
boolean autoRegister = session.needsRegistration();
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
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);
}
} else if (authPlugin.forceLogin(player)) {
//save will happen on success message from bukkit
sendBukkitLoginNotification(autoRegister);
}
} 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, BungeeAuthPlugin authPlugin) {
String password = plugin.generateStringPassword();
if (session.isAlreadySaved() || 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(message);
}
}
}
private void forceLogin(BungeeLoginSession session, BungeeAuthPlugin authPlugin) {
if (session.isAlreadySaved() || 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(message);
}
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.5.2</version>
<version>1.7.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -19,7 +19,7 @@
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.4.6</version>
<version>2.4.7</version>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->

View File

@@ -11,14 +11,14 @@ import java.sql.Statement;
import java.util.UUID;
import java.util.logging.Level;
public class Storage {
public class AuthStorage {
private static final String PREMIUM_TABLE = "premium";
private final FastLoginCore core;
private final HikariDataSource dataSource;
public Storage(FastLoginCore core, String driver, String host, int port, String databasePath
public AuthStorage(FastLoginCore core, String driver, String host, int port, String databasePath
, String user, String pass) {
this.core = core;
@@ -30,8 +30,6 @@ public class Storage {
databasePath = databasePath.replace("{pluginDir}", core.getDataFolder().getAbsolutePath());
databaseConfig.setThreadFactory(core.getThreadFactory());
String jdbcUrl = "jdbc:";
if (driver.contains("sqlite")) {
jdbcUrl += "sqlite" + "://" + databasePath;
@@ -50,6 +48,7 @@ public class Storage {
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), "
@@ -57,7 +56,6 @@ public class Storage {
+ "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) "
+ ")";
@@ -67,6 +65,34 @@ public class Storage {
}
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) {
core.getLogger().log(Level.FINE, "Error dropping unique uuid index", sqlEx);
}
try {
createStmt.executeUpdate("CREATE INDEX uuid_idx on premium (UUID)");
} catch (SQLException sqlEx) {
core.getLogger().log(Level.FINE, "Error creating uuid index", sqlEx);
}
} finally {
closeQuietly(con);
closeQuietly(createStmt);
@@ -79,7 +105,7 @@ public class Storage {
ResultSet resultSet = null;
try {
con = dataSource.getConnection();
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE Name=?");
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE Name=? LIMIT 1");
loadStmt.setString(1, name);
resultSet = loadStmt.executeQuery();
@@ -120,7 +146,7 @@ public class Storage {
ResultSet resultSet = null;
try {
con = dataSource.getConnection();
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE UUID=?");
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE UUID=? LIMIT 1");
loadStmt.setString(1, uuid.toString().replace("-", ""));
resultSet = loadStmt.executeQuery();
@@ -155,12 +181,13 @@ public class Storage {
con = dataSource.getConnection();
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
//User was authenticated with a premium authentication, so it's possible that the player is premium
if (uuid != null) {
//User was authenticated with a premium authentication, so it's possible that the player is premium
updateStmt = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET NAME=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP"
+ " WHERE UUID=? AND PREMIUM=1");
+ " WHERE UUID=?");
updateStmt.setString(1, playerProfile.getPlayerName());
updateStmt.setString(2, playerProfile.getLastIp());
@@ -175,8 +202,7 @@ public class Storage {
}
saveStmt = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) "
, Statement.RETURN_GENERATED_KEYS);
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) ", Statement.RETURN_GENERATED_KEYS);
if (uuid == null) {
saveStmt.setString(1, null);

View File

@@ -0,0 +1,80 @@
package com.github.games647.fastlogin.core;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
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 final Object lock = new Object();
private int id;
public BalancedSSLFactory(SSLSocketFactory oldFactory, Set<InetAddress> localAddresses) {
this.oldFactory = oldFactory;
this.localAddresses = new ArrayList<>(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 next;
synchronized (lock) {
next = id;
id++;
if (next == Integer.MAX_VALUE) {
id = 0;
}
}
int index = next % localAddresses.size();
return localAddresses.get(index);
}
}

View File

@@ -20,7 +20,7 @@ public abstract class FastLoginCore {
protected final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private MojangApiConnector mojangApiConnector;
private Storage storage;
private AuthStorage storage;
public void setMojangApiConnector(MojangApiConnector mojangApiConnector) {
this.mojangApiConnector = mojangApiConnector;
@@ -30,7 +30,7 @@ public abstract class FastLoginCore {
return mojangApiConnector;
}
public Storage getStorage() {
public AuthStorage getStorage() {
return storage;
}
@@ -49,7 +49,7 @@ public abstract class FastLoginCore {
public abstract void loadConfig();
public boolean setupDatabase(String driver, String host, int port, String database, String user, String password) {
storage = new Storage(this, driver, host, port, database, user, password);
storage = new AuthStorage(this, driver, host, port, database, user, password);
try {
storage.createTables();
return true;

View File

@@ -4,29 +4,76 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashSet;
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 = 1 * 1_000;
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);
protected final FastLoginCore plugin;
private final ConcurrentMap<Object, Object> requests;
private final BalancedSSLFactory sslFactory;
private final int rateLimit;
private long lastRateLimit;
public MojangApiConnector(FastLoginCore plugin) {
this.plugin = plugin;
protected final Logger logger;
public MojangApiConnector(ConcurrentMap<Object, Object> requests, Logger logger, List<String> localAddresses
, int rateLimit) {
this.logger = logger;
this.requests = requests;
if (rateLimit > 600) {
this.rateLimit = 600;
} else {
this.rateLimit = rateLimit;
}
if (localAddresses.isEmpty()) {
this.sslFactory = null;
} else {
Set<InetAddress> addresses = new HashSet<>();
for (String localAddress : localAddresses) {
try {
InetAddress address = InetAddress.getByName(localAddress);
if (!address.isAnyLocalAddress()) {
logger.log(Level.WARNING, "Submitted IP-Address is not local", 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);
}
}
/**
@@ -37,19 +84,31 @@ public abstract class MojangApiConnector {
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
// 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 {
HttpURLConnection connection = getConnection(UUID_LINK + playerName);
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 != null && !line.equals("null")) {
return 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) {
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", 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
}
@@ -57,18 +116,49 @@ public abstract class MojangApiConnector {
return null;
}
public UUID getUUIDFromAPI(String playerName) {
try {
HttpURLConnection httpConnection = (HttpURLConnection) new URL(MCAPI_UUID_URL + playerName).openConnection();
httpConnection.addRequestProperty("Content-Type", "application/json");
httpConnection.setRequestProperty("User-Agent", USER_AGENT);
if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
//cracked
return null;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(httpConnection.getInputStream()));
StringBuilder inputBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
inputBuilder.append(line);
}
String input = inputBuilder.toString();
return getUUIDFromJson(input);
} catch (IOException iOException) {
logger.log(Level.SEVERE, "Tried converting name->uuid from third-party api", iOException);
}
return null;
}
public abstract boolean hasJoinedServer(Object session, String serverId);
protected abstract UUID getUUIDFromJson(String json);
protected HttpURLConnection getConnection(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
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

@@ -19,14 +19,29 @@ public class AutoInImporter extends Importer {
try {
con = source.getConnection();
stmt = con.createStatement();
int importedRows = stmt.executeUpdate("INSERT INTO " + targetTable + " SELECT"
int importedRows = stmt.executeUpdate("INSERT INTO " + targetTable + " (Name, Premium, LastIp, UUID) SELECT"
+ " name AS Name,"
+ " enabledLogin AS Premium,"
/* Enable premium authentication only for those who want to be auto logged in, so
they have their cracked protection disabled */
+ " !protection AND premium AS Premium,"
+ " '' AS LastIp,"
/* Remove the dashes - returns null if puuid is null too */
+ " REPLACE(puuid, '-', '') AS UUID"
+ " FROM " + USER_TABLE
+ " JOIN " + UUID_TABLE
+ " ON " + UUID_TABLE + ".id = " + UUID_TABLE + ".nickname_id");
/* Get the premium uuid */
+ " LEFT JOIN " + " ("
/* Prevent duplicates */
+ "SELECT * FROM " + UUID_TABLE + " GROUP BY nickname_id"
+ ") uuids"
+ " ON " + USER_TABLE + ".id = uuids.nickname_id");
/* 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 */
stmt.executeUpdate("UPDATE `premium` SET `UUID`=NULL WHERE PREMIUM=0");
return importedRows;
} finally {
closeQuietly(stmt);

View File

@@ -100,6 +100,35 @@ nameChangeCheck: false
# 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
# Database configuration
# Recommened is the use of MariaDB (a better version of MySQL)

View File

@@ -27,12 +27,18 @@ add-premium: '&2Added to the list of premium players'
# 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'
@@ -74,6 +80,13 @@ 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 ================================

View File

@@ -8,7 +8,7 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.5.2</version>
<version>1.7.1</version>
<inceptionYear>2015</inceptionYear>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.5.2</version>
<version>1.7.1</version>
<relativePath>../pom.xml</relativePath>
</parent>