mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-12-23 15:18:09 +01:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596caa0573 | ||
|
|
fe4331298f | ||
|
|
a67d84ef3f | ||
|
|
71362dfd7d | ||
|
|
fcd98fce43 | ||
|
|
6c1c4e7286 | ||
|
|
164fb735d6 | ||
|
|
fa1b0970a5 | ||
|
|
974bf498fc | ||
|
|
27c04ff08f | ||
|
|
fb357424e6 | ||
|
|
c73bb70256 | ||
|
|
dc395cdc3f | ||
|
|
f7626ab969 | ||
|
|
5f9802d589 | ||
|
|
642c1621ad | ||
|
|
eb965d5a48 | ||
|
|
457bc9cf47 | ||
|
|
2ab3c6b77c | ||
|
|
f27bad02d3 | ||
|
|
9334296beb | ||
|
|
fd9940e6f0 | ||
|
|
0745957e79 | ||
|
|
bb2e60f6e1 | ||
|
|
d15861b8e5 | ||
|
|
b84b340a77 | ||
|
|
c50249edea | ||
|
|
757ddb905a | ||
|
|
9914b7f358 | ||
|
|
bba4eb4eec | ||
|
|
2b16f3341f | ||
|
|
167ce66057 | ||
|
|
8d1021e44c | ||
|
|
a811a741f5 | ||
|
|
a6348766b3 | ||
|
|
22dcc50950 | ||
|
|
bd3494eed0 | ||
|
|
1aba9a0f3b | ||
|
|
6faf00e1bf | ||
|
|
0d89614f3c | ||
|
|
b009658eea | ||
|
|
2881689f09 | ||
|
|
6d1a97fd32 | ||
|
|
b74faa2fd5 | ||
|
|
4800a88886 | ||
|
|
92c9ab5b76 | ||
|
|
d90e3fdb44 | ||
|
|
8abbb8f07c | ||
|
|
f04a44b1d2 | ||
|
|
1a66121977 | ||
|
|
413a0325f8 | ||
|
|
9fc7e0bf43 | ||
|
|
ac8bcb1758 | ||
|
|
bebcb3e9de | ||
|
|
0b899f61a8 | ||
|
|
7733135ce4 | ||
|
|
be89eec23b | ||
|
|
679060d4e9 | ||
|
|
f6aa064835 | ||
|
|
de4b73c3bd | ||
|
|
ac15829dcc | ||
|
|
0b709997a4 | ||
|
|
8809875ca4 | ||
|
|
aa30c070b9 | ||
|
|
51d0aefbf3 | ||
|
|
cb876a52bd | ||
|
|
3e844be65d | ||
|
|
dce95cf0d0 | ||
|
|
81eeaeae83 | ||
|
|
6b1542de88 | ||
|
|
99b7367366 | ||
|
|
961b144efb | ||
|
|
dcd06ad613 | ||
|
|
c4c043e1c5 | ||
|
|
87aa9dd668 | ||
|
|
2838c06ab3 | ||
|
|
ae58e0539a | ||
|
|
624745728f | ||
|
|
d0287ec2b4 | ||
|
|
e6a4af92cc | ||
|
|
8f3920fa99 | ||
|
|
a723b2ddd3 | ||
|
|
5cf67127c7 | ||
|
|
e5309b9fa1 | ||
|
|
e439126294 | ||
|
|
59703bac4e | ||
|
|
bfaf390463 | ||
|
|
9e06fd7735 | ||
|
|
d56a0f9ff1 | ||
|
|
96fe190cac | ||
|
|
d4f5b547d4 | ||
|
|
67a4f41056 | ||
|
|
5174a84a17 | ||
|
|
a7b164b513 | ||
|
|
ffa5059c67 | ||
|
|
dfe37dfc1b | ||
|
|
6edd40742d | ||
|
|
b697dc6655 | ||
|
|
36974450ce | ||
|
|
57a59045ce | ||
|
|
11cc4eabc0 | ||
|
|
53e02d5457 | ||
|
|
0f85674ec1 | ||
|
|
378ab09bc8 | ||
|
|
740b11b434 | ||
|
|
77f0184899 | ||
|
|
2885daf8b9 | ||
|
|
1e128d12f5 | ||
|
|
f2a8446c8d | ||
|
|
cc8c49e25b | ||
|
|
25e182148f | ||
|
|
f00608c321 | ||
|
|
b86bdf5f23 | ||
|
|
9a30a0b299 | ||
|
|
fd3b1ed8b6 | ||
|
|
f3e675e547 | ||
|
|
0967f31b9a | ||
|
|
8cb4621055 | ||
|
|
f610001c9b | ||
|
|
dd386408d1 | ||
|
|
10bfd279d6 | ||
|
|
5608821fe3 | ||
|
|
9c0ad7d70c | ||
|
|
099b8e5d0a | ||
|
|
b4ade882be | ||
|
|
4a3cb42152 | ||
|
|
8fc5050e8e | ||
|
|
015739fe4c | ||
|
|
b2ae46a90a | ||
|
|
353cd17823 | ||
|
|
157b8499a9 | ||
|
|
bd46dae086 | ||
|
|
eacbb1ed76 | ||
|
|
e389433138 | ||
|
|
d1b2fe8865 | ||
|
|
3b4c4a1c79 | ||
|
|
b22df62f90 | ||
|
|
d118de8649 | ||
|
|
f8c10d6890 | ||
|
|
c3f8e59a9a | ||
|
|
834818bb7a | ||
|
|
fa46dc690b | ||
|
|
fdc2772f38 | ||
|
|
53af09ae34 | ||
|
|
f6f6aaf1de | ||
|
|
c0ef95e808 | ||
|
|
cb129547f5 | ||
|
|
eb394b5f60 | ||
|
|
aebbc84621 | ||
|
|
0eee6ba2be | ||
|
|
7e2057a7a2 | ||
|
|
800f077be0 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
# netbeans
|
||||
/nbproject
|
||||
nb-configuration.xml
|
||||
/bukkit/nbproject/
|
||||
|
||||
# maven
|
||||
/target
|
||||
@@ -40,3 +41,9 @@ gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Project module targets
|
||||
bukkit/target
|
||||
universal/target
|
||||
bungee/target
|
||||
core/target
|
||||
|
||||
15
.travis.yml
Normal file
15
.travis.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Use https://travis-ci.org/ for automatic tests
|
||||
|
||||
# speed up testing http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
# This is a java project
|
||||
language: java
|
||||
|
||||
script: mvn compile test
|
||||
|
||||
# We run on 7+
|
||||
jdk:
|
||||
- openjdk7
|
||||
- oraclejdk7
|
||||
- oraclejdk8
|
||||
195
CHANGELOG.md
Normal file
195
CHANGELOG.md
Normal file
@@ -0,0 +1,195 @@
|
||||
######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
|
||||
|
||||
* Fixed BungeeCord support by correctly saving the proxy ids
|
||||
|
||||
######1.5
|
||||
|
||||
* Added localization
|
||||
* Fixed NPE on premium name check if it's pure cracked player
|
||||
* Fixed NPE in BungeeCord on cracked login for existing players
|
||||
* Fixed saving of existing cracked players
|
||||
|
||||
######1.4
|
||||
|
||||
* Added Bungee setAuthPlugin method
|
||||
* Added nameChangeCheck
|
||||
* Multiple BungeeCord support
|
||||
|
||||
######1.3.1
|
||||
|
||||
* Prevent thread create violation in BungeeCord
|
||||
|
||||
######1.3
|
||||
|
||||
* Added support for AuthMe 3.X
|
||||
* Fixed premium logins if the server is not fully started
|
||||
* Added other command argument to /premium and /cracked
|
||||
* Added support for LogIt
|
||||
* Fixed 1.7 Minecraft support by removing guava 11+ only features -> Cauldron support
|
||||
* Fixed BungeeCord support in Cauldron
|
||||
|
||||
######1.2.1
|
||||
|
||||
* Fix premium status change notification message on BungeeCord
|
||||
|
||||
######1.2
|
||||
|
||||
* Fix race condition in BungeeCord
|
||||
* Fix dead lock in xAuth
|
||||
* Added API methods for plugins to set their own password generator
|
||||
* Added API methods for plugins to set their own auth plugin hook
|
||||
=> Added support for AdvancedLogin
|
||||
|
||||
######1.1
|
||||
|
||||
* Make the configuration options also work under BungeeCord (premiumUUID, forwardSkin)
|
||||
* Catch configuration loading exception if it's not spigot build
|
||||
* Fix config loading for older Spigot builds
|
||||
|
||||
######1.0
|
||||
|
||||
* Massive refactor to handle errors on force actions safely
|
||||
* force Methods now runs async too
|
||||
* force methods now returns a boolean to reflect if the method was successful
|
||||
* isRegistered method should now throw an exception if the plugin was unable to query the requested data
|
||||
|
||||
######0.8
|
||||
|
||||
* Fixed BungeeCord support for the Bukkit module
|
||||
* Added database storage to save the premium state
|
||||
* Fix logical error on /premium (Thanks to @NorbiPeti)
|
||||
* Fixed issues with host lookup from hosts file (Thanks to @NorbiPeti)
|
||||
* Remove handshake listener because it creates errors on some systems
|
||||
|
||||
######0.7
|
||||
|
||||
* Added BungeeAuth support
|
||||
* Added /premium [player] command with optional player parameter
|
||||
* Added a check if the player is already on the premium list
|
||||
* Added a forwardSkin config option
|
||||
* Added premium UUID support
|
||||
* Updated to the newest changes of Spigot
|
||||
* Removes the need of an Bukkit auth plugin if you use a bungeecord one
|
||||
* Optimize performance and thread-safety
|
||||
* Fixed BungeeCord support
|
||||
* Changed config option autologin to autoregister to clarify the usage
|
||||
|
||||
######0.6
|
||||
|
||||
* Fixed 1.9 bugs
|
||||
* Added UltraAuth support
|
||||
|
||||
######0.5
|
||||
|
||||
* Added unpremium command
|
||||
* Added autologin - See config
|
||||
* Added config
|
||||
* Added isRegistered API method
|
||||
* Added forceRegister API method
|
||||
|
||||
* Fixed CrazyLogin player data restore -> Fixes memory leaks with this plugin
|
||||
* Fixed premium name check to protocolsupport
|
||||
* Improved permissions management
|
||||
|
||||
######0.4
|
||||
|
||||
* Added forward premium skin
|
||||
* Added plugin support for protocolsupport
|
||||
|
||||
######0.3.2
|
||||
|
||||
* Run packet readers in a different thread (separated from the Netty I/O Thread)
|
||||
-> Improves performance
|
||||
* Fixed Plugin disable if the server is in online mode but have to be in offline mode
|
||||
|
||||
######0.3.1
|
||||
|
||||
* Improved BungeeCord security
|
||||
|
||||
#####0.3
|
||||
|
||||
* Added BungeeCord support
|
||||
* Decrease timeout checks in order to fail faster on connection problems
|
||||
* Code style improvements
|
||||
|
||||
######0.2.4
|
||||
|
||||
* Fixed NPE on invalid sessions
|
||||
* Improved security by generating a randomized serverId
|
||||
* Removed /premium [player] because it's safer for premium players who join without registration
|
||||
|
||||
######0.2.3
|
||||
|
||||
* Remove useless AuthMe forcelogin code
|
||||
* Send a kick message to the client instead of just "Disconnect"
|
||||
* Reformat source code
|
||||
* Fix thread safety for fake start packets (Bukkit.getOfflinePlayer doesn't look like to be thread-safe)
|
||||
* Added more documentation
|
||||
|
||||
######0.2.2
|
||||
|
||||
* Compile project with Java 7 :(
|
||||
|
||||
######0.2.1
|
||||
* A couple of security fixes (premium players cannot longer steal the account of a cracked account)
|
||||
* Added a /premium command to mark you as premium player
|
||||
|
||||
#####0.2
|
||||
|
||||
* Added support for CrazyLogin and LoginSecurity
|
||||
* Now minecraft version independent
|
||||
* Added debug logging
|
||||
* Code clean up
|
||||
* More state validation
|
||||
* Added better error handling
|
||||
|
||||
#####0.1
|
||||
* First release
|
||||
186
README.md
186
README.md
@@ -1,13 +1,179 @@
|
||||
# FastLogin
|
||||
|
||||
Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentification.
|
||||
[](https://travis-ci.org/games647/FastLogin)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8ZBULMAPN7MZC)
|
||||
|
||||
Requirements:
|
||||
* [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
|
||||
* Bukkit 1.8.8
|
||||
* Java 8 or above
|
||||
* An auth plugin. Supported Plugins:
|
||||
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
||||
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
||||
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
|
||||
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
|
||||
Checks if a Minecraft player has a paid account (premium). If so, they can skip offline authentication (auth plugins).
|
||||
So they don't need to enter passwords. This is also called auto login (auto-login).
|
||||
|
||||
###Features:
|
||||
|
||||
* Detect paid accounts from others
|
||||
* Automatically login paid accounts (premium)
|
||||
* Support various of auth plugins
|
||||
* Cauldron support
|
||||
* Forge/Sponge message support
|
||||
* Premium UUID support
|
||||
* Forwards Skins
|
||||
* Detect user name changed and will update the existing database record
|
||||
* BungeeCord support
|
||||
* Auto register new premium players
|
||||
* Plugin: ProtocolSupport is supported and can be used as an alternative to ProtocolLib
|
||||
* No client modifications needed
|
||||
* Good performance by using async non blocking operations
|
||||
* Locale messages
|
||||
* Free
|
||||
* Open source
|
||||
|
||||
***
|
||||
|
||||
###Commands:
|
||||
* /premium [player] Label the invoker or the argument as paid account
|
||||
* /cracked [player] Label the invoker or the argument as cracked account
|
||||
|
||||
###Permissions:
|
||||
* fastlogin.bukkit.command.premium
|
||||
* fastlogin.bukkit.command.cracked
|
||||
|
||||
###Requirements:
|
||||
* Plugin: [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/) or [ProtocolSupport](http://www.spigotmc.org/resources/protocolsupport.7201/)
|
||||
* Tested Bukkit/[Spigot](https://www.spigotmc.org) 1.9 (could also work with other versions)
|
||||
* Java 7+
|
||||
* Run Spigot and/or BungeeCord/Waterfall in offline mode (see server.properties or config.yml)
|
||||
* An auth plugin. Supported plugins
|
||||
|
||||
####Bukkit/Spigot/PaperSpigot
|
||||
|
||||
* [AuthMe (both 5.X and 3.X)](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
||||
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
||||
* [LogIt](https://github.com/XziomekX/LogIt)
|
||||
* [AdvancedLogin (Paid)](https://www.spigotmc.org/resources/advancedlogin.10510/)
|
||||
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
|
||||
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
|
||||
* [RoyalAuth](http://dev.bukkit.org/bukkit-plugins/royalauth/)
|
||||
* [UltraAuth](http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/)
|
||||
|
||||
####BungeeCord/Waterfall
|
||||
|
||||
* [BungeeAuth](https://www.spigotmc.org/resources/bungeeauth.493/)
|
||||
|
||||
###Downloads
|
||||
|
||||
https://www.spigotmc.org/resources/fastlogin.14153/history
|
||||
|
||||
***
|
||||
|
||||
###How to install
|
||||
|
||||
####Bukkit/Spigot/PaperSpigot
|
||||
|
||||
1. Download and install ProtocolLib
|
||||
2. Download and install FastLogin
|
||||
3. Set your server in offline mode by setting the value onlinemode in your server.properties to false
|
||||
|
||||
####BungeeCord/Waterfall
|
||||
|
||||
1. Activate BungeeCord in the Spigot configuration
|
||||
2. Restart your server
|
||||
3. Now there is proxy-whitelist file in the FastLogin folder
|
||||
Put your stats id from the BungeeCord config into this file
|
||||
4. Activate ipForward in your BungeeCord config
|
||||
5. Download and Install FastLogin on BungeeCord AND Spigot
|
||||
6. Check your database settings in the config of FastLogin on BungeeCord
|
||||
7. Set your proxy (BungeeCord) in offline mode by setting the value onlinemode in your config.yml to false
|
||||
8. (BungeeCord doesn't support SQLite per default, so you should change the configuration to MySQL or MariaDB)
|
||||
|
||||
***
|
||||
|
||||
###FAQ
|
||||
|
||||
####Index
|
||||
1. [How does Minecraft logins work?](#how-does-minecraft-logins-work)
|
||||
2. [How does this plugin work?](#how-does-this-plugin-work)
|
||||
3. [Why does the plugin require offline mode?](#why-does-the-plugin-require-offline-mode)
|
||||
4. [Can cracked player join with premium usernames?](#can-cracked-player-join-with-premium-usernames)
|
||||
5. [Why do players have to invoke a command?](#why-do-players-have-to-invoke-a-command)
|
||||
6. [What happens if a paid account joins with a used username?](#what-happens-if-a-paid-account-joins-with-a-used-username)
|
||||
7. [Does the plugin have BungeeCord support?](#does-the-plugin-have-bungeecord-support)
|
||||
8. [Could premium players have a premium UUID and Skin?](#could-premium-players-have-a-premium-uuid-and-skin)
|
||||
9. [Is this plugin compatible with Cauldron?](#is-this-plugin-compatible-with-cauldron)
|
||||
|
||||
####How does minecraft logins work?
|
||||
######Online Mode
|
||||
1. Client -> Server: I want to login, here is my username
|
||||
2. Server -> Client: Okay. I'm in online mode so here is my public key for encryption and my serverid
|
||||
3. Client -> Mojang: I'm player "xyz". I want to join a server with that serverid
|
||||
4. Mojang -> Client: Session data checked. You can continue
|
||||
5. Client -> Server: I received a successful response from Mojang. Heres our shared secret key
|
||||
6. Server -> Mojang: Does the player "xyz" with this shared secret key has a valid account to join me?
|
||||
7. Mojang -> Server: Yes, the player has the following additionally properties (UUID, Skin)
|
||||
8. Client and Server: encrypt all following communication packet
|
||||
9. Server -> Client: Everything checked you can play now
|
||||
|
||||
|
||||
######Offline Mode
|
||||
In offline mode step 2-7 is skipped. So a login request is directly followed by 8.
|
||||
|
||||
######More details
|
||||
http://wiki.vg/Protocol#Login
|
||||
|
||||
####How does this plugin work?
|
||||
By using ProtocolLib, this plugin works as a proxy between the client and server. This plugin will fake that the server
|
||||
runs in online mode. It does everything an online mode server would do. This will be for example, generating keys or
|
||||
checking for valid sessions. Because everything is the same compared to an offline mode login after an encrypted
|
||||
connection, we will intercept only **login** packets of **premium** players.
|
||||
|
||||
1. Player is connecting to the server.
|
||||
2. Plugin checks if the username we received activated the fast login method (i.e. using command)
|
||||
3. Run a check if the username is currently used by a paid account.
|
||||
(We don't know yet if the client connecting is premium)
|
||||
4. Request an Mojang Session Server authentication
|
||||
5. On response check if all data is correct
|
||||
6. Encrypt the connection
|
||||
7. On success intercept all related login packets and fake a new login packet as a normal offline login
|
||||
|
||||
####Why does the plugin require offline mode?
|
||||
1. As you can see in the question "how does minecraft login works", offline mode is equivalent to online mode except of
|
||||
the encryption and session checks on login. So we can intercept and cancel the first packets for premium players and
|
||||
enable an encrypted connection. Then we send a new fake packet in order to pretend that this a new login request from
|
||||
a offline mode player. The server will handle the rest.
|
||||
2. Some plugins check if the server is in online mode. If so, they could process the real offline (cracked) accounts
|
||||
incorrectly. For example, a plugin tries to fetch the UUID from Mojang, but the name of the player is not associated to
|
||||
a paid account.
|
||||
3. Servers, who allow cracked players and just speed up logins for premium players, are **already** in offline mode.
|
||||
|
||||
####Can cracked player join with premium usernames?
|
||||
Yes, indeed. Therefore the command for toggling the fast login method exists.
|
||||
|
||||
####Why do players have to invoke a command?
|
||||
1. It's a secure way to make sure a person with a paid account cannot steal the account
|
||||
of a cracked player that has the same username. The player have to proof first that it's his own account.
|
||||
2. We only receive the username from the player on login. We could check if that username is associated
|
||||
to a paid account but if we request a online mode login from a cracked player (who uses a username from
|
||||
a paid account), the player will disconnect with the reason "bad login" or "Invalid session". There is no way to change
|
||||
that message on the server side (without client modifications), because it's a connection between the Client and the
|
||||
Sessionserver.
|
||||
3. If a premium player would skip registration too, a player of a cracked account could later still register the
|
||||
account and would claim and steal the account from the premium player. Because commands cannot be invoked unless the
|
||||
player has a account or is logged in, protects this method also premium players
|
||||
|
||||
###What happens if a paid account joins with a used username?
|
||||
The player on the server have to activate the feature of this plugin by command. If a person buys the username
|
||||
of his own account, it's still secured. A normal offline mode login makes sure he's the owner of the server account
|
||||
and Mojang account. Then the command can be executed. So someone different cannot steal the account of cracked player
|
||||
by buying the username.
|
||||
|
||||
####Does the plugin have BungeeCord support?
|
||||
Yes it has. See the how to install above.
|
||||
|
||||
####Could premium players have a premium UUID and Skin?
|
||||
Since 0.7 both features are implemented. You can check the config.yml in order to activate it.
|
||||
|
||||
####Is this plugin compatible with Cauldron?
|
||||
It's not tested yet, but all needed methods also exists in Cauldron so it could work together.
|
||||
|
||||
***
|
||||
|
||||
###Useful Links:
|
||||
* [Login Protocol](http://wiki.vg/Protocol#Login)
|
||||
* [Protocol Encryption](http://wiki.vg/Protocol_Encryption)
|
||||
BIN
bukkit/lib/ProtocolSupport b337.jar
Normal file
BIN
bukkit/lib/ProtocolSupport b337.jar
Normal file
Binary file not shown.
BIN
bukkit/lib/UltraAuth v2.0.2.jar
Normal file
BIN
bukkit/lib/UltraAuth v2.0.2.jar
Normal file
Binary file not shown.
199
bukkit/pom.xml
Normal file
199
bukkit/pom.xml
Normal file
@@ -0,0 +1,199 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin.bukkit</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLoginBukkit</name>
|
||||
|
||||
<repositories>
|
||||
<!--Bukkit-Server-API -->
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--LoginSecurity-->
|
||||
<repository>
|
||||
<id>lenis0012-repo</id>
|
||||
<url>http://ci.lenis0012.com/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--ProtocolLib-->
|
||||
<repository>
|
||||
<id>dmulloy2-repo</id>
|
||||
<url>http://repo.dmulloy2.net/content/groups/public/</url>
|
||||
</repository>
|
||||
|
||||
<!--Authme Reloaded-->
|
||||
<repository>
|
||||
<id>xephi-repo</id>
|
||||
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--xAuth-->
|
||||
<repository>
|
||||
<id>luricos.de-repo</id>
|
||||
<url>http://repo.luricos.de/bukkit-plugins/</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>logit-only-repo</id>
|
||||
<url>http://ci.ac3-servers.eu/job/LogIt-Classic/2/maven-repository/repository/</url>
|
||||
</repository>
|
||||
|
||||
<!--Github automatic maven builds-->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Server API-->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.10-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Library for listening and sending Minecraft packets-->
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>protcolsupport</groupId>
|
||||
<artifactId>ProtocolSupport</artifactId>
|
||||
<version>Build-337</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/ProtocolSupport b337.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!--Login Plugins-->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.2-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<!--Oringal owner is lenis, but maven always uses the newest version-->
|
||||
<groupId>com.github.lenis0012</groupId>
|
||||
<artifactId>LoginSecurity-2</artifactId>
|
||||
<!--Old version 2.0 -->
|
||||
<version>-9c09e73b7f-1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</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>
|
||||
<version>SNAPSHOT</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.RoyalDev</groupId>
|
||||
<artifactId>RoyalAuth</artifactId>
|
||||
<version>-e21354a9b7-1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.luricos.bukkit</groupId>
|
||||
<artifactId>xAuth</artifactId>
|
||||
<version>2.6</version>
|
||||
<optional>true</optional>
|
||||
<!--These artifacts produce conflicts on downloading-->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!--No maven repository :(-->
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyCore</artifactId>
|
||||
<version>10.7.7</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyCore v10.7.7.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyLogin</artifactId>
|
||||
<version>7.23</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ultraauth</groupId>
|
||||
<artifactId>ultraauth</artifactId>
|
||||
<version>2.0.2</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/UltraAuth v2.0.2.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public class BukkitCore extends FastLoginCore {
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
public BukkitCore(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDataFolder() {
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return plugin.getLogger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThreadFactory getThreadFactory() {
|
||||
String pluginName = plugin.getName();
|
||||
return new ThreadFactoryBuilder()
|
||||
.setNameFormat(pluginName + " Database Pool Thread #%1$d")
|
||||
//Hikari create daemons by default
|
||||
.setDaemon(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMessages() {
|
||||
plugin.saveResource("messages.yml", false);
|
||||
|
||||
File messageFile = new File(plugin.getDataFolder(), "messages.yml");
|
||||
YamlConfiguration messageConfig = YamlConfiguration.loadConfiguration(messageFile);
|
||||
|
||||
InputStreamReader defaultReader = new InputStreamReader(plugin.getResource("messages.yml"), Charsets.UTF_8);
|
||||
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(defaultReader);
|
||||
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()) {
|
||||
localeMessages.remove(key);
|
||||
} else {
|
||||
localeMessages.put(key, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadConfig() {
|
||||
plugin.saveDefaultConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.github.games647.fastlogin.core.LoginSession;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
|
||||
/**
|
||||
* Represents a client connecting to the server.
|
||||
*
|
||||
* This session is invalid if the player disconnects or the login was successful
|
||||
*/
|
||||
public class BukkitLoginSession extends LoginSession {
|
||||
|
||||
private final String serverId;
|
||||
private final byte[] verifyToken;
|
||||
|
||||
private UUID uuid;
|
||||
private boolean verified;
|
||||
|
||||
private String encodedSkinData;
|
||||
private String skinSignature;
|
||||
|
||||
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
|
||||
, PlayerProfile profile) {
|
||||
super(username, registered, profile);
|
||||
|
||||
this.serverId = serverId;
|
||||
this.verifyToken = ArrayUtils.clone(verifyToken);
|
||||
}
|
||||
|
||||
//available for bungeecord
|
||||
public BukkitLoginSession(String username, boolean registered) {
|
||||
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, registered, null);
|
||||
}
|
||||
|
||||
//cracked player
|
||||
public BukkitLoginSession(String username, PlayerProfile profile) {
|
||||
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the random generated server id. This makes sure the request sent from the client is just for this server.
|
||||
*
|
||||
* See this for details http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
|
||||
*
|
||||
* Empty if it's a BungeeCord connection
|
||||
*
|
||||
* @return random generated server id
|
||||
*/
|
||||
public String getServerId() {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the verify token the server sent to the client.
|
||||
*
|
||||
* Empty if it's a BungeeCord connection
|
||||
*
|
||||
* @return the verify token from the server
|
||||
*/
|
||||
public byte[] getVerifyToken() {
|
||||
return ArrayUtils.clone(verifyToken);
|
||||
}
|
||||
|
||||
public synchronized String getEncodedSkinData() {
|
||||
return encodedSkinData;
|
||||
}
|
||||
|
||||
public synchronized String getSkinSignature() {
|
||||
return skinSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the premium skin property which was retrieved by the session server
|
||||
*
|
||||
* @param encodedData
|
||||
* @param skinSignature
|
||||
*/
|
||||
public synchronized void setSkin(String encodedData, String skinSignature) {
|
||||
this.encodedSkinData = encodedData;
|
||||
this.skinSignature = skinSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the player has a premium (paid account) account and valid session
|
||||
*
|
||||
* @param verified whether the player has valid session
|
||||
*/
|
||||
public synchronized void setVerified(boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the premium UUID of this player
|
||||
*
|
||||
* @return the premium UUID or null if not fetched
|
||||
*/
|
||||
public synchronized UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the online UUID if it's fetched
|
||||
*
|
||||
* @param uuid premium UUID
|
||||
*/
|
||||
public synchronized void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the player has a premium (paid account) account and valid session
|
||||
*
|
||||
* @return whether the player has a valid session
|
||||
*/
|
||||
public synchronized boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class DefaultPasswordGenerator implements PasswordGenerator {
|
||||
|
||||
@Override
|
||||
public String getRandomPassword(Player player) {
|
||||
return RandomStringUtils.random(8, true, true);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,48 @@
|
||||
package com.github.games647.fastlogin;
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Encryption and decryption minecraft util for connection between servers
|
||||
* and paid minecraft account clients.
|
||||
*
|
||||
* Source: https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java
|
||||
*
|
||||
* Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8
|
||||
*/
|
||||
public class Encryption {
|
||||
public class EncryptionUtil {
|
||||
|
||||
public static KeyPair generateKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
|
||||
keypairgenerator.initialize(1024);
|
||||
return keypairgenerator.generateKeyPair();
|
||||
keyPairGenerator.initialize(1_024);
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
|
||||
//Should be existing in every vm
|
||||
return null;
|
||||
throw new ExceptionInInitializerError(nosuchalgorithmexception);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getServerIdHash(String serverId, PublicKey publickey, SecretKey secretkey) {
|
||||
public static byte[] getServerIdHash(String serverId, PublicKey publicKey, SecretKey secretKey) {
|
||||
return digestOperation("SHA-1"
|
||||
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretkey.getEncoded(), publickey.getEncoded()});
|
||||
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded()});
|
||||
}
|
||||
|
||||
private static byte[] digestOperation(String algo, byte[]... content) {
|
||||
@@ -61,26 +59,26 @@ public class Encryption {
|
||||
}
|
||||
}
|
||||
|
||||
public static PublicKey decodePublicKey(byte[] encodedKey) {
|
||||
try {
|
||||
X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
|
||||
KeyFactory keyfactory = KeyFactory.getInstance("RSA");
|
||||
// public static PublicKey decodePublicKey(byte[] encodedKey) {
|
||||
// try {
|
||||
// KeyFactory keyfactory = KeyFactory.getInstance("RSA");
|
||||
//
|
||||
// X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
|
||||
// return keyfactory.generatePublic(x509encodedkeyspec);
|
||||
// } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
|
||||
// //ignore
|
||||
// }
|
||||
//
|
||||
// System.err.println("Public key reconstitute failed!");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return keyfactory.generatePublic(x509encodedkeyspec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
|
||||
;
|
||||
}
|
||||
|
||||
System.err.println("Public key reconstitute failed!");
|
||||
return null;
|
||||
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] encryptedSharedKey) {
|
||||
return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES");
|
||||
}
|
||||
|
||||
public static SecretKey decryptSharedKey(PrivateKey privatekey, byte[] encryptedSharedKey) {
|
||||
return new SecretKeySpec(decryptData(privatekey, encryptedSharedKey), "AES");
|
||||
}
|
||||
|
||||
public static byte[] decryptData(Key key, byte[] abyte) {
|
||||
return cipherOperation(Cipher.DECRYPT_MODE, key, abyte);
|
||||
public static byte[] decryptData(Key key, byte[] data) {
|
||||
return cipherOperation(Cipher.DECRYPT_MODE, key, data);
|
||||
}
|
||||
|
||||
private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
|
||||
@@ -107,19 +105,19 @@ public class Encryption {
|
||||
System.err.println("Cipher creation failed!");
|
||||
return null;
|
||||
}
|
||||
//
|
||||
// public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
|
||||
// try {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||||
//
|
||||
// cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
|
||||
// return cipher;
|
||||
// } catch (GeneralSecurityException generalsecurityexception) {
|
||||
// throw new RuntimeException(generalsecurityexception);
|
||||
// }
|
||||
// }
|
||||
|
||||
public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||||
|
||||
cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
|
||||
return cipher;
|
||||
} catch (GeneralSecurityException generalsecurityexception) {
|
||||
throw new RuntimeException(generalsecurityexception);
|
||||
}
|
||||
}
|
||||
|
||||
private Encryption() {
|
||||
private EncryptionUtil() {
|
||||
//utility
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import com.avaje.ebeaninternal.api.ClassUtil;
|
||||
import com.comphenix.protocol.AsynchronousManager;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
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.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;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
|
||||
*/
|
||||
public class FastLoginBukkit extends JavaPlugin {
|
||||
|
||||
private static final int WORKER_THREADS = 3;
|
||||
|
||||
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
|
||||
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
|
||||
|
||||
private boolean bungeeCord;
|
||||
private 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 = 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.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);
|
||||
}
|
||||
} catch (Exception | NoSuchMethodError ex) {
|
||||
getLogger().warning("Cannot check bungeecord support. You use a non-spigot build");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
if (getServer().getOnlineMode()) {
|
||||
//we need to require offline to prevent a session request for a offline player
|
||||
getLogger().severe("Server have to be in offline mode");
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bungeeCord) {
|
||||
setServerStarted();
|
||||
|
||||
//check for incoming messages from the bungeecord version of this plugin
|
||||
getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this));
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, getName());
|
||||
//register listeners on success
|
||||
} else {
|
||||
String driver = getConfig().getString("driver");
|
||||
String host = getConfig().getString("host", "");
|
||||
int port = getConfig().getInt("port", 3306);
|
||||
String database = getConfig().getString("database");
|
||||
|
||||
String username = getConfig().getString("username", "");
|
||||
String password = getConfig().getString("password", "");
|
||||
|
||||
if (!core.setupDatabase(driver, host, port, database, username, password)) {
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) {
|
||||
getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this);
|
||||
} 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 = ProtocolLibrary.getProtocolManager().getAsynchronousManager();
|
||||
|
||||
StartPacketListener startPacketListener = new StartPacketListener(this);
|
||||
EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this);
|
||||
|
||||
asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS);
|
||||
asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS);
|
||||
getServer().getPluginManager().registerEvents(new LoginSkinApplyListener(this), this);
|
||||
} else {
|
||||
getLogger().warning("Either ProtocolLib or ProtocolSupport have to be installed "
|
||||
+ "if you don't use BungeeCord");
|
||||
}
|
||||
}
|
||||
|
||||
//delay dependency setup because we load the plugin very early where plugins are initialized yet
|
||||
getServer().getScheduler().runTask(this, new DelayedAuthHook(this));
|
||||
|
||||
getServer().getPluginManager().registerEvents(new BukkitJoinListener(this), this);
|
||||
|
||||
//register commands using a unique name
|
||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
||||
getCommand("cracked").setExecutor(new CrackedCommand(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
//clean up
|
||||
session.clear();
|
||||
|
||||
if (core != null) {
|
||||
core.close();
|
||||
}
|
||||
|
||||
//remove old blacklists
|
||||
for (Player player : getServer().getOnlinePlayers()) {
|
||||
player.removeMetadata(getName(), this);
|
||||
}
|
||||
}
|
||||
|
||||
public FastLoginCore getCore() {
|
||||
return core;
|
||||
}
|
||||
|
||||
public String generateStringPassword(Player player) {
|
||||
return passwordGenerator.getRandomPassword(player);
|
||||
}
|
||||
|
||||
public void setPasswordGenerator(PasswordGenerator passwordGenerator) {
|
||||
this.passwordGenerator = passwordGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
|
||||
* account)
|
||||
*
|
||||
* @return a thread-safe session map
|
||||
*/
|
||||
public ConcurrentMap<String, BukkitLoginSession> getSessions() {
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between the client and server
|
||||
*
|
||||
* @return the server KeyPair
|
||||
*/
|
||||
public KeyPair getServerKey() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the auth plugin hook in order to interact with the plugins. This can be null if no supporting auth plugin
|
||||
* was found.
|
||||
*
|
||||
* @return interface to any supported auth plugin
|
||||
*/
|
||||
public BukkitAuthPlugin getAuthPlugin() {
|
||||
if (authPlugin == null) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ex) {
|
||||
getLogger().log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return authPlugin;
|
||||
}
|
||||
|
||||
public void setAuthPluginHook(BukkitAuthPlugin authPlugin) {
|
||||
this.authPlugin = authPlugin;
|
||||
}
|
||||
|
||||
public boolean isBungeeCord() {
|
||||
return bungeeCord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait before the server is fully started. This is workaround, because connections right on startup are not
|
||||
* injected by ProtocolLib
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isServerFullyStarted() {
|
||||
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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
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;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
public class MojangApiBukkit extends MojangApiConnector {
|
||||
|
||||
//mojang api check to prove a player is logged in minecraft and made a join server request
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
public MojangApiBukkit(ConcurrentMap<Object, Object> requests, Logger logger, List<String> localAddresses
|
||||
, int rateLimit) {
|
||||
super(requests, logger, localAddresses, rateLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasJoinedServer(Object session, String serverId) {
|
||||
if (!(session instanceof BukkitLoginSession)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BukkitLoginSession playerSession = (BukkitLoginSession) session;
|
||||
try {
|
||||
String url = HAS_JOINED_URL + "username=" + playerSession.getUsername() + "&serverId=" + serverId;
|
||||
HttpURLConnection conn = getConnection(url);
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != null && !line.equals("null")) {
|
||||
//validate parsing
|
||||
//http://wiki.vg/Protocol_Encryption#Server
|
||||
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) userData.get("id");
|
||||
playerSession.setUuid(FastLoginCore.parseId(uuid));
|
||||
|
||||
JSONArray properties = (JSONArray) userData.get("properties");
|
||||
JSONObject skinProperty = (JSONObject) properties.get(0);
|
||||
|
||||
String propertyName = (String) skinProperty.get("name");
|
||||
if (propertyName.equals("textures")) {
|
||||
String skinValue = (String) skinProperty.get("value");
|
||||
String signature = (String) skinProperty.get("signature");
|
||||
playerSession.setSkin(skinValue, signature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
//catch not only ioexceptions also parse and NPE on unexpected json format
|
||||
logger.log(Level.WARNING, "Failed to verify session", ex);
|
||||
}
|
||||
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getUUIDFromJson(String json) {
|
||||
boolean isArray = json.startsWith("[");
|
||||
|
||||
JSONObject mojangPlayer;
|
||||
if (isArray) {
|
||||
JSONArray array = (JSONArray) JSONValue.parse(json);
|
||||
mojangPlayer = (JSONObject) array.get(0);
|
||||
} else {
|
||||
mojangPlayer = (JSONObject) JSONValue.parse(json);
|
||||
}
|
||||
|
||||
String uuid = (String) mojangPlayer.get("id");
|
||||
if (uuid == null || uuid.equals("null")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FastLoginCore.parseId(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.games647.fastlogin.bukkit;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public interface PasswordGenerator {
|
||||
|
||||
String getRandomPassword(Player player);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.github.games647.fastlogin.bukkit.commands;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class CrackedCommand implements CommandExecutor {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public CrackedCommand(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(plugin.getCore().getMessage("no-console"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (plugin.isBungeeCord()) {
|
||||
notifiyBungeeCord(sender, sender.getName());
|
||||
String message = plugin.getCore().getMessage("wait-on-proxy");
|
||||
if (message != null) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
} else {
|
||||
//todo: load async if
|
||||
final PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
|
||||
if (profile.isPremium()) {
|
||||
sender.sendMessage(plugin.getCore().getMessage("remove-premium"));
|
||||
profile.setPremium(false);
|
||||
profile.setUuid(null);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
plugin.getCore().getStorage().save(profile);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sender.sendMessage(plugin.getCore().getMessage("not-premium"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifiyBungeeCord(Player sender, String target) {
|
||||
if (plugin.isBungeeCord()) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
dataOutput.writeUTF("OFF");
|
||||
dataOutput.writeUTF(target);
|
||||
|
||||
sender.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.github.games647.fastlogin.bukkit.commands;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
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.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Let users activate fast login by command. This only be accessible if
|
||||
* the user has access to it's account. So we can make sure that not another
|
||||
* person with a paid account and the same username can steal his account.
|
||||
*/
|
||||
public class PremiumCommand implements CommandExecutor {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public PremiumCommand(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(plugin.getCore().getMessage("no-console"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (plugin.isBungeeCord()) {
|
||||
notifiyBungeeCord(sender, sender.getName());
|
||||
String message = plugin.getCore().getMessage("wait-on-proxy");
|
||||
if (message != null) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
} else {
|
||||
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(plugin.getCore().getMessage("already-exists"));
|
||||
} 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"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifiyBungeeCord(Player sender, String target) {
|
||||
if (plugin.isBungeeCord()) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
dataOutput.writeUTF("ON");
|
||||
dataOutput.writeUTF(target);
|
||||
|
||||
sender.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import com.avaje.ebeaninternal.api.ClassUtil;
|
||||
|
||||
import fr.xephi.authme.api.API;
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/Xephi/AuthMeReloaded/
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/authme-reloaded/
|
||||
* Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
|
||||
*/
|
||||
public class AuthMeHook implements BukkitAuthPlugin {
|
||||
|
||||
private final boolean isNewAPIAvailable;
|
||||
|
||||
public AuthMeHook() {
|
||||
this.isNewAPIAvailable = ClassUtil.isPresent("fr.xephi.authme.api.NewAPI");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(Player player) {
|
||||
//skips registration and login
|
||||
if (isNewAPIAvailable) {
|
||||
NewAPI.getInstance().forceLogin(player);
|
||||
} else {
|
||||
API.forceLogin(player);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
if (isNewAPIAvailable) {
|
||||
return NewAPI.getInstance().isRegistered(playerName);
|
||||
} else {
|
||||
return API.isRegistered(playerName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
if (isNewAPIAvailable) {
|
||||
NewAPI.getInstance().forceRegister(player, password);
|
||||
} else {
|
||||
API.registerPlayer(player.getName(), password);
|
||||
forceLogin(player);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin in Bukkit/Spigot/... servers
|
||||
*/
|
||||
public interface BukkitAuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player after
|
||||
* the player joined successfully the server.
|
||||
*
|
||||
* <strong>This operation will be performed async while the player successfully
|
||||
* joined the server.</strong>
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceLogin(Player player);
|
||||
|
||||
/**
|
||||
* Checks whether an account exists for this player name.
|
||||
*
|
||||
* This check should check if a cracked player account exists
|
||||
* so we can be sure the premium player doesn't steal the account
|
||||
* of that player.
|
||||
*
|
||||
* This operation will be performed async while the player is
|
||||
* connecting.
|
||||
*
|
||||
* @param playerName player name
|
||||
* @return if the player has an account
|
||||
* @throws Exception if an error occurred
|
||||
*/
|
||||
boolean isRegistered(String playerName) throws Exception;
|
||||
|
||||
/**
|
||||
* Forces a register in order to protect the paid account.
|
||||
*
|
||||
* <strong>This operation will be performed async while the player successfully
|
||||
* joined the server.</strong>
|
||||
*
|
||||
* After a successful registration the player should be logged
|
||||
* in too.
|
||||
*
|
||||
* The method will be called only for premium accounts.
|
||||
* So it's recommended to set additionally premium property
|
||||
* if possible.
|
||||
*
|
||||
* Background: If we don't register an account, cracked players
|
||||
* could steal the unregistered account from the paid
|
||||
* player account
|
||||
*
|
||||
* @param player the premium account
|
||||
* @param password a strong random generated password
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceRegister(Player player, String password);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import de.st_ddt.crazylogin.CrazyLogin;
|
||||
import de.st_ddt.crazylogin.data.LoginPlayerData;
|
||||
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
|
||||
import de.st_ddt.crazylogin.listener.PlayerListener;
|
||||
import de.st_ddt.crazylogin.metadata.Authenticated;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.apache.commons.lang.reflect.FieldUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/ST-DDT/CrazyLogin
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/server-mods/crazylogin/
|
||||
*/
|
||||
public class CrazyLoginHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
|
||||
private final PlayerListener playerListener = getListener();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe operation
|
||||
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin
|
||||
, new Callable<LoginPlayerData>() {
|
||||
@Override
|
||||
public LoginPlayerData call() throws Exception {
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData != null) {
|
||||
//mark the account as logged in
|
||||
playerData.setLoggedIn(true);
|
||||
|
||||
String ip = player.getAddress().getAddress().getHostAddress();
|
||||
//this should be done after login to restore the inventory, unhide players, prevent potential memory leaks...
|
||||
//from: https://github.com/ST-DDT/CrazyLogin/blob/master/src/main/java/de/st_ddt/crazylogin/CrazyLogin.java#L1948
|
||||
playerData.resetLoginFails();
|
||||
player.setFireTicks(0);
|
||||
|
||||
if (playerListener != null) {
|
||||
playerListener.removeMovementBlocker(player);
|
||||
playerListener.disableHidenInventory(player);
|
||||
playerListener.disableSaveLogin(player);
|
||||
playerListener.unhidePlayer(player);
|
||||
}
|
||||
|
||||
//loginFailuresPerIP.remove(IP);
|
||||
//illegalCommandUsesPerIP.remove(IP);
|
||||
//tempBans.remove(IP);
|
||||
playerData.addIP(ip);
|
||||
player.setMetadata("Authenticated", new Authenticated(crazyLoginPlugin, player));
|
||||
crazyLoginPlugin.unregisterDynamicHooks();
|
||||
return playerData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
LoginPlayerData result = future.get();
|
||||
if (result != null && result.isLoggedIn()) {
|
||||
//SQL-Queries should run async
|
||||
crazyLoginPlugin.getCrazyDatabase().saveWithoutPassword(result);
|
||||
return true;
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
return crazyLoginPlugin.getPlayerData(playerName) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final Player player, String password) {
|
||||
CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
|
||||
|
||||
//this executes a sql query and accesses only thread safe collections so we can run it async
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData == null) {
|
||||
//create a fake account - this will be saved to the database with the password=FAILEDLOADING
|
||||
//user cannot login with that password unless the admin uses plain text
|
||||
//this automatically marks the player as logged in
|
||||
playerData = new LoginPlayerData(player);
|
||||
crazyDatabase.save(playerData);
|
||||
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private PlayerListener getListener() {
|
||||
PlayerListener listener;
|
||||
try {
|
||||
listener = (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;
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import io.github.lucaseasedup.logit.CancelledState;
|
||||
import io.github.lucaseasedup.logit.LogItCore;
|
||||
import io.github.lucaseasedup.logit.account.Account;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/XziomekX/LogIt
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: Unknown
|
||||
* Spigot: Unknown
|
||||
*/
|
||||
public class LogItHook implements BukkitAuthPlugin {
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(Player player) {
|
||||
return LogItCore.getInstance().getSessionManager().startSession(player) == CancelledState.NOT_CANCELLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
return LogItCore.getInstance().getAccountManager().isRegistered(playerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
Account account = new Account(player.getName());
|
||||
account.changePassword(password);
|
||||
account.setLastActiveDate(System.currentTimeMillis() / 1000);
|
||||
account.setRegistrationDate(System.currentTimeMillis() / 1000);
|
||||
return LogItCore.getInstance().getAccountManager().insertAccount(account) == CancelledState.NOT_CANCELLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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.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;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/lenis0012/LoginSecurity-2 Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
|
||||
* Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
|
||||
*/
|
||||
public class LoginSecurityHook implements BukkitAuthPlugin {
|
||||
|
||||
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.LoginSecurity", getClass());
|
||||
if (newVersion) {
|
||||
this.securityPlugin = null;
|
||||
} else {
|
||||
this.securityPlugin = com.lenis0012.bukkit.ls.LoginSecurity.instance;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(securityPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
String name = player.getName().toLowerCase();
|
||||
|
||||
//mark the user as logged in
|
||||
securityPlugin.authList.remove(name);
|
||||
//cancel timeout timer
|
||||
securityPlugin.thread.timeout.remove(name);
|
||||
//remove effects and restore location
|
||||
securityPlugin.rehabPlayer(player, name);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
securityPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean 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("-", ""));
|
||||
}
|
||||
|
||||
public boolean oldForceRegister(Player player, String password) {
|
||||
DataManager dataManager = securityPlugin.data;
|
||||
|
||||
UUID playerUUID = player.getUniqueId();
|
||||
String uuidString = playerUUID.toString().replace("-", "");
|
||||
InetAddress ipAddress = player.getAddress().getAddress();
|
||||
String passwordHash = securityPlugin.hasher.hash(password);
|
||||
|
||||
//this executes a sql query without interacting with other parts so we can run it async.
|
||||
dataManager.register(uuidString, passwordHash, securityPlugin.hasher.getTypeId(), ipAddress.toString());
|
||||
String storedPassword = dataManager.getPassword(uuidString);
|
||||
if (storedPassword != null && storedPassword.equals(passwordHash)) {
|
||||
//the register method silents any excpetion so check if our entry was saved
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.royaldev.royalauth.AuthPlayer;
|
||||
import org.royaldev.royalauth.Config;
|
||||
import org.royaldev.royalauth.RoyalAuth;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/RoyalDev/RoyalAuth
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/royalauth/
|
||||
*/
|
||||
public class RoyalAuthHook implements BukkitAuthPlugin {
|
||||
|
||||
private final RoyalAuth royalAuthPlugin = (RoyalAuth) Bukkit.getPluginManager().getPlugin("RoyalAuth");
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(royalAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
|
||||
|
||||
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdLogin.java#L62
|
||||
//not thread-safe
|
||||
authPlayer.login();
|
||||
|
||||
return authPlayer.isLoggedIn();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
royalAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(playerName);
|
||||
return authPlayer.isRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdRegister.java#L50
|
||||
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
|
||||
|
||||
boolean registerSuccess = authPlayer.setPassword(password, Config.passwordHashType);
|
||||
if (registerSuccess) {
|
||||
//login in the player after registration
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import ultraauth.api.UltraAuthAPI;
|
||||
import ultraauth.main.Main;
|
||||
import ultraauth.managers.PlayerManager;
|
||||
|
||||
/**
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
|
||||
* Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
|
||||
*/
|
||||
public class UltraAuthHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final Plugin ultraAuthPlugin = Main.main;
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
UltraAuthAPI.authenticatedPlayer(player);
|
||||
return UltraAuthAPI.isAuthenticated(player);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
ultraAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
return UltraAuthAPI.isRegisterd(new FakePlayer(playerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(Player player, String password) {
|
||||
UltraAuthAPI.setPlayerPasswordOnline(player, password);
|
||||
if (PlayerManager.getInstance().checkPlayerPassword(player, password)) {
|
||||
//the register method silents any excpetion so check if our entry was saved
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.github.games647.fastlogin.bukkit.hooks;
|
||||
|
||||
import de.luricos.bukkit.xAuth.xAuth;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/LycanDevelopment/xAuth/
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Bukkit: http://dev.bukkit.org/bukkit-plugins/xauth/
|
||||
*/
|
||||
public class xAuthHook implements BukkitAuthPlugin {
|
||||
|
||||
protected final xAuth xAuthPlugin = xAuth.getPlugin();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(final Player player) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
if (xAuthPlayer != null) {
|
||||
//we checked that the player is premium (paid account)
|
||||
xAuthPlayer.setPremium(true);
|
||||
|
||||
//unprotect the inventory, op status...
|
||||
return xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
//this will load the player if it's not in the cache
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(playerName);
|
||||
return xAuthPlayer != null && xAuthPlayer.isRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(final Player player, final String password) {
|
||||
//not thread-safe
|
||||
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
if (xAuthPlayer != null) {
|
||||
//this should run async because the plugin executes a sql query, but the method
|
||||
//accesses non thread-safe collections :(
|
||||
boolean registerSuccess = xAuthPlugin.getAuthClass(xAuthPlayer)
|
||||
.adminRegister(player.getName(), password, null);
|
||||
|
||||
return registerSuccess;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
boolean success = future.get();
|
||||
if (success) {
|
||||
//login in the player after registration
|
||||
return forceLogin(player);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
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.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
|
||||
* plugin can skip authentication.
|
||||
*/
|
||||
public class BukkitJoinListener implements Listener {
|
||||
|
||||
private static final long DELAY_LOGIN = 20L / 2;
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public BukkitJoinListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
|
||||
if (loginEvent.getResult() == Result.ALLOWED && !plugin.isServerFullyStarted()) {
|
||||
loginEvent.disallow(Result.KICK_OTHER, plugin.getCore().getMessage("not-started"));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
|
||||
Player player = joinEvent.getPlayer();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
|
||||
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
/**
|
||||
* Responsible for receiving messages from a BungeeCord instance.
|
||||
*
|
||||
* This class also receives the plugin message from the bungeecord version of this plugin in order to get notified if
|
||||
* the connection is in online mode.
|
||||
*/
|
||||
public class BungeeCordListener implements PluginMessageListener {
|
||||
|
||||
private static final String FILE_NAME = "proxy-whitelist.txt";
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
//null if whitelist is empty so bungeecord support is disabled
|
||||
private final Set<UUID> proxyIds;
|
||||
|
||||
public BungeeCordListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
this.proxyIds = loadBungeeCordIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, final Player player, byte[] message) {
|
||||
if (!channel.equals(plugin.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayDataInput dataInput = ByteStreams.newDataInput(message);
|
||||
String subchannel = dataInput.readUTF();
|
||||
plugin.getLogger().log(Level.FINEST, "Received plugin message for subchannel {0} from {1}"
|
||||
, new Object[]{subchannel, player});
|
||||
|
||||
final String playerName = dataInput.readUTF();
|
||||
|
||||
//check if the player is still online or disconnected
|
||||
final Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
|
||||
//fail if target player is blacklisted because already authed or wrong bungeecord id
|
||||
if (checkedPlayer != null && !checkedPlayer.hasMetadata(plugin.getName())) {
|
||||
//blacklist this target player for BungeeCord Id brute force attacks
|
||||
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
|
||||
|
||||
//bungeecord UUID
|
||||
long mostSignificantBits = dataInput.readLong();
|
||||
long leastSignificantBits = dataInput.readLong();
|
||||
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
|
||||
plugin.getLogger().log(Level.FINEST, "Received proxy id {0} from {1}", new Object[]{sourceId, player});
|
||||
|
||||
//fail if BungeeCord support is disabled (id = null)
|
||||
if (proxyIds.contains(sourceId)) {
|
||||
final String id = '/' + checkedPlayer.getAddress().getAddress().getHostAddress() + ':'
|
||||
+ checkedPlayer.getAddress().getPort();
|
||||
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
|
||||
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, true);
|
||||
playerSession.setVerified(true);
|
||||
plugin.getSessions().put(id, playerSession);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
|
||||
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
try {
|
||||
//we need to check if the player is registered on Bukkit too
|
||||
if (authPlugin == null || !authPlugin.isRegistered(playerName)) {
|
||||
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, false);
|
||||
playerSession.setVerified(true);
|
||||
plugin.getSessions().put(id, playerSession);
|
||||
new ForceLoginTask(plugin, player).run();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<UUID> loadBungeeCordIds() {
|
||||
File whitelistFile = new File(plugin.getDataFolder(), FILE_NAME);
|
||||
//create a new folder if it doesn't exist. Fail silently otherwise
|
||||
whitelistFile.getParentFile().mkdir();
|
||||
try {
|
||||
if (!whitelistFile.exists()) {
|
||||
whitelistFile.createNewFile();
|
||||
}
|
||||
|
||||
Set<UUID> ids = Sets.newHashSet();
|
||||
|
||||
List<String> lines = Files.readLines(whitelistFile, Charsets.UTF_8);
|
||||
for (String line : lines) {
|
||||
if (line == null || line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UUID uuid = UUID.fromString(line.trim());
|
||||
ids.add(uuid);
|
||||
}
|
||||
|
||||
return ids;
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener.protocollib;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedChatComponent;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
|
||||
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class VerifyResponseTask implements Runnable {
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
private final PacketEvent packetEvent;
|
||||
|
||||
private final Player fromPlayer;
|
||||
|
||||
private final byte[] sharedSecret;
|
||||
|
||||
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player fromPlayer, byte[] sharedSecret) {
|
||||
this.plugin = plugin;
|
||||
this.packetEvent = packetEvent;
|
||||
this.fromPlayer = fromPlayer;
|
||||
this.sharedSecret = sharedSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
BukkitLoginSession session = plugin.getSessions().get(fromPlayer.getAddress().toString());
|
||||
if (session == null) {
|
||||
disconnect(plugin.getCore().getMessage("invalid-requst"), true
|
||||
, "Player {0} tried to send encryption response at invalid state", fromPlayer.getAddress());
|
||||
} else {
|
||||
verifyResponse(session);
|
||||
}
|
||||
} finally {
|
||||
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyResponse(BukkitLoginSession session) {
|
||||
PrivateKey privateKey = plugin.getServerKey().getPrivate();
|
||||
|
||||
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
|
||||
if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//this makes sure the request from the client is for us
|
||||
//this might be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
|
||||
String generatedId = session.getServerId();
|
||||
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
|
||||
//generate the server id based on client and server data
|
||||
byte[] serverIdHash = EncryptionUtil.getServerIdHash(generatedId, plugin.getServerKey().getPublic(), loginKey);
|
||||
String serverId = (new BigInteger(serverIdHash)).toString(16);
|
||||
|
||||
String username = session.getUsername();
|
||||
if (plugin.getCore().getMojangApiConnector().hasJoinedServer(session, serverId)) {
|
||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||
|
||||
session.setVerified(true);
|
||||
setPremiumUUID(session.getUuid());
|
||||
receiveFakeStartPacket(username);
|
||||
} else {
|
||||
//user tried to fake a authentication
|
||||
disconnect(plugin.getCore().getMessage("invalid-session"), true
|
||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||
, session.getUsername(), fromPlayer.getAddress(), serverId);
|
||||
}
|
||||
|
||||
//this is a fake packet; it shouldn't be send to the server
|
||||
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPremiumUUID(UUID premiumUUID) {
|
||||
if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) {
|
||||
try {
|
||||
Object networkManager = getNetworkManager();
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
|
||||
Field spoofField = FuzzyReflection.fromObject(networkManager).getFieldByType("spoofedUUID", UUID.class);
|
||||
spoofField.set(networkManager, premiumUUID);
|
||||
} catch (ReflectiveOperationException reflectiveOperationException) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", reflectiveOperationException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey) {
|
||||
byte[] requestVerify = session.getVerifyToken();
|
||||
//encrypted verify token
|
||||
byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1);
|
||||
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
|
||||
if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
|
||||
//check if the verify token are equal to the server sent one
|
||||
disconnect(plugin.getCore().getMessage("invalid-verify-token"), true
|
||||
, "Player {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}"
|
||||
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//try to get the networkManager from ProtocolLib
|
||||
private Object getNetworkManager() throws IllegalAccessException, NoSuchFieldException {
|
||||
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer);
|
||||
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
|
||||
injectorField.setAccessible(true);
|
||||
|
||||
Object rawInjector = injectorField.get(socketInjector);
|
||||
|
||||
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||
injectorField.setAccessible(true);
|
||||
return injectorField.get(rawInjector);
|
||||
}
|
||||
|
||||
private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException {
|
||||
try {
|
||||
//get the NMS connection handle of this player
|
||||
Object networkManager = getNetworkManager();
|
||||
|
||||
//try to detect the method by parameters
|
||||
Method encryptConnectionMethod = FuzzyReflection
|
||||
.fromObject(networkManager).getMethodByParameters("a", SecretKey.class);
|
||||
|
||||
//encrypt/decrypt following packets
|
||||
//the client expects this behaviour
|
||||
encryptConnectionMethod.invoke(networkManager, loginKey);
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
disconnect(plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) {
|
||||
if (debug) {
|
||||
plugin.getLogger().log(Level.FINE, logMessage, arguments);
|
||||
} else {
|
||||
plugin.getLogger().log(Level.SEVERE, logMessage, arguments);
|
||||
}
|
||||
|
||||
kickPlayer(packetEvent.getPlayer(), kickReason);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
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));
|
||||
|
||||
try {
|
||||
//send kick packet at login state
|
||||
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
|
||||
protocolManager.sendServerPacket(player, kickPacket);
|
||||
//tell the server that we want to close the connection
|
||||
player.kickPlayer("Disconnect");
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Error sending kickpacket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
//fake a new login packet in order to let the server handle all the other stuff
|
||||
private void receiveFakeStartPacket(String username) {
|
||||
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
|
||||
|
||||
//see StartPacketListener for packet information
|
||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
|
||||
|
||||
//uuid is ignored by the packet definition
|
||||
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
|
||||
startPacket.getGameProfiles().write(0, fakeProfile);
|
||||
try {
|
||||
//we don't want to handle our own packets so ignore filters
|
||||
protocolManager.recieveClientPacket(fromPlayer, startPacket, false);
|
||||
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
kickPlayer(fromPlayer, plugin.getCore().getMessage("error-kick"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import protocolsupport.api.events.PlayerLoginStartEvent;
|
||||
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
|
||||
|
||||
public class ProtocolSupportListener implements Listener {
|
||||
|
||||
protected final FastLoginBukkit plugin;
|
||||
|
||||
public ProtocolSupportListener(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onLoginStart(PlayerLoginStartEvent loginStartEvent) {
|
||||
plugin.setServerStarted();
|
||||
if (loginStartEvent.isLoginDenied()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = loginStartEvent.getName();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(loginStartEvent.getAddress().toString());
|
||||
|
||||
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;
|
||||
|
||||
//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) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
|
||||
startPremiumSession(username, loginStartEvent, false, profile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//no premium check passed so we save it as a cracked player
|
||||
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
|
||||
plugin.getSessions().put(loginStartEvent.getAddress().toString(), loginSession);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
|
||||
}
|
||||
} else if (profile.isPremium()) {
|
||||
startPremiumSession(username, loginStartEvent, true, profile);
|
||||
} else {
|
||||
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
|
||||
plugin.getSessions().put(loginStartEvent.getAddress().toString(), loginSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPropertiesResolve(PlayerPropertiesResolveEvent propertiesResolveEvent) {
|
||||
//skin was resolved -> premium player
|
||||
if (propertiesResolveEvent.hasProperty("textures")) {
|
||||
InetSocketAddress address = propertiesResolveEvent.getAddress();
|
||||
BukkitLoginSession session = plugin.getSessions().get(address.toString());
|
||||
if (session != null) {
|
||||
session.setVerified(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startPremiumSession(String playerName, PlayerLoginStartEvent loginStartEvent, boolean registered
|
||||
, PlayerProfile playerProfile) {
|
||||
loginStartEvent.setOnlineMode(true);
|
||||
InetSocketAddress address = loginStartEvent.getAddress();
|
||||
|
||||
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, null, null, registered, playerProfile);
|
||||
plugin.getSessions().put(address.toString(), playerSession);
|
||||
if (plugin.getConfig().getBoolean("premiumUuid")) {
|
||||
loginStartEvent.setUseOnlineModeUUID(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.github.games647.fastlogin.bukkit.tasks;
|
||||
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.AuthMeHook;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.CrazyLoginHook;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.LogItHook;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.LoginSecurityHook;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.UltraAuthHook;
|
||||
import com.github.games647.fastlogin.bukkit.hooks.xAuthHook;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public class DelayedAuthHook implements Runnable {
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
|
||||
public DelayedAuthHook(FastLoginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean hookFound = registerHooks();
|
||||
if (plugin.isBungeeCord()) {
|
||||
plugin.getLogger().info("BungeeCord setting detected. No auth plugin is required");
|
||||
} else if (!hookFound) {
|
||||
plugin.getLogger().warning("No auth plugin were found by this plugin "
|
||||
+ "(other plugins could hook into this after the intialization of this plugin)"
|
||||
+ "and bungeecord is deactivated. "
|
||||
+ "Either one or both of the checks have to pass in order to use this plugin");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean registerHooks() {
|
||||
BukkitAuthPlugin authPluginHook = null;
|
||||
try {
|
||||
List<Class<? extends BukkitAuthPlugin>> supportedHooks = Lists.newArrayList(AuthMeHook.class
|
||||
, CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class
|
||||
, xAuthHook.class);
|
||||
for (Class<? extends BukkitAuthPlugin> clazz : supportedHooks) {
|
||||
String pluginName = clazz.getSimpleName().replace("Hook", "");
|
||||
//uses only member classes which uses AuthPlugin interface (skip interfaces)
|
||||
if (Bukkit.getServer().getPluginManager().getPlugin(pluginName) != null) {
|
||||
//check only for enabled plugins. A single plugin could be disabled by plugin managers
|
||||
authPluginHook = clazz.newInstance();
|
||||
plugin.getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
|
||||
}
|
||||
|
||||
if (authPluginHook == null) {
|
||||
//run this check for exceptions (errors) and not found plugins
|
||||
plugin.getLogger().warning("No support offline Auth plugin found. ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plugin.getAuthPlugin() == null) {
|
||||
plugin.setAuthPluginHook(authPluginHook);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class ForceLoginTask implements Runnable {
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
protected final Player player;
|
||||
|
||||
public ForceLoginTask(FastLoginBukkit plugin, Player player) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isOnlineThreadSafe()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//remove the bungeecord identifier if there is ones
|
||||
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
|
||||
BukkitLoginSession session = plugin.getSessions().remove(id);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
AuthStorage storage = plugin.getCore().getStorage();
|
||||
PlayerProfile playerProfile = session.getProfile();
|
||||
|
||||
//check if it's the same player as we checked before
|
||||
if (session.isVerified() && player.getName().equals(session.getUsername())) {
|
||||
//premium player
|
||||
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
|
||||
if (authPlugin == null) {
|
||||
//maybe only bungeecord plugin
|
||||
sendSuccessNotification();
|
||||
} else {
|
||||
boolean success = false;
|
||||
if (isOnlineThreadSafe()) {
|
||||
if (session.needsRegistration()) {
|
||||
success = forceRegister(authPlugin, player);
|
||||
} else {
|
||||
success = forceLogin(authPlugin, player);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
//update only on success to prevent corrupt data
|
||||
if (playerProfile != null) {
|
||||
playerProfile.setUuid(session.getUuid());
|
||||
//save cracked players too
|
||||
playerProfile.setPremium(true);
|
||||
storage.save(playerProfile);
|
||||
}
|
||||
|
||||
sendSuccessNotification();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//cracked player
|
||||
if (playerProfile != null) {
|
||||
playerProfile.setUuid(null);
|
||||
playerProfile.setPremium(false);
|
||||
storage.save(playerProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean forceRegister(BukkitAuthPlugin authPlugin, Player player) {
|
||||
plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName());
|
||||
|
||||
String generatedPassword = plugin.generateStringPassword(player);
|
||||
boolean success = authPlugin.forceRegister(player, generatedPassword);
|
||||
String message = plugin.getCore().getMessage("auto-register");
|
||||
if (success && message != null) {
|
||||
message = message.replace("%password", generatedPassword);
|
||||
player.sendMessage(message);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean forceLogin(BukkitAuthPlugin authPlugin, Player player) {
|
||||
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
|
||||
boolean success = authPlugin.forceLogin(player);
|
||||
|
||||
String message = plugin.getCore().getMessage("auto-login");
|
||||
if (success && message != null) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void sendSuccessNotification() {
|
||||
if (plugin.isBungeeCord()) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
dataOutput.writeUTF("SUCCESS");
|
||||
|
||||
player.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOnlineThreadSafe() {
|
||||
//the playerlist isn't thread-safe
|
||||
Future<Boolean> onlineFuture = Bukkit.getScheduler().callSyncMethod(plugin, new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return player.isOnline();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return onlineFuture.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to perform thread-safe online check", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
bukkit/src/main/resources/plugin.yml
Normal file
60
bukkit/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
# project informations for Bukkit in order to register our plugin with all it components
|
||||
# ${-} are variables from Maven (pom.xml) which will be replaced after the build
|
||||
name: ${project.parent.name}
|
||||
version: ${project.version}
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
# meta informations for plugin managers
|
||||
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
|
||||
description: |
|
||||
${project.description}
|
||||
website: ${project.url}
|
||||
dev-url: ${project.url}
|
||||
|
||||
# Load the plugin as early as possible to inject it for all players
|
||||
load: STARTUP
|
||||
|
||||
softdepend:
|
||||
# We require either ProtocolLib or ProtocolSupport
|
||||
- ProtocolSupport
|
||||
- ProtocolLib
|
||||
# Auth plugins - we delay the hook
|
||||
# - xAuth
|
||||
# - AuthMe
|
||||
# - LogIt
|
||||
# - CrazyLogin
|
||||
# - LoginSecurity
|
||||
# - RoyalAuth
|
||||
# - UltraAuth
|
||||
|
||||
commands:
|
||||
${project.parent.name}:
|
||||
description: 'Label the invoker as premium'
|
||||
aliases: [prem, premium, loginfast]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.premium
|
||||
|
||||
cracked:
|
||||
description: 'Label the invoker or the player specified as cracked if he was marked premium before'
|
||||
aliases: [unpremium]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.unpremium
|
||||
|
||||
permissions:
|
||||
${project.artifactId}.command.premium:
|
||||
description: 'Label themselves as premium'
|
||||
default: true
|
||||
|
||||
${project.artifactId}.command.premium.other:
|
||||
description: 'Label others as premium'
|
||||
children:
|
||||
${project.artifactId}.command.premium: true
|
||||
|
||||
${project.artifactId}.command.cracked:
|
||||
description: 'Label themselves as cracked'
|
||||
default: true
|
||||
|
||||
${project.artifactId}.command..cracked.other:
|
||||
description: 'Label others as cracked'
|
||||
children:
|
||||
${project.artifactId}.command.cracked: true
|
||||
61
bungee/pom.xml
Normal file
61
bungee/pom.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin.bungee</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!--Represents the main plugin-->
|
||||
<name>FastLoginBungee</name>
|
||||
|
||||
<repositories>
|
||||
<!--BungeeCord with also the part outside the API-->
|
||||
<repository>
|
||||
<id>myplayplanet-REPO</id>
|
||||
<url>http://maven.myplayplanet.net/</url>
|
||||
</repository>
|
||||
|
||||
<!--Github automatic maven builds-->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-proxy</artifactId>
|
||||
<version>1.8-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>io.github.waterfallmc</groupId>
|
||||
<artifactId>waterfall-api</artifactId>
|
||||
<version>1.9-SNAPSHOT</version>
|
||||
<type>jar</type>
|
||||
<scope>provided</scope>
|
||||
</dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.MatteCarra</groupId>
|
||||
<artifactId>BungeeAuth</artifactId>
|
||||
<version>-1.2.1-gc367d92-8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.core.FastLoginCore;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import net.md_5.bungee.api.scheduler.GroupedThreadFactory;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
public class BungeeCore extends FastLoginCore {
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
|
||||
public BungeeCore(FastLoginBungee plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDataFolder() {
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return plugin.getLogger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThreadFactory getThreadFactory() {
|
||||
String pluginName = plugin.getDescription().getName();
|
||||
return new ThreadFactoryBuilder()
|
||||
.setNameFormat(pluginName + " Database Pool Thread #%1$d")
|
||||
//Hikari create daemons by default
|
||||
.setDaemon(true)
|
||||
.setThreadFactory(new GroupedThreadFactory(plugin, pluginName)).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMessages() {
|
||||
try {
|
||||
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, defaults);
|
||||
for (String key : messageConfig.getKeys()) {
|
||||
String message = ChatColor.translateAlternateColorCodes('&', messageConfig.getString(key));
|
||||
if (!message.isEmpty()) {
|
||||
localeMessages.put(key, message);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Failed to load messages", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadConfig() {
|
||||
if (!getDataFolder().exists()) {
|
||||
getDataFolder().mkdir();
|
||||
}
|
||||
|
||||
saveDefaultFile("config.yml");
|
||||
}
|
||||
|
||||
private void saveDefaultFile(String fileName) {
|
||||
File configFile = new File(getDataFolder(), fileName);
|
||||
if (!configFile.exists()) {
|
||||
try (InputStream in = plugin.getResourceAsStream(fileName)) {
|
||||
Files.copy(in, configFile.toPath());
|
||||
} catch (IOException ioExc) {
|
||||
getLogger().log(Level.SEVERE, "Error saving default " + fileName, ioExc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.core.LoginSession;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
|
||||
public class BungeeLoginSession extends LoginSession {
|
||||
|
||||
private boolean alreadySaved;
|
||||
|
||||
public BungeeLoginSession(String username, boolean registered, PlayerProfile profile) {
|
||||
super(username, registered, profile);
|
||||
}
|
||||
|
||||
public void setRegistered(boolean registered) {
|
||||
this.registered = registered;
|
||||
}
|
||||
|
||||
public boolean isAlreadySaved() {
|
||||
return alreadySaved;
|
||||
}
|
||||
|
||||
public void setAlreadySaved(boolean alreadySaved) {
|
||||
this.alreadySaved = alreadySaved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthHook;
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.github.games647.fastlogin.bungee.listener.PlayerConnectionListener;
|
||||
import com.github.games647.fastlogin.bungee.listener.PluginMessageListener;
|
||||
import com.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;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
/**
|
||||
* BungeeCord version of FastLogin. This plugin keeps track on online mode connections.
|
||||
*/
|
||||
public class FastLoginBungee extends Plugin {
|
||||
|
||||
private static final char[] PASSWORD_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
.toCharArray();
|
||||
|
||||
private final FastLoginCore loginCore = new BungeeCore(this);
|
||||
private BungeeAuthPlugin bungeeAuthPlugin;
|
||||
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.loadConfig();
|
||||
loginCore.loadMessages();
|
||||
|
||||
try {
|
||||
File configFile = new File(getDataFolder(), "config.yml");
|
||||
config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
|
||||
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 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;
|
||||
}
|
||||
} catch (IOException ioExc) {
|
||||
getLogger().log(Level.SEVERE, "Error loading config. Disabling plugin...", ioExc);
|
||||
return;
|
||||
}
|
||||
|
||||
//events
|
||||
getProxy().getPluginManager().registerListener(this, new PlayerConnectionListener(this));
|
||||
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
|
||||
|
||||
//this is required to listen to messages from the server
|
||||
getProxy().registerChannel(getDescription().getName());
|
||||
|
||||
registerHook();
|
||||
}
|
||||
|
||||
public String generateStringPassword() {
|
||||
StringBuilder generatedPassword = new StringBuilder(8);
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
generatedPassword.append(PASSWORD_CHARACTERS[random.nextInt(PASSWORD_CHARACTERS.length - 1)]);
|
||||
}
|
||||
|
||||
return generatedPassword.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
loginCore.close();
|
||||
}
|
||||
|
||||
public FastLoginCore getCore() {
|
||||
return loginCore;
|
||||
}
|
||||
|
||||
public void setAuthPluginHook(BungeeAuthPlugin authPlugin) {
|
||||
this.bungeeAuthPlugin = authPlugin;
|
||||
}
|
||||
|
||||
public Configuration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public ConcurrentMap<PendingConnection, BungeeLoginSession> getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public Set<UUID> getPendingConfirms() {
|
||||
return pendingConfirms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auth plugin hook for BungeeCord
|
||||
*
|
||||
* @return the auth hook for BungeeCord. null if none found
|
||||
*/
|
||||
public BungeeAuthPlugin getBungeeAuthPlugin() {
|
||||
return bungeeAuthPlugin;
|
||||
}
|
||||
|
||||
private void registerHook() {
|
||||
Plugin plugin = getProxy().getPluginManager().getPlugin("BungeeAuth");
|
||||
if (plugin != null) {
|
||||
bungeeAuthPlugin = new BungeeAuthHook();
|
||||
getLogger().info("Hooked into BungeeAuth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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(ConcurrentMap<Object, Object> requests, Logger logger, List<String> localAddresses
|
||||
, int rateLimit) {
|
||||
super(requests, logger, localAddresses, rateLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getUUIDFromJson(String json) {
|
||||
boolean isArray = json.startsWith("[");
|
||||
|
||||
MojangPlayer mojangPlayer;
|
||||
if (isArray) {
|
||||
mojangPlayer = BungeeCord.getInstance().gson.fromJson(json, MojangPlayer[].class)[0];
|
||||
} else {
|
||||
mojangPlayer = BungeeCord.getInstance().gson.fromJson(json, MojangPlayer.class);
|
||||
}
|
||||
|
||||
if (mojangPlayer.getId() == null || mojangPlayer.getId().equals("null")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FastLoginCore.parseId(mojangPlayer.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasJoinedServer(Object session, String serverId) {
|
||||
//this is not needed in Bungee
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.games647.fastlogin.bungee;
|
||||
|
||||
public class MojangPlayer {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.github.games647.fastlogin.bungee.hooks;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import me.vik1395.BungeeAuth.ListenerClass;
|
||||
import me.vik1395.BungeeAuth.Main;
|
||||
import me.vik1395.BungeeAuth.Password.PasswordHandler;
|
||||
import me.vik1395.BungeeAuth.Tables;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/MatteCarra/BungeeAuth
|
||||
*
|
||||
* Project page:
|
||||
*
|
||||
* Spigot: https://www.spigotmc.org/resources/bungeeauth.493/
|
||||
*/
|
||||
public class BungeeAuthHook implements BungeeAuthPlugin {
|
||||
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L32
|
||||
private final Tables databaseConnection = new Tables();
|
||||
|
||||
@Override
|
||||
public boolean forceLogin(ProxiedPlayer player) {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L92-95
|
||||
Main.plonline.add(player.getName());
|
||||
|
||||
//renamed from ct to databaseConnection
|
||||
// databaseConnection.setStatus(player.getName(), "online");
|
||||
Class<?>[] parameterTypes = new Class<?>[]{String.class, String.class};
|
||||
Object[] arguments = new Object[]{player.getName(), "online"};
|
||||
|
||||
try {
|
||||
callProtected("setStatus", parameterTypes, arguments);
|
||||
ListenerClass.movePlayer(player, false);
|
||||
|
||||
//proparly not thread-safe
|
||||
ListenerClass.prelogin.get(player.getName()).cancel();
|
||||
} catch (Exception ex) {
|
||||
Main.plugin.getLogger().log(Level.SEVERE, "Error force loging in player", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String playerName) throws Exception {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L46
|
||||
//renamed t to databaseConnection
|
||||
return databaseConnection.checkPlayerEntry(playerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceRegister(ProxiedPlayer player, String password) {
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L102
|
||||
PasswordHandler ph = new PasswordHandler();
|
||||
Random rand = new Random();
|
||||
int maxp = 7; //Total Password Hashing methods.
|
||||
Date dNow = new Date();
|
||||
SimpleDateFormat ft = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
|
||||
|
||||
String Pw = password;
|
||||
String pType = "" + rand.nextInt(maxp + 1);
|
||||
String regdate = ft.format(dNow);
|
||||
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L60
|
||||
String lastip = player.getAddress().getAddress().getHostAddress();
|
||||
String lastseen = regdate;
|
||||
String hash = ph.newHash(Pw, pType);
|
||||
|
||||
//creates a new SQL entry with the player's details.
|
||||
|
||||
//renamed t to databaseConnection
|
||||
// databaseConnection.newPlayerEntry(player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen);
|
||||
|
||||
Class<?>[] parameterTypes = new Class<?>[] {String.class, String.class, String.class, String.class
|
||||
, String.class, String.class, String.class, String.class};
|
||||
Object[] arguments = new Object[] {player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen};
|
||||
|
||||
try {
|
||||
callProtected("newPlayerEntry", parameterTypes, arguments);
|
||||
//proparly not thread-safe
|
||||
forceLogin(player);
|
||||
} catch (Exception ex) {
|
||||
Main.plugin.getLogger().log(Level.SEVERE, "[BungeeAuth] Error when creating a new player in the Database", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//pail ;(
|
||||
private void callProtected(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
|
||||
Class<Tables> tableClass = Tables.class;
|
||||
|
||||
Method method = tableClass.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
method.invoke(databaseConnection, arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.games647.fastlogin.bungee.hooks;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin in BungeeCord/Waterfall/... servers
|
||||
*/
|
||||
public interface BungeeAuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player after
|
||||
* the player joined successfully a server.
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceLogin(ProxiedPlayer player);
|
||||
|
||||
/**
|
||||
* Checks whether an account exists for this player name.
|
||||
*
|
||||
* This check should check if a cracked player account exists
|
||||
* so we can be sure the premium player doesn't steal the account
|
||||
* of that player.
|
||||
*
|
||||
* This operation will be performed async while the player is
|
||||
* connecting
|
||||
*
|
||||
* @param playerName player name
|
||||
* @return if the player has an account
|
||||
* @throws Exception if an error occurred
|
||||
*/
|
||||
boolean isRegistered(String playerName) throws Exception;
|
||||
|
||||
/**
|
||||
* Forces a register in order to protect the paid account.
|
||||
* The method will be invoked after the player joined a server.
|
||||
*
|
||||
* After a successful registration the player should be logged
|
||||
* in too.
|
||||
*
|
||||
* The method will be called only for premium accounts.
|
||||
* So it's recommended to set additionally premium property
|
||||
* if possible.
|
||||
*
|
||||
* If we don't register an account, cracked players
|
||||
* could steal the unregistered account from the paid
|
||||
* player account
|
||||
*
|
||||
* @param player the premium account
|
||||
* @param password a strong random generated password
|
||||
* @return if the operation was successful
|
||||
*/
|
||||
boolean forceRegister(ProxiedPlayer player, String password);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.github.games647.fastlogin.bungee.listener;
|
||||
|
||||
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;
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.UUID;
|
||||
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.PreLoginEvent;
|
||||
import net.md_5.bungee.api.event.ServerConnectedEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.connection.LoginResult;
|
||||
import net.md_5.bungee.connection.LoginResult.Property;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
|
||||
/**
|
||||
* Enables online mode logins for specified users and sends
|
||||
* plugin message to the Bukkit version of this plugin in
|
||||
* order to clear that the connection is online mode.
|
||||
*/
|
||||
public class PlayerConnectionListener implements Listener {
|
||||
|
||||
protected final FastLoginBungee plugin;
|
||||
|
||||
public PlayerConnectionListener(FastLoginBungee plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPreLogin(PreLoginEvent preLoginEvent) {
|
||||
if (preLoginEvent.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
preLoginEvent.registerIntent(plugin);
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, new AsyncPremiumCheck(plugin, preLoginEvent));
|
||||
}
|
||||
|
||||
@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(connection.getUniqueId());
|
||||
|
||||
//bungeecord will do this automatically so override it on disabled option
|
||||
InitialHandler initialHandler = (InitialHandler) connection;
|
||||
if (!plugin.getConfig().getBoolean("premiumUuid")) {
|
||||
try {
|
||||
UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(Charsets.UTF_8));
|
||||
|
||||
//bungeecord doesn't support overriding the premium uuid
|
||||
//so we have to do it with reflection
|
||||
Field idField = InitialHandler.class.getDeclaredField("uniqueId");
|
||||
idField.setAccessible(true);
|
||||
idField.set(connection, offlineUUID);
|
||||
} catch (NoSuchFieldException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to set offline uuid", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!plugin.getConfig().getBoolean("forwardSkin")) {
|
||||
//this is null on offline mode
|
||||
LoginResult loginProfile = initialHandler.getLoginProfile();
|
||||
if (loginProfile != null) {
|
||||
loginProfile.setProperties(new Property[]{});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onServerConnected(ServerConnectedEvent serverConnectedEvent) {
|
||||
ProxiedPlayer player = serverConnectedEvent.getPlayer();
|
||||
ForceLoginTask loginTask = new ForceLoginTask(plugin, player, serverConnectedEvent.getServer());
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
|
||||
ProxiedPlayer player = disconnectEvent.getPlayer();
|
||||
plugin.getSession().remove(player.getPendingConnection());
|
||||
plugin.getPendingConfirms().remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.github.games647.fastlogin.bungee.listener;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
|
||||
import com.github.games647.fastlogin.bungee.FastLoginBungee;
|
||||
import com.github.games647.fastlogin.bungee.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;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
|
||||
public class PluginMessageListener implements Listener {
|
||||
|
||||
protected final FastLoginBungee plugin;
|
||||
|
||||
public PluginMessageListener(FastLoginBungee plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPluginMessage(PluginMessageEvent pluginMessageEvent) {
|
||||
String channel = pluginMessageEvent.getTag();
|
||||
if (pluginMessageEvent.isCancelled() || !plugin.getDescription().getName().equals(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//the client shouldn't be able to read the messages in order to know something about server internal states
|
||||
//moreover the client shouldn't be able fake a running premium check by sending the result message
|
||||
pluginMessageEvent.setCancelled(true);
|
||||
|
||||
//check if the message is sent from the server
|
||||
if (Server.class.isAssignableFrom(pluginMessageEvent.getSender().getClass())) {
|
||||
readMessage(pluginMessageEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void readMessage(PluginMessageEvent pluginMessageEvent) {
|
||||
//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();
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.github.games647.fastlogin.bungee.tasks;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
|
||||
import com.github.games647.fastlogin.bungee.FastLoginBungee;
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.PreLoginEvent;
|
||||
|
||||
public class AsyncPremiumCheck implements Runnable {
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
private final PreLoginEvent preLoginEvent;
|
||||
|
||||
public AsyncPremiumCheck(FastLoginBungee plugin, PreLoginEvent preLoginEvent) {
|
||||
this.plugin = plugin;
|
||||
this.preLoginEvent = preLoginEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
PendingConnection connection = preLoginEvent.getConnection();
|
||||
plugin.getSession().remove(connection);
|
||||
|
||||
String username = connection.getName();
|
||||
try {
|
||||
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(username);
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.getUserId() == -1) {
|
||||
UUID premiumUUID = null;
|
||||
if (plugin.getConfig().getBoolean("nameChangeCheck") || plugin.getConfig().getBoolean("autoRegister")) {
|
||||
premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username);
|
||||
}
|
||||
|
||||
if (premiumUUID == null
|
||||
|| (!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) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check premium state", ex);
|
||||
} finally {
|
||||
preLoginEvent.completeIntent(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPremiumName(String username, PendingConnection connection, PlayerProfile profile)
|
||||
throws Exception {
|
||||
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
|
||||
if (plugin.getConfig().getBoolean("autoRegister")
|
||||
&& (authPlugin == null || !authPlugin.isRegistered(username))) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
requestPremiumLogin(connection, profile, username, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkNameChange(UUID premiumUUID, PendingConnection connection, String username) {
|
||||
//user not exists in the db
|
||||
if (plugin.getConfig().getBoolean("nameChangeCheck")) {
|
||||
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(premiumUUID);
|
||||
if (profile != null) {
|
||||
//uuid exists in the database
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
|
||||
requestPremiumLogin(connection, profile, username, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void requestPremiumLogin(PendingConnection con, PlayerProfile profile, String username, boolean register) {
|
||||
con.setOnlineMode(true);
|
||||
plugin.getSession().put(con, new BungeeLoginSession(username, register, profile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.github.games647.fastlogin.bungee.tasks;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.FastLoginBungee;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
public class AsyncToggleMessage implements Runnable {
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
private final ProxiedPlayer fromPlayer;
|
||||
private final String targetPlayer;
|
||||
private final boolean toPremium;
|
||||
|
||||
public AsyncToggleMessage(FastLoginBungee plugin, ProxiedPlayer fromPlayer, String targetPlayer
|
||||
, boolean toPremium) {
|
||||
this.plugin = plugin;
|
||||
this.fromPlayer = fromPlayer;
|
||||
this.targetPlayer = targetPlayer;
|
||||
this.toPremium = toPremium;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (toPremium) {
|
||||
activatePremium();
|
||||
} else {
|
||||
turnOffPremium();
|
||||
}
|
||||
}
|
||||
|
||||
private void turnOffPremium() {
|
||||
PlayerProfile playerProfile = plugin.getCore().getStorage().loadProfile(targetPlayer);
|
||||
if (!playerProfile.isPremium()) {
|
||||
fromPlayer.sendMessage(TextComponent.fromLegacyText(plugin.getCore().getMessage("not-premium")));
|
||||
return;
|
||||
}
|
||||
|
||||
playerProfile.setPremium(false);
|
||||
playerProfile.setUuid(null);
|
||||
plugin.getCore().getStorage().save(playerProfile);
|
||||
fromPlayer.sendMessage(TextComponent.fromLegacyText(plugin.getCore().getMessage("remove-premium")));
|
||||
}
|
||||
|
||||
private void activatePremium() {
|
||||
PlayerProfile playerProfile = plugin.getCore().getStorage().loadProfile(targetPlayer);
|
||||
if (playerProfile.isPremium()) {
|
||||
fromPlayer.sendMessage(TextComponent.fromLegacyText(plugin.getCore().getMessage("already-exists")));
|
||||
return;
|
||||
}
|
||||
|
||||
playerProfile.setPremium(true);
|
||||
plugin.getCore().getStorage().save(playerProfile);
|
||||
fromPlayer.sendMessage(TextComponent.fromLegacyText(plugin.getCore().getMessage("add-premium")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.github.games647.fastlogin.bungee.tasks;
|
||||
|
||||
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
|
||||
import com.github.games647.fastlogin.bungee.FastLoginBungee;
|
||||
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
|
||||
import com.github.games647.fastlogin.core.PlayerProfile;
|
||||
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;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
|
||||
public class ForceLoginTask implements Runnable {
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
private final ProxiedPlayer player;
|
||||
private final Server server;
|
||||
|
||||
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player, Server server) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
PendingConnection pendingConnection = player.getPendingConnection();
|
||||
BungeeLoginSession session = plugin.getSession().get(pendingConnection);
|
||||
|
||||
if (session == null || !player.isConnected()) {
|
||||
plugin.getLogger().log(Level.FINE, "Invalid session player {0} proparly left the server", player);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerProfile playerProfile = session.getProfile();
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBukkitLoginNotification(boolean autoRegister) {
|
||||
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
||||
//subchannel name
|
||||
if (autoRegister) {
|
||||
dataOutput.writeUTF("AUTO_REGISTER");
|
||||
} else {
|
||||
dataOutput.writeUTF("AUTO_LOGIN");
|
||||
}
|
||||
|
||||
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
|
||||
dataOutput.writeUTF(player.getName());
|
||||
|
||||
//proxy identifier to check if it's a acceptable proxy
|
||||
UUID proxyId = UUID.fromString(plugin.getProxy().getConfig().getUuid());
|
||||
dataOutput.writeLong(proxyId.getMostSignificantBits());
|
||||
dataOutput.writeLong(proxyId.getLeastSignificantBits());
|
||||
|
||||
if (server != null) {
|
||||
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
16
bungee/src/main/resources/bungee.yml
Normal file
16
bungee/src/main/resources/bungee.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# project informations for BungeeCord
|
||||
# This file will be prioritised over plugin.yml which can be also used for Bungee
|
||||
# This make it easy to combine BungeeCord and Bukkit support in one plugin
|
||||
name: ${project.parent.name}
|
||||
# ${-} will be automatically replaced by Maven
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
version: ${project.version}
|
||||
author: games647, http://github.com/games647/FastLogin/graphs/contributors
|
||||
|
||||
softdepends:
|
||||
# BungeeCord auth plugins
|
||||
- BungeeAuth
|
||||
|
||||
description: |
|
||||
${project.description}
|
||||
32
core/pom.xml
Normal file
32
core/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>fastlogin.core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLoginCore</name>
|
||||
|
||||
<dependencies>
|
||||
<!--Database pooling-->
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>2.4.7</version>
|
||||
</dependency>
|
||||
|
||||
<!--Logging framework implements slf4j which is required by hikari-->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-jdk14</artifactId>
|
||||
<version>1.7.21</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.github.games647.fastlogin.core;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class AuthStorage {
|
||||
|
||||
private static final String PREMIUM_TABLE = "premium";
|
||||
|
||||
private final FastLoginCore core;
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
public AuthStorage(FastLoginCore core, String driver, String host, int port, String databasePath
|
||||
, String user, String pass) {
|
||||
this.core = core;
|
||||
|
||||
HikariConfig databaseConfig = new HikariConfig();
|
||||
databaseConfig.setUsername(user);
|
||||
databaseConfig.setPassword(pass);
|
||||
databaseConfig.setDriverClassName(driver);
|
||||
databaseConfig.setThreadFactory(core.getThreadFactory());
|
||||
|
||||
databasePath = databasePath.replace("{pluginDir}", core.getDataFolder().getAbsolutePath());
|
||||
|
||||
String jdbcUrl = "jdbc:";
|
||||
if (driver.contains("sqlite")) {
|
||||
jdbcUrl += "sqlite" + "://" + databasePath;
|
||||
databaseConfig.setConnectionTestQuery("SELECT 1");
|
||||
} else {
|
||||
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
|
||||
}
|
||||
|
||||
databaseConfig.setJdbcUrl(jdbcUrl);
|
||||
this.dataSource = new HikariDataSource(databaseConfig);
|
||||
}
|
||||
|
||||
public void createTables() throws SQLException {
|
||||
Connection con = null;
|
||||
Statement createStmt = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
createStmt = con.createStatement();
|
||||
|
||||
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
|
||||
+ "UserID INTEGER PRIMARY KEY AUTO_INCREMENT, "
|
||||
+ "UUID CHAR(36), "
|
||||
+ "Name VARCHAR(16) NOT NULL, "
|
||||
+ "Premium BOOLEAN NOT NULL, "
|
||||
+ "LastIp VARCHAR(255) NOT NULL, "
|
||||
+ "LastLogin TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
|
||||
//the premium shouldn't steal the cracked account by changing the name
|
||||
+ "UNIQUE (Name) "
|
||||
+ ")";
|
||||
|
||||
if (dataSource.getJdbcUrl().contains("sqlite")) {
|
||||
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
|
||||
}
|
||||
|
||||
createStmt.executeUpdate(createDataStmt);
|
||||
|
||||
//drop the old unique uuid index
|
||||
try {
|
||||
if (dataSource.getJdbcUrl().contains("sqlite")) {
|
||||
String tempTableCreate = createDataStmt.replace(PREMIUM_TABLE, PREMIUM_TABLE + "_TEMP")
|
||||
//if we already imported the table fail here
|
||||
.replace("IF NOT EXISTS", "");
|
||||
//create a temp table insert it there and then back
|
||||
createStmt.executeUpdate(tempTableCreate);
|
||||
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + "_TEMP SELECT * FROM " + PREMIUM_TABLE);
|
||||
|
||||
createStmt.executeUpdate("DROP TABLE " + PREMIUM_TABLE);
|
||||
createStmt.executeUpdate(createDataStmt);
|
||||
|
||||
//insert it back into the new table
|
||||
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + " SELECT * FROM " + PREMIUM_TABLE + "_TEMP");
|
||||
} else {
|
||||
createStmt.executeUpdate("ALTER TABLE premium DROP INDEX UUID");
|
||||
}
|
||||
} catch (SQLException sqlEx) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerProfile loadProfile(String name) {
|
||||
Connection con = null;
|
||||
PreparedStatement loadStmt = null;
|
||||
ResultSet resultSet = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE Name=? LIMIT 1");
|
||||
loadStmt.setString(1, name);
|
||||
|
||||
resultSet = loadStmt.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
long userId = resultSet.getInt(1);
|
||||
|
||||
String unparsedUUID = resultSet.getString(2);
|
||||
UUID uuid;
|
||||
if (unparsedUUID == null) {
|
||||
uuid = null;
|
||||
} else {
|
||||
uuid = FastLoginCore.parseId(unparsedUUID);
|
||||
}
|
||||
|
||||
boolean premium = resultSet.getBoolean(4);
|
||||
String lastIp = resultSet.getString(5);
|
||||
long lastLogin = resultSet.getTimestamp(6).getTime();
|
||||
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
|
||||
return playerProfile;
|
||||
} else {
|
||||
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
|
||||
return crackedProfile;
|
||||
}
|
||||
} catch (SQLException sqlEx) {
|
||||
core.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
closeQuietly(loadStmt);
|
||||
closeQuietly(resultSet);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public PlayerProfile loadProfile(UUID uuid) {
|
||||
Connection con = null;
|
||||
PreparedStatement loadStmt = null;
|
||||
ResultSet resultSet = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE UUID=? LIMIT 1");
|
||||
loadStmt.setString(1, uuid.toString().replace("-", ""));
|
||||
|
||||
resultSet = loadStmt.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
long userId = resultSet.getInt(1);
|
||||
|
||||
String name = resultSet.getString(3);
|
||||
boolean premium = resultSet.getBoolean(4);
|
||||
String lastIp = resultSet.getString(5);
|
||||
long lastLogin = resultSet.getTimestamp(6).getTime();
|
||||
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
|
||||
return playerProfile;
|
||||
}
|
||||
} catch (SQLException sqlEx) {
|
||||
core.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
closeQuietly(loadStmt);
|
||||
closeQuietly(resultSet);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean save(PlayerProfile playerProfile) {
|
||||
Connection con = null;
|
||||
PreparedStatement updateStmt = null;
|
||||
PreparedStatement saveStmt = null;
|
||||
|
||||
ResultSet generatedKeys = null;
|
||||
try {
|
||||
con = dataSource.getConnection();
|
||||
|
||||
UUID uuid = playerProfile.getUuid();
|
||||
|
||||
if (playerProfile.getUserId() == -1) {
|
||||
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=?");
|
||||
|
||||
updateStmt.setString(1, playerProfile.getPlayerName());
|
||||
updateStmt.setString(2, playerProfile.getLastIp());
|
||||
updateStmt.setString(3, uuid.toString().replace("-", ""));
|
||||
|
||||
int affectedRows = updateStmt.executeUpdate();
|
||||
if (affectedRows > 0) {
|
||||
//username changed and we updated the existing database record
|
||||
//so we don't need to run an insert
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
saveStmt = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
|
||||
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) ", Statement.RETURN_GENERATED_KEYS);
|
||||
|
||||
if (uuid == null) {
|
||||
saveStmt.setString(1, null);
|
||||
} else {
|
||||
saveStmt.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStmt.setString(2, playerProfile.getPlayerName());
|
||||
saveStmt.setBoolean(3, playerProfile.isPremium());
|
||||
saveStmt.setString(4, playerProfile.getLastIp());
|
||||
|
||||
saveStmt.execute();
|
||||
|
||||
generatedKeys = saveStmt.getGeneratedKeys();
|
||||
if (generatedKeys != null && generatedKeys.next()) {
|
||||
playerProfile.setUserId(generatedKeys.getInt(1));
|
||||
}
|
||||
} else {
|
||||
saveStmt = con.prepareStatement("UPDATE " + PREMIUM_TABLE
|
||||
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
|
||||
|
||||
if (uuid == null) {
|
||||
saveStmt.setString(1, null);
|
||||
} else {
|
||||
saveStmt.setString(1, uuid.toString().replace("-", ""));
|
||||
}
|
||||
|
||||
saveStmt.setString(2, playerProfile.getPlayerName());
|
||||
saveStmt.setBoolean(3, playerProfile.isPremium());
|
||||
saveStmt.setString(4, playerProfile.getLastIp());
|
||||
|
||||
saveStmt.setLong(5, playerProfile.getUserId());
|
||||
saveStmt.execute();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (SQLException ex) {
|
||||
core.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
|
||||
} finally {
|
||||
closeQuietly(con);
|
||||
closeQuietly(updateStmt);
|
||||
closeQuietly(saveStmt);
|
||||
closeQuietly(generatedKeys);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
dataSource.close();
|
||||
}
|
||||
|
||||
private void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception closeEx) {
|
||||
core.getLogger().log(Level.SEVERE, "Failed to close connection", closeEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.github.games647.fastlogin.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public abstract class FastLoginCore {
|
||||
|
||||
public static UUID parseId(String withoutDashes) {
|
||||
return UUID.fromString(withoutDashes.substring(0, 8)
|
||||
+ "-" + withoutDashes.substring(8, 12)
|
||||
+ "-" + withoutDashes.substring(12, 16)
|
||||
+ "-" + withoutDashes.substring(16, 20)
|
||||
+ "-" + withoutDashes.substring(20, 32));
|
||||
}
|
||||
|
||||
protected final Map<String, String> localeMessages = new ConcurrentHashMap<>();
|
||||
private MojangApiConnector mojangApiConnector;
|
||||
private AuthStorage storage;
|
||||
|
||||
public void setMojangApiConnector(MojangApiConnector mojangApiConnector) {
|
||||
this.mojangApiConnector = mojangApiConnector;
|
||||
}
|
||||
|
||||
public MojangApiConnector getMojangApiConnector() {
|
||||
return mojangApiConnector;
|
||||
}
|
||||
|
||||
public AuthStorage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public abstract File getDataFolder();
|
||||
|
||||
public abstract Logger getLogger();
|
||||
|
||||
public abstract ThreadFactory getThreadFactory();
|
||||
|
||||
public String getMessage(String key) {
|
||||
return localeMessages.get(key);
|
||||
}
|
||||
|
||||
public abstract void loadMessages();
|
||||
|
||||
public abstract void loadConfig();
|
||||
|
||||
public boolean setupDatabase(String driver, String host, int port, String database, String user, String password) {
|
||||
storage = new AuthStorage(this, driver, host, port, database, user, password);
|
||||
try {
|
||||
storage.createTables();
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (storage != null) {
|
||||
storage.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.games647.fastlogin.core;
|
||||
|
||||
public class LoginSession {
|
||||
|
||||
private final String username;
|
||||
private final PlayerProfile profile;
|
||||
protected boolean registered;
|
||||
|
||||
public LoginSession(String username, boolean registered, PlayerProfile profile) {
|
||||
this.username = username;
|
||||
this.registered = registered;
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* This value is always false if we authenticate the player with a cracked authentication
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean needsRegistration() {
|
||||
return !registered;
|
||||
}
|
||||
|
||||
public PlayerProfile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.github.games647.fastlogin.core;
|
||||
|
||||
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 = 3 * 1_000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
private static final String MCAPI_UUID_URL = "https://mcapi.ca/uuid/player/";
|
||||
|
||||
//only premium (paid account) users have a uuid from here
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
//this includes a-zA-Z1-9_
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
|
||||
private static final int RATE_LIMIT_CODE = 429;
|
||||
|
||||
//compile the pattern only on plugin enable -> and this have to be threadsafe
|
||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||
|
||||
private final ConcurrentMap<Object, Object> requests;
|
||||
private final BalancedSSLFactory sslFactory;
|
||||
private final int rateLimit;
|
||||
private long lastRateLimit;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param playerName
|
||||
* @return null on non-premium
|
||||
*/
|
||||
public UUID getPremiumUUID(String playerName) {
|
||||
//check if it's a valid playername
|
||||
if (playernameMatcher.matcher(playerName).matches()) {
|
||||
// only make a API call if the name is valid existing mojang account
|
||||
|
||||
if (requests.size() >= rateLimit || System.currentTimeMillis() - lastRateLimit < 1_000 * 60 * 10) {
|
||||
// plugin.getLogger().fine("STILL WAITING FOR RATE_LIMIT - TRYING Third-party API");
|
||||
return getUUIDFromAPI(playerName);
|
||||
}
|
||||
|
||||
requests.put(new Object(), new Object());
|
||||
|
||||
try {
|
||||
HttpsURLConnection connection = getConnection(UUID_LINK + playerName);
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != 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) {
|
||||
logger.log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public UUID getUUIDFromAPI(String playerName) {
|
||||
try {
|
||||
HttpURLConnection httpConnection = (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 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.games647.fastlogin.core;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PlayerProfile {
|
||||
|
||||
private final String playerName;
|
||||
|
||||
private long userId;
|
||||
|
||||
private UUID uuid;
|
||||
private boolean premium;
|
||||
private String lastIp;
|
||||
private long lastLogin;
|
||||
|
||||
public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium
|
||||
, String lastIp, long lastLogin) {
|
||||
this.userId = userId;
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
|
||||
public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
|
||||
this.userId = -1;
|
||||
|
||||
this.uuid = uuid;
|
||||
this.playerName = playerName;
|
||||
this.premium = premium;
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public synchronized long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public synchronized void setUserId(long generatedId) {
|
||||
this.userId = generatedId;
|
||||
}
|
||||
|
||||
public synchronized UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public synchronized void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public synchronized boolean isPremium() {
|
||||
return premium;
|
||||
}
|
||||
|
||||
public synchronized void setPremium(boolean premium) {
|
||||
this.premium = premium;
|
||||
}
|
||||
|
||||
public synchronized String getLastIp() {
|
||||
return lastIp;
|
||||
}
|
||||
|
||||
public synchronized void setLastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public synchronized long getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public synchronized void setLastLogin(long lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.github.games647.fastlogin.core.importer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public class AutoInImporter extends Importer {
|
||||
|
||||
private static final String USER_TABLE = "nicknames";
|
||||
private static final String UUID_TABLE = "uuids";
|
||||
private static final String SESSION_TABLE = "sessions";
|
||||
|
||||
@Override
|
||||
public int importData(DataSource source, DataSource target, String targetTable) throws SQLException {
|
||||
Connection con = null;
|
||||
Statement stmt = null;
|
||||
try {
|
||||
con = source.getConnection();
|
||||
stmt = con.createStatement();
|
||||
int importedRows = stmt.executeUpdate("INSERT INTO " + targetTable + " (Name, Premium, LastIp, UUID) SELECT"
|
||||
+ " name AS Name,"
|
||||
/* 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
|
||||
/* 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);
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.github.games647.fastlogin.core.importer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public class BPAImporter extends Importer {
|
||||
|
||||
private static final String DEFAULT_TABLE_NAME = "users";
|
||||
|
||||
@Override
|
||||
public int importData(DataSource source, DataSource target, String targetTable) throws SQLException {
|
||||
Connection con = null;
|
||||
Statement stmt = null;
|
||||
try {
|
||||
con = source.getConnection();
|
||||
stmt = con.createStatement();
|
||||
int importedRows = stmt.executeUpdate("INSERT INTO " + targetTable + " SELECT"
|
||||
+ " nick AS Name,"
|
||||
+ " NULL AS UUID,"
|
||||
+ " checked AS Premium,"
|
||||
+ " lastIP AS LastIp,"
|
||||
+ " FROM_UNIXTIME(lastJoined * 0.001) AS LastLogin"
|
||||
+ " FROM " + DEFAULT_TABLE_NAME);
|
||||
return importedRows;
|
||||
} finally {
|
||||
closeQuietly(stmt);
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.github.games647.fastlogin.core.importer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public class ElDziAuthImporter extends Importer {
|
||||
|
||||
private static final String TABLE_NAME = "accounts";
|
||||
|
||||
@Override
|
||||
public int importData(DataSource source, DataSource target, String targetTable) throws SQLException {
|
||||
Connection con = null;
|
||||
Statement stmt = null;
|
||||
try {
|
||||
con = source.getConnection();
|
||||
stmt = con.createStatement();
|
||||
int importedRows = stmt.executeUpdate("INSERT INTO " + targetTable + " SELECT"
|
||||
+ " nick AS Name,"
|
||||
+ " uuid AS UUID,"
|
||||
+ " premium AS Premium,"
|
||||
+ " lastIp AS LastIp,"
|
||||
+ " FROM_UNIXTIME(lastPlayed * 0.001) AS LastLogin"
|
||||
+ " FROM " + TABLE_NAME);
|
||||
return importedRows;
|
||||
} finally {
|
||||
closeQuietly(stmt);
|
||||
closeQuietly(con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.github.games647.fastlogin.core.importer;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public abstract class Importer {
|
||||
|
||||
public abstract int importData(DataSource source, DataSource target, String targetTable) throws SQLException;
|
||||
|
||||
protected void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception ignore) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
core/src/main/resources/config.yml
Normal file
146
core/src/main/resources/config.yml
Normal file
@@ -0,0 +1,146 @@
|
||||
# FastLogin config
|
||||
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
|
||||
# Source code: https://github.com/games647/FastLogin
|
||||
#
|
||||
# You can access the newest config here:
|
||||
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/config.yml
|
||||
|
||||
# Request a premium login without forcing the player to type a command
|
||||
#
|
||||
# If you activate autoRegister, this plugin will check/do these points on login:
|
||||
# 1. An existing cracked account shouldn't exist
|
||||
# -> paid accounts cannot steal the existing account of cracked players
|
||||
# - (Already registered players could still use the /premium command to activate premium checks)
|
||||
# 2. Automatically registers an account with a strong random generated password
|
||||
# -> cracked player cannot register an account for the premium player and so cannot the steal the account
|
||||
#
|
||||
# Furthermore the premium player check have to be made based on the player name
|
||||
# This means if a cracked player connects to the server and we request a paid account login from this player
|
||||
# the player just disconnect and sees the message: 'bad login' or 'invalid session'
|
||||
# There is no way to change this message
|
||||
# For more information: https://github.com/games647/FastLogin#why-do-players-have-to-invoke-a-command
|
||||
autoRegister: false
|
||||
|
||||
# If this plugin detected that a player has a premium, it can also set the associated
|
||||
# uuid from that account. So if the players changes their usernames, they will still have
|
||||
# the same playerdata (inventory, permissions, ...)
|
||||
#
|
||||
# Warning: This also means that the UUID will be different if the player is connecting
|
||||
# through a offline mode connection. This **could** cause plugin compatibility issues.
|
||||
#
|
||||
# This is a example and doesn't apply for every plugin.
|
||||
# Example: If you want to ban players who aren't online at the moment, the ban plugin will look
|
||||
# after a offline uuid associated to the player, because the server is in offline mode. Then the premium
|
||||
# players could still join the server, because they have different UUID.
|
||||
#
|
||||
# Moreover you may want to convert the offline UUID to a premium UUID. This will ensure that the player
|
||||
# will have the same inventory, permissions, ... if they switched to premium authentification from offline/cracked
|
||||
# authentification.
|
||||
#
|
||||
# This feature requires Cauldron, Spigot or a fork of Spigot (PaperSpigot, TacoSpigot)
|
||||
premiumUuid: false
|
||||
|
||||
# This will make an additional check (only for player names which are not in the database) against the mojang servers
|
||||
# in order to get the premium UUID. If that premium UUID is in the database, we can assume on sucessful login that the
|
||||
# player changed it's username and we just update the name in the database.
|
||||
# Examples:
|
||||
# #### Case 1
|
||||
# nameChangeCheck = false ----- autoRegister = false
|
||||
#
|
||||
# Player logins as cracked until the player invoked the command /premium. Then we could override the existing database
|
||||
# record.
|
||||
#
|
||||
# #### Case 2
|
||||
#
|
||||
# nameChangeCheck = true ----- autoRegister = false
|
||||
#
|
||||
# Connect the Mojang API and check what UUID the player has (UUID exists => Paid Minecraft account). If that UUID is in
|
||||
# the database it's an **existing player** and FastLogin can **assume** the player is premium and changed the username.
|
||||
# If it's not in the database, it's a new player and **could be a cracked player**. So we just use a offline mode
|
||||
# authentication for this player.
|
||||
#
|
||||
# **Limitation**: Cracked players who uses the new username of a paid account cannot join the server if the database
|
||||
# contains the old name. (Example: The owner of the paid account no longer plays on the server, but changed the username
|
||||
# in the meanwhile).
|
||||
#
|
||||
# #### Case 3
|
||||
#
|
||||
# nameChangeCheck = false ----- autoRegister = true
|
||||
#
|
||||
# We will always request a premium authentication if the username is unknown to us, but is in use by a paid minecraft
|
||||
# account. This means it's kind of a more aggressive check like nameChangeCheck = true and autoRegister = false, because
|
||||
# it request a premium authentication which are completely new to us, that even the premium UUID is not in our database.
|
||||
#
|
||||
# **Limitation**: see below
|
||||
#
|
||||
# #### Case 4
|
||||
#
|
||||
# nameChangeCheck = true ----- autoRegister = true
|
||||
#
|
||||
# Based on autoRegister it checks if the player name is premium and login using a premium authentication. After that
|
||||
# fastlogin receives the premium UUID and can update the database record.
|
||||
#
|
||||
# **Limitation from autoRegister**: New offline players who uses the username of an existing minecraft cannot join the
|
||||
# server.
|
||||
nameChangeCheck: false
|
||||
|
||||
# If your players have a premium account and a skin associated to their account, this plugin
|
||||
# can download the data and set it to the online player.
|
||||
#
|
||||
# Keep in mind that this will only works if the player:
|
||||
# * is the owner of the premium account
|
||||
# * the serverconnection is established through a premium connection (paid account authentification)
|
||||
# * has a skin
|
||||
#
|
||||
# This means this plugin doesn't need to create a new connection to the Mojang servers, because
|
||||
# the skin data is included in the Auth-Verification-Response sent by Mojang. If you want to use for other
|
||||
# players like cracked player, you have to use other plugins.
|
||||
#
|
||||
# If you want to use skins for your cracked player, you need an additional plugin like
|
||||
# ChangeSkin, SkinRestoer, ...
|
||||
forwardSkin: true
|
||||
|
||||
# Displays a warning message that this message SHOULD only be invoked by
|
||||
# users who actually are the owner of this account. So not by cracked players
|
||||
#
|
||||
# If they still want to invoke the command, they have to invoke /premium again
|
||||
premium-warning: true
|
||||
|
||||
# If you have autoRegister or nameChangeCheck enabled, you could be rate-limited by Mojang.
|
||||
# The requests of the both options will be only made by FastLogin if the username is unknown to the server
|
||||
# You are allowed to make 600 requests per 10-minutes (60 per minute)
|
||||
# If you own a big server this value could be too low
|
||||
# Once the limit is reached, new players are always logged in as cracked until the rate-limit is expired.
|
||||
# (to the next ten minutes)
|
||||
#
|
||||
# The limit is IP-wide. If you have multiple IPv4-Addreses you specify them here. FastLogin will then use it in rotating
|
||||
# order --> 5 different IP-addreses 5 * 600 per 10 minutes
|
||||
# If this list is empty only the default one will be used
|
||||
#
|
||||
# Lists are created like this:
|
||||
#ip-addresses:
|
||||
# - 192-168-0-2
|
||||
ip-addresses: []
|
||||
|
||||
# How many requests should be established until the plugin uses the third-party API https://mcapi.ca/
|
||||
# Once this number is reached in a range of ten minutes it will start connecting to https://mcapi.ca/ for the next ten minutes
|
||||
# This option exists in order to workaround the rate-limiting. Name -> UUID are fetched in the same way like heads
|
||||
#
|
||||
# If you want to join the discussion visit this: https://github.com/games647/FastLogin/issues/27#issuecomment-226954350
|
||||
mojang-request-limit: 600
|
||||
|
||||
# Database configuration
|
||||
# Recommened is the use of MariaDB (a better version of MySQL)
|
||||
|
||||
# Single file SQLite database
|
||||
driver: org.sqlite.JDBC
|
||||
# File location
|
||||
database: '{pluginDir}/FastLogin.db'
|
||||
|
||||
# MySQL
|
||||
#driver: com.mysql.jdbc.Driver
|
||||
#host: localhost
|
||||
#port: 3306
|
||||
#database: fastlogin
|
||||
#username: myUser
|
||||
#password: myPassword
|
||||
92
core/src/main/resources/messages.yml
Normal file
92
core/src/main/resources/messages.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
# FastLogin localization
|
||||
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
|
||||
# Source code: https://github.com/games647/FastLogin
|
||||
#
|
||||
# You can access the newest locale here:
|
||||
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/messages.yml
|
||||
#
|
||||
# You want to have language template? Visit the Github Wiki here:
|
||||
# https://github.com/games647/FastLogin/wiki/English
|
||||
|
||||
# In order to split a message into seperate lines you could just make a new line, but keep the '
|
||||
# Example:
|
||||
# bla: '&aFirst line
|
||||
# Second line
|
||||
# Third line'
|
||||
|
||||
# If you want to disable a message, you can just set it to a empty value.
|
||||
# In this case no message will be sent
|
||||
# Example:
|
||||
# bla: ''
|
||||
|
||||
# ========= Shared (BungeeCord and Bukkit) ============
|
||||
|
||||
# Player activated premium logins in order to skip offline authentication
|
||||
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'
|
||||
|
||||
# ========= Bukkit/Spigot/PaperSpigot/TacoSpigot only ================================
|
||||
|
||||
# The user skipped the authentication, because it was a premium player
|
||||
auto-login: '&2Auto logged in'
|
||||
|
||||
# The user was auto registered on the first join. The user account will be registered to protect it from cracked players
|
||||
# The password can be used if the mojang servers are down and you still want your premium users to login (PLANNED)
|
||||
auto-register: '&2Auto registered with password: %password
|
||||
You may want change it?'
|
||||
|
||||
# Player is not able to toggle the premium state of other players
|
||||
no-permission: '&4Not enough permissions'
|
||||
|
||||
# Although the console can toggle the premium state, it's not possible for the console itself.
|
||||
# Because the console is not a user. (obviously, isn't it?)
|
||||
no-console: '&4You are not a player. You cannot toggle the premium state for YOURSELF as a console'
|
||||
|
||||
# The user wants to toggle premium state, but BungeeCord support is enabled. This means the server have to communicate
|
||||
# with the BungeeCord first which will establish a connection with the database server.
|
||||
wait-on-proxy: '&6Sending request...'
|
||||
|
||||
# When ProtocolLib is enabled and the plugin is unable to continue handling a login request after a requested premium
|
||||
# authentication. In this state the client expects a success packet with a encrypted connection or disconnect packet.
|
||||
# So we kick the player, if we cannot encrypt the connection. In other situation (example: premium name check),
|
||||
# the player will be just authenticated as cracked
|
||||
error-kick: '&4Error occured'
|
||||
|
||||
# The server sents a verify token within the premium authentication reqest. If this doesn't match on response,
|
||||
# it could be another client sending malicious packets
|
||||
invalid-verify-token: '&4Invalid token'
|
||||
|
||||
# The client sent no request join server request to the mojang servers which would proof that it's owner of that
|
||||
# acciunt. Only modified clients would do this.
|
||||
invalid-session: '&4Invalid session'
|
||||
|
||||
# The client sent a malicous packet without a login request packet
|
||||
invalid-requst: '&4Invalid request'
|
||||
|
||||
# Message if the bukkit isn't fully started to inject the packets
|
||||
not-started: '&cServer is not fully started yet. Please retry'
|
||||
|
||||
# Warning message if a user invoked /premium command
|
||||
premium-warning: '&c&lWARNING: &6This command should &lonly&6 be invoked if you are the owner of this paid minecraft account
|
||||
Type &a/premium&6 again to confirm'
|
||||
|
||||
# ========= Bungee/Waterfall only ================================
|
||||
|
||||
|
||||
Binary file not shown.
114
pom.xml
114
pom.xml
@@ -5,12 +5,12 @@
|
||||
<groupId>com.github.games647</groupId>
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>FastLogin</name>
|
||||
<version>0.2.2</version>
|
||||
<version>1.7.1</version>
|
||||
<inceptionYear>2015</inceptionYear>
|
||||
<url>https://github.com/games647/FastLogin</url>
|
||||
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
|
||||
<description>
|
||||
Automatically logins premium (paid accounts) player on a offline mode server
|
||||
</description>
|
||||
@@ -21,6 +21,13 @@
|
||||
<outputDir>${basedir}/target</outputDir>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>bukkit</module>
|
||||
<module>bungee</module>
|
||||
<module>universal</module>
|
||||
</modules>
|
||||
|
||||
<issueManagement>
|
||||
<system>GitHub</system>
|
||||
<url>https://github.com/games647/FastLogin/issues</url>
|
||||
@@ -41,21 +48,19 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.2</version>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
<showWarnings>true</showWarnings>
|
||||
<showDeprecation>true</showDeprecation>
|
||||
<!--false means actual true http://jira.codehaus.org/browse/MCOMPILER-209-->
|
||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<outputDirectory>${outputDir}</outputDirectory>
|
||||
</configuration>
|
||||
@@ -71,103 +76,12 @@
|
||||
|
||||
<!--Add the license to jar in order to see it in the final jar-->
|
||||
<resource>
|
||||
<directory>${basedir}</directory>
|
||||
<!--Parent folder-->
|
||||
<directory>${basedir}/..</directory>
|
||||
<includes>
|
||||
<include>LICENSE</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<!--Bukkit-Server-API -->
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--ProtocolLib-->
|
||||
<repository>
|
||||
<id>dmulloy2-repo</id>
|
||||
<url>http://repo.dmulloy2.net/content/groups/public/</url>
|
||||
</repository>
|
||||
|
||||
<!--Authme Reloaded-->
|
||||
<repository>
|
||||
<id>xephi-repo</id>
|
||||
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--xAuth-->
|
||||
<repository>
|
||||
<id>luricos.de-repo</id>
|
||||
<url>http://repo.luricos.de/bukkit-plugins/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!--Server API-->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.8.8-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Library for listening and sending Minecraft packets-->
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>3.6.5-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!--Login Plugins-->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.luricos.bukkit</groupId>
|
||||
<artifactId>xAuth</artifactId>
|
||||
<version>2.6</version>
|
||||
<!--These artifacts produce conflicts on downloading-->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>net.gravitydevelopment.updater</groupId>
|
||||
<artifactId>updater</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>net.ess3</groupId>
|
||||
<artifactId>EssentialsGroupManager</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyCore</artifactId>
|
||||
<version>10.7.7</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyCore v10.7.7.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyLogin</artifactId>
|
||||
<version>7.23</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.lenis0012.ls</groupId>
|
||||
<artifactId>LoginSecurity</artifactId>
|
||||
<version>2.0.10</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/LoginSecurity v2.0.10.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import com.github.games647.fastlogin.listener.PlayerListener;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.utility.SafeCacheBuilder;
|
||||
import com.github.games647.fastlogin.hooks.AuthPlugin;
|
||||
import com.github.games647.fastlogin.listener.EncryptionPacketListener;
|
||||
import com.github.games647.fastlogin.listener.StartPacketListener;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class FastLogin extends JavaPlugin {
|
||||
|
||||
private static final int TIMEOUT = 15000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
//provide a immutable key pair to be thread safe
|
||||
private final KeyPair keyPair = Encryption.generateKeyPair();
|
||||
|
||||
//we need a thread-safe set because we access it async in the packet listener
|
||||
private final Set<String> enabledPremium = Sets.newConcurrentHashSet();
|
||||
|
||||
//this map is thread-safe for async access (Packet Listener)
|
||||
//SafeCacheBuilder is used in order to be version independent
|
||||
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
|
||||
//mapped by ip:port
|
||||
.expireAfterWrite(2, TimeUnit.MINUTES)
|
||||
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
|
||||
.build(new CacheLoader<String, PlayerSession>() {
|
||||
|
||||
@Override
|
||||
public PlayerSession load(String key) throws Exception {
|
||||
//A key should be inserted manually on start packet
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
//online mode is only changeable after a restart so check it here
|
||||
if (getServer().getOnlineMode()) {
|
||||
getLogger().severe("Server have to be in offline mode");
|
||||
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (!isEnabled() || !registerHooks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//register packet listeners on success
|
||||
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
|
||||
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
|
||||
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
|
||||
|
||||
//register commands
|
||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
//clean up
|
||||
session.clear();
|
||||
enabledPremium.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
|
||||
* account)
|
||||
*
|
||||
* @return a thread-safe session map
|
||||
*/
|
||||
public ConcurrentMap<String, PlayerSession> getSessions() {
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server KeyPair
|
||||
*
|
||||
* @return the server KeyPair
|
||||
*/
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of user who activated premium logins
|
||||
*
|
||||
* @return user who activated premium logins
|
||||
*/
|
||||
public Set<String> getEnabledPremium() {
|
||||
return enabledPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a Mojang API connection. The connection is not started in this method
|
||||
*
|
||||
* @param url the url connecting to
|
||||
* @return the prepared connection
|
||||
*
|
||||
* @throws IOException on invalid url format or on {@link java.net.URL#openConnection() }
|
||||
*/
|
||||
public HttpURLConnection getConnection(String url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
connection.setReadTimeout(TIMEOUT);
|
||||
//the new Mojang API just uses json as response
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private boolean registerHooks() {
|
||||
AuthPlugin authPluginHook = null;
|
||||
try {
|
||||
String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
|
||||
//Look through all classes in the hooks package and look for supporting plugins on the server
|
||||
for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
|
||||
//remove the hook suffix
|
||||
String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
|
||||
Class<?> clazz = clazzInfo.load();
|
||||
//uses only member classes which uses AuthPlugin interface (skip interfaces)
|
||||
if (AuthPlugin.class.isAssignableFrom(clazz)
|
||||
&& getServer().getPluginManager().isPluginEnabled(pluginName)) {
|
||||
authPluginHook = (AuthPlugin) clazz.newInstance();
|
||||
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException | IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
|
||||
}
|
||||
|
||||
if (authPluginHook == null) {
|
||||
//run this check for exceptions and not found plugins
|
||||
getLogger().warning("No support offline Auth plugin found. ");
|
||||
getLogger().warning("Disabling this plugin...");
|
||||
|
||||
setEnabled(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
//We found a supporting plugin - we can now register a forwarding listener
|
||||
getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
/**
|
||||
* Represents a client connecting to the server.
|
||||
*
|
||||
* This session is invalid if the player disconnects or the login was successful
|
||||
*/
|
||||
public class PlayerSession {
|
||||
|
||||
private final byte[] verifyToken;
|
||||
private final String username;
|
||||
private boolean verified;
|
||||
|
||||
public PlayerSession(byte[] verifyToken, String username) {
|
||||
this.username = username;
|
||||
this.verifyToken = verifyToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the verify token the server sent to the client.
|
||||
*
|
||||
* @return the verify token from the server
|
||||
*/
|
||||
public byte[] getVerifyToken() {
|
||||
return verifyToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the username the player sent to the server
|
||||
*
|
||||
* @return the client sent username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @param verified whether the player has valid session
|
||||
*/
|
||||
public synchronized void setVerified(boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the player has a premium (paid account) account
|
||||
* and valid session
|
||||
*
|
||||
* @return whether the player has a valid session
|
||||
*/
|
||||
public synchronized boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PremiumCommand implements CommandExecutor {
|
||||
|
||||
private final FastLogin plugin;
|
||||
|
||||
public PremiumCommand(FastLogin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Only players can add themselves as premium");
|
||||
return true;
|
||||
}
|
||||
|
||||
String playerName = sender.getName();
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sender.hasPermission(plugin.getName() + ".command." + command.getName() + ".others")) {
|
||||
String playerName = args[0];
|
||||
//todo check if valid username
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added "
|
||||
+ ChatColor.DARK_BLUE + ChatColor.BOLD + playerName
|
||||
+ ChatColor.RESET + ChatColor.DARK_GREEN + " to the list of premium players");
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Not enough permissions");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
import fr.xephi.authme.cache.limbo.LimboCache;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/Xephi/AuthMeReloaded/
|
||||
* Project page: http://dev.bukkit.org/bukkit-plugins/authme-reloaded/
|
||||
*/
|
||||
public class AuthMeHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
//here is the gamemode, inventory ... saved
|
||||
if (!LimboCache.getInstance().hasLimboPlayer(player.getName().toLowerCase())) {
|
||||
//add cache entry - otherwise logging in wouldn't work
|
||||
LimboCache.getInstance().addLimboPlayer(player);
|
||||
}
|
||||
|
||||
//skips registration and login
|
||||
NewAPI.getInstance().forceLogin(player);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Represents a supporting authentication plugin
|
||||
*/
|
||||
public interface AuthPlugin {
|
||||
|
||||
/**
|
||||
* Login the premium (paid account) player
|
||||
*
|
||||
* @param player the player that needs to be logged in
|
||||
*/
|
||||
void forceLogin(Player player);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import de.st_ddt.crazylogin.CrazyLogin;
|
||||
import de.st_ddt.crazylogin.data.LoginPlayerData;
|
||||
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/ST-DDT/CrazyLogin
|
||||
* Project page: http://dev.bukkit.org/server-mods/crazylogin/
|
||||
*/
|
||||
public class CrazyLoginHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
|
||||
CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
|
||||
|
||||
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
|
||||
if (playerData == null) {
|
||||
//create a fake account - this will be saved to the database with the password=FAILEDLOADING
|
||||
//user cannot login with that password unless the admin uses plain text
|
||||
playerData = new LoginPlayerData(player);
|
||||
crazyDatabase.save(playerData);
|
||||
} else {
|
||||
//mark the account as logged in
|
||||
playerData.setLoggedIn(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import com.lenis0012.bukkit.ls.LoginSecurity;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
|
||||
* Project page: https://github.com/lenis0012/LoginSecurity-2
|
||||
*
|
||||
* on join:
|
||||
* https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282
|
||||
*/
|
||||
public class LoginSecurityHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
//Login command of this plugin: (How the plugin logs the player in)
|
||||
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39
|
||||
LoginSecurity securityPlugin = LoginSecurity.instance;
|
||||
String name = player.getName().toLowerCase();
|
||||
|
||||
//mark the user as logged in
|
||||
securityPlugin.authList.remove(name);
|
||||
//cancel timeout timer
|
||||
securityPlugin.thread.timeout.remove(name);
|
||||
//remove effects
|
||||
securityPlugin.rehabPlayer(player, name);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import de.luricos.bukkit.xAuth.xAuth;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer;
|
||||
import de.luricos.bukkit.xAuth.xAuthPlayer.Status;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/LycanDevelopment/xAuth/
|
||||
* Project page: http://dev.bukkit.org/bukkit-plugins/xauth/
|
||||
*/
|
||||
public class xAuthHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player player) {
|
||||
xAuth xAuthPlugin = xAuth.getPlugin();
|
||||
|
||||
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
|
||||
//we checked that the player is premium (paid account)
|
||||
xAuthPlayer.setPremium(true);
|
||||
//mark the player online
|
||||
xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName());
|
||||
|
||||
//update last login time
|
||||
xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis()));
|
||||
|
||||
//mark the player as logged in
|
||||
xAuthPlayer.setStatus(Status.AUTHENTICATED);
|
||||
|
||||
//restore inventory
|
||||
xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.github.games647.fastlogin.Encryption;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
/**
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Encryption_Response
|
||||
*
|
||||
* sharedSecret=encrypted byte array
|
||||
* verify token=encrypted byte array
|
||||
*/
|
||||
public class EncryptionPacketListener extends PacketAdapter {
|
||||
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need this type
|
||||
private final FastLogin plugin;
|
||||
|
||||
public EncryptionPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
||||
//run async in order to not block the server, because we make api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*
|
||||
* Minecraft Server implementation
|
||||
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//the player name is unknown to ProtocolLib - now uses ip:port as key
|
||||
String uniqueSessionKey = player.getAddress().toString();
|
||||
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
|
||||
if (session == null) {
|
||||
disconnect(packetEvent, "Invalid request", Level.FINE
|
||||
, "Player {0} tried to send encryption response"
|
||||
+ "on an invalid connection state"
|
||||
, player.getAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] sharedSecret = packet.getByteArrays().read(0);
|
||||
byte[] clientVerify = packet.getByteArrays().read(1);
|
||||
|
||||
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
|
||||
byte[] serverVerify = session.getVerifyToken();
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
|
||||
if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
|
||||
//check if the verify token are equal to the server sent one
|
||||
disconnect(packetEvent, "Invalid token", Level.FINE
|
||||
, "Player {0} ({1}) tried to login with an invalid verify token. "
|
||||
+ "Server: {2} Client: {3}"
|
||||
, session.getUsername(), player.getAddress(), serverVerify, clientVerify);
|
||||
return;
|
||||
}
|
||||
|
||||
SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
|
||||
try {
|
||||
//get the NMS connection handle of this player
|
||||
Object networkManager = getNetworkManager(player);
|
||||
|
||||
//try to detect the method by parameters
|
||||
Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager)
|
||||
.getMethodByParameters("a", SecretKey.class);
|
||||
|
||||
//encrypt/decrypt following packets
|
||||
//the client expects this behaviour
|
||||
encryptConnectionMethod.invoke(networkManager, loginKey);
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
|
||||
//generate the server id based on client and server data
|
||||
String serverId = (new BigInteger(Encryption.getServerIdHash("", plugin.getKeyPair().getPublic(), loginKey)))
|
||||
.toString(16);
|
||||
|
||||
String username = session.getUsername();
|
||||
if (hasJoinedServer(username, serverId)) {
|
||||
session.setVerified(true);
|
||||
|
||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||
|
||||
receiveFakeStartPacket(username, player);
|
||||
} else {
|
||||
//user tried to fake a authentification
|
||||
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||
, session.getUsername(), player.getAddress(), serverId);
|
||||
}
|
||||
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
|
||||
, Object... arguments) {
|
||||
plugin.getLogger().log(logLevel, logMessage, arguments);
|
||||
packetEvent.getPlayer().kickPlayer(kickMessage);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private Object getNetworkManager(Player player)
|
||||
throws SecurityException, IllegalAccessException, NoSuchFieldException {
|
||||
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
||||
Field injectorField = injector.getClass().getDeclaredField("injector");
|
||||
injectorField.setAccessible(true);
|
||||
|
||||
Object rawInjector = injectorField.get(injector);
|
||||
|
||||
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||
injectorField.setAccessible(true);
|
||||
return injectorField.get(rawInjector);
|
||||
}
|
||||
|
||||
private boolean hasJoinedServer(String username, String serverId) {
|
||||
try {
|
||||
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
|
||||
|
||||
HttpURLConnection conn = plugin.getConnection(url);
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (!line.equals("null")) {
|
||||
//validate parsing
|
||||
JSONObject object = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) object.get("id");
|
||||
String name = (String) object.get("name");
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
//catch not only ioexceptions also parse and NPE on unexpected json format
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to verify if session is valid", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void receiveFakeStartPacket(String username, Player from) {
|
||||
//fake a new login packet
|
||||
//see StartPacketListener for packet information
|
||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
|
||||
|
||||
WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
|
||||
startPacket.getGameProfiles().write(0, fakeProfile);
|
||||
try {
|
||||
protocolManager.recieveClientPacket(from, startPacket, false);
|
||||
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
from.kickPlayer("Error occurred");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
import com.github.games647.fastlogin.hooks.AuthPlugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
public class PlayerListener implements Listener {
|
||||
|
||||
private final FastLogin plugin;
|
||||
private final AuthPlugin authPlugin;
|
||||
|
||||
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.authPlugin = authPlugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onJoin(PlayerJoinEvent joinEvent) {
|
||||
final Player player = joinEvent.getPlayer();
|
||||
String address = player.getAddress().toString();
|
||||
|
||||
//removing the session because we now use it
|
||||
PlayerSession session = plugin.getSessions().remove(address);
|
||||
//check if it's the same player as we checked before
|
||||
if (session != null && session.getUsername().equals(player.getName())
|
||||
&& session.isVerified()) {
|
||||
// Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
// if (player.isOnline()) {
|
||||
// plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName());
|
||||
// authPlugin.forceLogin(player);
|
||||
// }
|
||||
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (player.isOnline()) {
|
||||
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
|
||||
authPlugin.forceLogin(player);
|
||||
}
|
||||
}
|
||||
//Wait before auth plugin initializes the player
|
||||
}, 1 * 20L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Login_Start
|
||||
*
|
||||
* String=Username
|
||||
*/
|
||||
public class StartPacketListener extends PacketAdapter {
|
||||
|
||||
//only premium (paid account) users have a uuid from there
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
//this includes a-zA-Z1-9_
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
|
||||
private final FastLogin plugin;
|
||||
|
||||
//just create a new once on plugin enable
|
||||
private final Random random = new Random();
|
||||
//compile the pattern on plugin enable
|
||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||
|
||||
public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
||||
//run async in order to not block the server, because we make api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//this includes ip and port. Should be unique for 2 Minutes
|
||||
String sessionKey = player.getAddress().toString();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(sessionKey);
|
||||
|
||||
String username = packet.getGameProfiles().read(0).getName();
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
||||
, new Object[]{sessionKey, username});
|
||||
//do premium login process
|
||||
if (isPremium(username)) {
|
||||
//minecraft server implementation
|
||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
||||
sentEncryptionRequest(sessionKey, username, player, packetEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPremium(String playerName) {
|
||||
//check if it's a valid playername and the user activated fast logins
|
||||
if (playernameMatcher.matcher(playerName).matches() && plugin.getEnabledPremium().contains(playerName)) {
|
||||
//only make a API call if the name is valid existing mojang account
|
||||
try {
|
||||
HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName);
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
return responseCode == HttpURLConnection.HTTP_OK;
|
||||
//204 - no content for not found
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sentEncryptionRequest(String sessionKey, String username, Player player, PacketEvent packetEvent) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
try {
|
||||
/**
|
||||
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
|
||||
*
|
||||
* ServerID="" (String)
|
||||
* key=public server key
|
||||
* verifyToken=random 4 byte array
|
||||
*/
|
||||
PacketContainer newPacket = protocolManager
|
||||
.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
|
||||
|
||||
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic());
|
||||
byte[] verifyToken = new byte[4];
|
||||
random.nextBytes(verifyToken);
|
||||
newPacket.getByteArrays().write(0, verifyToken);
|
||||
|
||||
protocolManager.sendServerPacket(player, newPacket, false);
|
||||
|
||||
//cancel only if the player has a paid account otherwise login as normal offline player
|
||||
packetEvent.setCancelled(true);
|
||||
plugin.getSessions().put(sessionKey, new PlayerSession(verifyToken, username));
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# project informations for Bukkit in order to register our plugin with all it components
|
||||
# ${project.name} are variables from Maven (pom.xml) which will be replaced after the build
|
||||
name: ${project.name}
|
||||
version: ${project.version}
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
# meta informations for plugin managers
|
||||
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
|
||||
description: |
|
||||
${project.description}
|
||||
website: ${project.url}
|
||||
dev-url: ${project.url}
|
||||
|
||||
depend: [ProtocolLib]
|
||||
softdepend:
|
||||
- xAuth
|
||||
- AuthMe
|
||||
- CrazyLogin
|
||||
- LoginSecurity
|
||||
|
||||
commands:
|
||||
premium:
|
||||
description: 'Marks the invoker or the player specified as premium'
|
||||
aliases: [prem, fastlogin, loginfast]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.premium
|
||||
|
||||
permissions:
|
||||
${project.artifactId}.command.premium:
|
||||
description: 'Mark themselves as premium using a command'
|
||||
default: true
|
||||
${project.artifactId}.command.premium.others:
|
||||
description: 'Mark other people as premium'
|
||||
68
universal/pom.xml
Normal file
68
universal/pom.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.github.games647</groupId>
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>fastlogin-universal</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLoginUniversal</name>
|
||||
|
||||
<build>
|
||||
<defaultGoal>package</defaultGoal>
|
||||
<finalName>${project.parent.name}</finalName>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.4.3</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>${project.groupId}:*</include>
|
||||
<include>com.zaxxer:HikariCP</include>
|
||||
<include>org.slf4j:*</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.bukkit</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>fastlogin.bungee</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Reference in New Issue
Block a user