Compare commits

...

175 Commits
1.1.1 ... 1.10

Author SHA1 Message Date
games647
484855724b Add ip parameter to verify a player doesn't use an authentication proxy.
This doesn't prevent proxy connections in general, but it verifies that
the same IP that is used for connecting to the Minecraft server is also
used for authenticating against the Mojang servers.

This happens if someone uses McLeaks. They use an authentication proxy
in order to hide and control the credentials behind those leaked or
donated accounts. So a user of that service joins the server using
a direct connection, but asks the McLeaks servers to send a relevant
request to the Mojang session-servers in order to pass the premium
verification process.
2017-08-25 13:20:55 +02:00
games647
4ea7968366 Remove Importer to prepare for code refactor 2017-08-24 18:50:37 +02:00
games647
44a47bc97f Set default value for proxies 2017-08-20 21:40:37 +02:00
games647
82cb25f809 Output more informational messages by default 2017-08-19 21:53:07 +02:00
games647
551441cdc4 Add HTTP-proxies support 2017-08-18 16:09:59 +02:00
games647
22a56862b0 Remove mcapi.ca section and fix config typos 2017-08-16 17:18:58 +02:00
games647
edf5933e07 Set the fake offline UUID on lowest priority (-> as soon as possible)
Then every plugin listening on priority level higher than lowest can see that fake UUID

This also fixes race conditions for plugins listening on the same priority as FastLogin before (->low)
(Fixes #167)
2017-08-01 10:29:58 +02:00
games647
c6da04de70 Fix listening for login start packets if ProtocolLib is installed
Another call on ProtocolLib's types removes all previous listening types

Fixes #163
2017-07-25 13:18:08 +02:00
games647
0459b0a5a1 Remove bungee chatcolor for Bukkit to support KCauldron 2017-07-22 08:35:32 +02:00
games647
033333e35c Minor cleanup using inspections + Https
* Use https for maven repositories if possible
* Fix typos
* Merge ProtocolLib listeners into one class
* Upgrade maven plugins and dependencies
2017-07-22 08:27:55 +02:00
games647
6595dc6ac0 Increase hook delay to let ProtocolLib inject the listener 2017-06-30 17:37:57 +02:00
games647
ea44002e91 Update dependencies and format imports 2017-06-30 17:23:46 +02:00
games647
131de8404c Add support for new authme API 2017-06-12 17:26:46 +02:00
games647
fbdd8ffc35 Choose player name casing based on client request.
Since BungeeCord commit 5bc189fbb7e8ca19984544af78a083024404fb2a the name casing is based on
the exact name saved at Mojang. This means it could have breaking effects on FastLogin, because
it performs case-sensitive checks against the database. To provide backwards compatibility with
old data we restore the old implementation access for FastLogin.

Thanks to @Maxetto for pointing this out. This commit basically reverts:
059c3f346e
2017-06-07 21:09:00 +02:00
games647
7db8c78975 Drop support for old authme API 2017-06-04 15:52:01 +02:00
games647
b102f06f8e Update ProtocolLib to fix building 2017-05-27 11:24:43 +02:00
games647
a79e18445a Fix building because the bungee proxy repo is down [ci skip] 2017-05-19 12:01:02 +02:00
games647
cf1a0c1bef Remove ebean util usage to make it compatible with 1.12 2017-05-14 17:11:10 +02:00
games647
059c3f346e Lowercase name inside pendingconnection for comparisons against the database 2017-05-10 17:06:25 +02:00
Leo G. ~ Leoko
47db2c7858 Fixed AuthHook (#144)
* Fixed AuthHook

The setServerStarted()-Method is now also called if an extern AuthHook
hooks into FastLogin via the API

* Simplified if-Statement
2017-04-19 14:39:27 +02:00
games647
5bb8640d78 Do not try to hook into a plugin if auth plugin hook is already set using the FastLogin API 2017-04-17 15:22:09 +02:00
games647
881b2ec7bc Fix changelog markdown syntax 2017-04-15 09:42:17 +02:00
games647
194c67cd6f Fix markdown syntax 2017-04-05 09:24:41 +02:00
games647
863607c9a4 Add optional useSSL config option 2017-02-23 09:16:11 +01:00
games647
f37cc0a0db Add commit id to the version 2017-02-14 14:01:57 +01:00
games647
70a81bfcdf Correctly wait for BungeeAuth loading by using the correct depend tag (Fixes #119) 2017-02-10 19:06:57 +01:00
games647
b8d029d6da Remove third party API 2017-02-04 14:09:38 +01:00
games647
c47dd1df80 Fix FileNotFoundEx if the bungee config doesn't exist 2017-01-28 16:38:48 +01:00
games647
4d5b1787b1 Migrate to Java 7 NIO files 2017-01-26 09:52:45 +01:00
games647
8c764220bd Fix duplicate premium username message 2017-01-21 18:02:45 +01:00
games647
9af076b4c4 Fix premium username logging message at the wrong place 2017-01-09 17:57:50 +01:00
games647
22aa9287e9 Fix NoClassDef errors if the optional PlaceholderAPI is not available (Fixes #108) 2017-01-07 18:42:10 +01:00
games647
f08daa9b72 Update bungee-proxy maven repository 2017-01-06 13:00:17 +01:00
games647
bc53743c6b Add placeholder variables 2017-01-06 12:54:02 +01:00
games647
a430a079c9 Do no print auto login message on authme session reuse (Related #101) 2016-12-23 22:12:55 +01:00
games647
f3ac6090f1 Fix bungee online check (Fixes #101) 2016-12-23 10:01:38 +01:00
games647
5ca9b9c59a Add note about firewalling your spigot server if you use BungeeCord 2016-12-22 09:13:58 +01:00
games647
b886d1501f Update LoginSecurity to make it buildable 2016-12-16 15:56:30 +01:00
games647
0082cc6536 Use static builder to make it independent from ProtocolLib without throwing NoClassDefFoundError 2016-12-16 15:49:40 +01:00
games647
7f96d55084 Convert config values to string if casting fails 2016-11-26 13:27:39 +01:00
games647
3851d539f8 Workaround injector class is package private in older versions of ProtocolLib (Fixes #94) 2016-11-26 11:33:15 +01:00
games647
a25d97879f Fail safetly if there session was started (prevents duplicate errors) 2016-11-26 10:06:27 +01:00
games647
41abffdb08 Fix Spigot console command invocation sends result to ingame players 2016-10-20 14:06:18 +02:00
games647
e69eb70377 Update BungeeAuth dependency and use the new API 2016-10-05 10:06:02 +02:00
games647
e924b7a2fa Automatically register players who are not known to the auth plugin
Fixes #85
2016-10-03 13:46:33 +02:00
games647
157ca04691 Fix timestamp parsing in newer versions of SQLite 2016-09-23 12:26:18 +02:00
games647
ae3e03405d No duplicate login's like auth plugins auto logins if it's the same ip 2016-09-23 10:42:25 +02:00
games647
bebb04bdea Share the same force login mangement for less duplicate code 2016-09-22 10:56:31 +02:00
games647
91f41c55de Finally set a value to the API column 2016-09-21 13:24:26 +02:00
games647
1acc825f81 Remove deprecated API methods 2016-09-21 13:22:48 +02:00
games647
87ca00d75d [SwitchMode] Kick the player only if the player is unknown to us 2016-09-21 09:16:19 +02:00
games647
62ffb1a904 [Bukkit] Fix adding to premium whitelist 2016-09-20 13:55:03 +02:00
games647
5075a71843 A few code styling things 2016-09-20 13:32:06 +02:00
games647
da266c7e91 Fix loading of settings 2016-09-19 17:59:45 +02:00
games647
acab4766b1 Remove database migration logging 2016-09-19 15:59:57 +02:00
games647
bef90d11cd Deploy only the universal jar to the target folder 2016-09-18 11:40:21 +02:00
games647
a02acd2d63 Remove the nasty UltraAuth fakeplayer workaround 2016-09-18 10:38:05 +02:00
games647
ca42a7c19e Refactor more code for more Java 8 and Guava usage 2016-09-17 15:19:07 +02:00
games647
b533197f05 Fix config loading in BungeeCord 2016-09-17 15:19:07 +02:00
Maxetto
c94711f315 Fix verb (#79) 2016-09-17 08:02:33 +02:00
games647
ee7af80bf0 Fix travis 2016-09-16 17:41:23 +02:00
games647
17c2099bf1 Make use of the awesome Java 8 features 2016-09-16 17:40:42 +02:00
games647
31d6b67381 Try to upgrade to Java 8. I hope enough people are using it. 2016-09-16 16:31:47 +02:00
games647
4b423c9ccb Update ProtocolSupport and use a maven repository for it now 2016-09-16 10:45:05 +02:00
games647
4292e9aaa0 Less deprecated warnings + Clean up 2016-09-15 11:10:52 +02:00
games647
07d0aededa Fix loading with unloaded configuration values 2016-09-15 10:33:17 +02:00
games647
218bc50c96 Drop support for LoginSecurity 1.X since 2.X seems to be stable 2016-09-14 17:44:32 +02:00
games647
a3b2e33aad Switch to vik1395 repository for BungeeAuth 2016-09-13 09:53:46 +02:00
games647
76f5ba7ed1 Refactor a lot of code + Add Guava v10 as shared library 2016-09-11 21:26:03 +02:00
games647
2cd50d23ad Revert converting auth hooks to the new format
-> backwards compatibility
2016-09-11 20:07:21 +02:00
games647
9f5f61f1c2 Do the same for the password generator 2016-09-11 19:57:27 +02:00
games647
3e9c8e3a7e More shared project code for less errors and less duplication 2016-09-11 18:59:42 +02:00
games647
8e5da01be0 Added configuration to disable auto logins for 2Factor authentication
(Fixes #65)
2016-09-09 16:52:44 +02:00
games647
5022c9aa7b Add cracked whitelist (Fixes #42)
(switch-mode -> switching to online-mode from offlinemode)
2016-09-09 16:40:24 +02:00
games647
ad1ab22586 Test another locale sqlite fix 2016-09-08 11:48:13 +02:00
games647
99ef5ce726 Fix correct cracked permission for bukkit 2016-09-08 10:06:43 +02:00
games647
9b7634a9f3 Fix LogIt repository 2016-09-04 16:35:57 +02:00
games647
115fc2e7ba A try to fix SQLite timestamp parsing 2016-09-04 12:14:28 +02:00
games647
b660951e1e Fix compatibility with older ProtocolLib versions (for 1.7)
because of the missing getMethodAcccessorOrNull method
2016-09-03 10:21:42 +02:00
games647
e495f70ccd Fix logging exceptions on encryption enabling 2016-09-01 20:09:34 +02:00
games647
b35d67b5c0 Add missing add-premium-other message 2016-09-01 19:41:58 +02:00
games647
58ac73a5a9 Fix update username in FastLogin database after nameChange (Fixes #67) 2016-08-31 13:29:06 +02:00
games647
ebe768f7a2 Fix ProtocolSupport autoRegister 2016-08-30 12:27:02 +02:00
games647
d20db79f46 Add second attemp login -> cracked (Fixes #51) 2016-08-29 17:38:46 +02:00
games647
c28d889c1b Remove complex nameChange convert since we now allow duplicate uuids 2016-08-26 13:32:28 +02:00
games647
ad60397851 Added auto login importers 2016-08-25 17:45:09 +02:00
games647
88fdeff3f1 Update documentation 2016-08-25 17:44:51 +02:00
games647
558ee1c92c Fix console support for cracked/premium commands (Fixes# 62) 2016-08-20 17:38:47 +02:00
games647
3e84ebd787 [BungeeAuth] Do not login the player if it's already logged in using
sessions
2016-08-19 22:00:07 +02:00
games647
36d7564c3a Fix race condition when waiting for bukkit message while
bungee redirects player
2016-08-19 21:07:29 +02:00
games647
596caa0573 Invoke forcelogin in BungeeCord only once 2016-08-19 20:52:51 +02:00
games647
fe4331298f Send a message on BungeeCord if there is only an auth plugin 2016-08-18 20:22:04 +02:00
games647
a67d84ef3f Fix race condition in bungee<->bukkit 2016-08-18 20:15:43 +02:00
games647
71362dfd7d Log bungeeauth exceptions 2016-08-18 09:16:06 +02:00
Maxetto
fcd98fce43 Fix Color Code (#54)
Got it wrong the first time.
2016-08-09 19:17:48 +02:00
games647
6c1c4e7286 Fix third-party not premium player detection 2016-08-09 14:24:57 +02:00
games647
164fb735d6 Fix ProtocolSupport BungeeCord 2016-08-07 11:41:03 +02:00
games647
fa1b0970a5 Dump to 1.7.1 2016-08-01 12:59:04 +02:00
games647
974bf498fc Fix protocollsupport autoregister 2016-07-31 11:56:20 +02:00
games647
27c04ff08f Fix BungeeCord autoRegister (Fixes #46) 2016-07-31 09:57:50 +02:00
games647
fb357424e6 Run the plugin message reader async to prevent the timeout event
warning from BungeeCord
2016-07-26 14:30:11 +02:00
games647
c73bb70256 FIx autoregister bug 2016-07-25 19:29:22 +02:00
games647
dc395cdc3f Remove debug code 2016-07-20 20:03:17 +02:00
games647
f7626ab969 Read the fully input from mcapi.ca instead of just one line 2016-07-20 11:54:00 +02:00
games647
5f9802d589 Fix third party profile parsing 2016-07-19 10:47:47 +02:00
games647
642c1621ad Fine tune timeout length 2016-07-13 13:00:25 +02:00
games647
eb965d5a48 Fix importing 2016-07-13 10:05:22 +02:00
games647
457bc9cf47 Fix SQLite drop index 2016-07-12 13:06:27 +02:00
games647
2ab3c6b77c Update AutoIn importer 2016-07-12 12:39:06 +02:00
games647
f27bad02d3 Remove the uuid index for name change conflicts 2016-07-12 12:23:32 +02:00
games647
9334296beb Fix saving on name change 2016-07-10 19:25:37 +02:00
games647
fd9940e6f0 Fix setting skin on Cauldron (Fixes #36) 2016-07-10 13:34:08 +02:00
games647
0745957e79 Fix BungeeCord not setting an premium uuid (Fixes #35) 2016-07-09 13:30:43 +02:00
games647
bb2e60f6e1 State why choose the loginEvent and fix it 2016-07-08 16:47:50 +02:00
games647
d15861b8e5 Use the correct message key 2016-07-08 16:47:50 +02:00
Maxetto
b84b340a77 Consistency language update (#33)
Messages that aren't targeted to the player invoking the command shouldn't start with "You are", that could be confusing.

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

3
.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
# 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/
# speed up testing https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
sudo: false
# This is a java project
@@ -8,8 +8,5 @@ language: java
script: mvn compile test
# We run on 7+
jdk:
- openjdk7
- oraclejdk7
- oraclejdk8
# We run on 8
jdk: [oraclejdk8]

View File

@@ -1,17 +1,148 @@
######1.1
### 1.10
* Add support for HTTP proxies
* Set the fake offline UUID on lowest priority (-> as soon as possible)
* Remove bungee chatcolor for Bukkit to support KCauldron
* Minor cleanup using inspections + Https
* Increase hook delay to let ProtocolLib inject the listener
* Drop support for old AuthMe API + Add support for new AuthMe API
* Remove ebean util usage to make it compatible with 1.12
* Do not try to hook into a plugin if auth plugin hook is already set using the FastLogin API
* Automatically register accounts if they are not in the auth plugin database but in the FastLogin database
* Update BungeeAuth dependency and use the new API. Please update your plugin if you still use the old one.
* Remove deprecated API methods from the last version
* Finally set a value to the API column
* No duplicate session login
* Fix timestamp parsing in newer versions of SQLite
* Fix Spigot console command invocation sends result to in game players
### 1.9
* Added second attempt login -> cracked login
* Added cracked whitelist (switch-mode -> switching to online-mode from offlinemode)
* Added configuration to disable auto logins for 2Factor authentication
* Added missing add-premium-other message
* Upgrade to Java 8 -> Minimize file size
* Refactored/Cleaned up a lot of code
* [API] Deprecated platform specific authplugin. Please use AuthPlugin< platform specific player type >
* [API] Deprecated bukkit's password generator. Please use PasswordGenerator< platform specific player type >
* Fix ProtocolSupport autoRegister
* Fix update username in FastLogin database after nameChange
* Fix logging exceptions on encryption enabling
* Fix compatibility with older ProtocolLib versions (for 1.7) because of the missing getMethodAcccessorOrNull method
* Fix correct cracked permission for bukkit
* A try to fix SQLite timestamp parsing
* Drop support for LoginSecurity 1.X since 2.X seems to be stable
* Remove the nasty UltraAuth fakeplayer workaround by using a new api method. You should UltraAuth if you have it
### 1.8
* Added autoIn importer
* Added BFA importer
* Added ElDziAuth importer
* Fix third-party not premium player detection
* Fix ProtocolSupport BungeeCord
* Fix duplicate logins for BungeeAuth users
### 1.7.1
* Fix BungeeCord autoRegister (Fixes #46)
* Fix protocollsupport autoregister
### 1.7
* Added support for making requests to Mojang from different IPv4 addresses
* Added us.mcapi.com as third-party APIs to workaround rate-limits
* Fixed NPE in BungeeCord on cracked session
* Fixed skin applies if premium uuid is deactivated
* Fix player entry is not saved if namechangecheck is enabled
* Fix skin applies for third-party plugins
* Switch to mcapi.ca for uuid lookups
* Fix BungeeCord not setting an premium uuid
* Fix setting skin on Cauldron
* Fix saving on name change
### 1.6.2
* Fixed support for new LoginSecurity version
### 1.6.1
* Fix message typo in BungeeCord which created a NPE if premium-warning is activated
### 1.6
* Add a warning message if the user tries to invoke the premium command
* Added missing translation if the server isn't fully started
* Removed ProtocolLib as required dependency. You can use ProtocolSupport or BungeeCord as alternative
* Reduce the number of worker threads from 5 to 3 in ProtocolLib
* Process packets in ProtocolLib async/non-blocking -> better performance
* Fixed missing translation in commands
* Fixed cracked command not working on BungeeCord
* Fix error if forward skins is disabled
### 1.5.2
* Fixed BungeeCord force logins if there is a lobby server
* Removed cache expire in BungeeCord
* Applies skin earlier to make it visible for other plugins listening on login events
### 1.5.1
* Fixed BungeeCord support by correctly saving the proxy ids
### 1.5
* Added localization
* Fixed NPE on premium name check if it's pure cracked player
* Fixed NPE in BungeeCord on cracked login for existing players
* Fixed saving of existing cracked players
### 1.4
* Added Bungee setAuthPlugin method
* Added nameChangeCheck
* Multiple BungeeCord support
### 1.3.1
* Prevent thread create violation in BungeeCord
### 1.3
* Added support for AuthMe 3.X
* 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
### 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
### 0.8
* Fixed BungeeCord support for the Bukkit module
* Added database storage to save the premium state
@@ -19,7 +150,7 @@
* Fixed issues with host lookup from hosts file (Thanks to @NorbiPeti)
* Remove handshake listener because it creates errors on some systems
######0.7
### 0.7
* Added BungeeAuth support
* Added /premium [player] command with optional player parameter
@@ -32,14 +163,14 @@
* Fixed BungeeCord support
* Changed config option autologin to autoregister to clarify the usage
######0.6
### 0.6
* Fixed 1.9 bugs
* Added UltraAuth support
######0.5
### 0.5
* Added unpremium command
* Added cracked command
* Added autologin - See config
* Added config
* Added isRegistered API method
@@ -49,34 +180,34 @@
* Fixed premium name check to protocolsupport
* Improved permissions management
######0.4
### 0.4
* Added forward premium skin
* Added plugin support for protocolsupport
######0.3.2
### 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
### 0.3.1
* Improved BungeeCord security
#####0.3
### 0.3
* Added BungeeCord support
* Decrease timeout checks in order to fail faster on connection problems
* Code style improvements
######0.2.4
### 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
### 0.2.3
* Remove useless AuthMe forcelogin code
* Send a kick message to the client instead of just "Disconnect"
@@ -84,15 +215,15 @@
* Fix thread safety for fake start packets (Bukkit.getOfflinePlayer doesn't look like to be thread-safe)
* Added more documentation
######0.2.2
### 0.2.2
* Compile project with Java 7 :(
######0.2.1
### 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
### 0.2
* Added support for CrazyLogin and LoginSecurity
* Now minecraft version independent
@@ -101,5 +232,5 @@
* More state validation
* Added better error handling
#####0.1
* First release
### 0.1
* First release

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015
Copyright (c) 2015-17
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

120
README.md
View File

@@ -1,12 +1,9 @@
# FastLogin
[![Build Status](https://travis-ci.org/games647/FastLogin.svg?branch=master)](https://travis-ci.org/games647/FastLogin)
[![Donate Button](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8ZBULMAPN7MZC)
Checks if a minecraft player has a paid account (premium). If so, they can skip offline authentication (auth plugins).
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:
### Features:
* Detect paid accounts from others
* Automatically login paid accounts (premium)
@@ -15,53 +12,86 @@ So they don't need to enter passwords. This is also called auto login (auto-logi
* Forge/Sponge message support
* Premium UUID support
* Forwards Skins
* Detect user name changed and will update the existing database record
* BungeeCord support
* Auto register new premium players
* Plugin: ProtocolSupport is supported and can be used as an alternative to ProtocolLib
* No client modifications needed
* Good performance by using async non blocking operations
* Locale messages
* Import the database from similar plugins
* Free
* Open source
***
###Commands:
* /premium [player] Label the invoker as paid account
* /cracked [player] Label the invoker as cracked account
### Commands:
* /premium [player] Label the invoker or the argument as paid account
* /cracked [player] Label the invoker or the argument as cracked account
* /importdb <autoIn/bpa/eldzi> <mysql/sqlite> [host:port] [database] [username] [password] - Imports the database from another plugin
###Permissions:
### Permissions:
* fastlogin.bukkit.command.premium
* fastlogin.bukkit.command.cracked
* fastlogin.command.premium.other
* fastlogin.command.cracked.other
* fastlogin.command.import
###Requirements:
* Plugin: [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
### Requirements:
* Plugin: [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [ProtocolSupport](https://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
* An auth plugin. Supported plugins
####Bukkit/Spigot/PaperSPigot
#### Bukkit/Spigot/Paper
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
* [RoyalAuth](http://dev.bukkit.org/bukkit-plugins/royalauth/)
* [UltraAuth](http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/)
* [AuthMe (both 5.X and 3.X)](https://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
* [xAuth](https://dev.bukkit.org/bukkit-plugins/xauth/)
* [LogIt](https://github.com/XziomekX/LogIt)
* [AdvancedLogin (Paid)](https://www.spigotmc.org/resources/advancedlogin.10510/)
* [CrazyLogin](https://dev.bukkit.org/bukkit-plugins/crazylogin/)
* [LoginSecurity](https://dev.bukkit.org/bukkit-plugins/loginsecurity/)
* [RoyalAuth](https://dev.bukkit.org/bukkit-plugins/royalauth/)
* [UltraAuth](https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/)
####BungeeCord/Waterfall
#### BungeeCord/Waterfall
* [BungeeAuth](https://www.spigotmc.org/resources/bungeeauth.493/)
###Downloads
### Downloads
https://www.spigotmc.org/resources/fastlogin.14153/history
***
###FAQ
### How to install
####Index
1. [How does minecraft logins work?](#how-does-minecraft-logins-work)
#### Bukkit/Spigot/Paper
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. You should *always* firewall your spigot server that it's only accessible through BungeeCord https://www.spigotmc.org/wiki/bungeecord-installation/#post-installation
9. (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)
@@ -71,26 +101,26 @@ https://www.spigotmc.org/resources/fastlogin.14153/history
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
#### 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
2. Server -> Client: Okay. I'm in online mode so here is my public key for encryption and my server id
3. Client -> Mojang: I'm player "xyz". I want to join a server with that server id
4. Mojang -> Client: Session data checked. You can continue
5. Client -> Server: I received a successful response from Mojang. Heres our shared secret key
5. Client -> Server: I received a successful response from Mojang. Here 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
###### Offline Mode
In offline mode step 2-7 is skipped. So a login request is directly followed by 8.
######More details
###### More details
http://wiki.vg/Protocol#Login
####How does this plugin work?
#### 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
@@ -105,7 +135,7 @@ connection, we will intercept only **login** packets of **premium** players.
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?
#### 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
@@ -115,42 +145,38 @@ incorrectly. For example, a plugin tries to fetch the UUID from Mojang, but the
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?
#### 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?
#### 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.
session-server.
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?
### What happens if a paid account joins with a used username?
The player on the server have to activate the feature of this plugin by command. If a person buys the username
of his own account, it's still secured. A normal offline mode login makes sure he's the owner of the server account
and Mojang account. Then the command can be executed. So someone different cannot steal the account of cracked player
by buying the username.
####Does the plugin have BungeeCord support?
Yes it has. Just activate ipForward in your BungeeCord config and place the plugin in the plugins folder of
Bukkit/Spigot and BungeeCord. Then you have fill your BungeeCord Id (from the Stats-Option in the BungeeCord config)
into the whitelist file of your Bukkit/Spigot server. For security reasons, don't post this Id on Forums.
#### Does the plugin have BungeeCord support?
Yes it has. See the how to install above.
This plugin will automatically detect if BungeeCord is running and handle premium checks on BungeeCord.
####Could premium players have a premium UUID and Skin?
#### 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?
#### 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:
### Useful Links:
* [Login Protocol](http://wiki.vg/Protocol#Login)
* [Protocol Encryption](http://wiki.vg/Protocol_Encryption)
* [Protocol Encryption](http://wiki.vg/Protocol_Encryption)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.1</version>
<version>1.10</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -22,16 +22,22 @@
<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-->
<!--AuthMe Reloaded-->
<repository>
<id>xephi-repo</id>
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
<url>https://ci.xephi.fr/plugin/repository/everything/</url>
</repository>
<!--xAuth-->
@@ -45,14 +51,27 @@
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<!--PlaceholderAPI -->
<repository>
<id>placeholderapi</id>
<url>http://repo.extendedclip.com/content/repositories/placeholderapi/</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.9-R0.1-SNAPSHOT</version>
<version>1.12-pre2-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -60,23 +79,35 @@
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>3.6.5-SNAPSHOT</version>
<optional>true</optional>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>protcolsupport</groupId>
<groupId>com.github.ProtocolSupport</groupId>
<artifactId>ProtocolSupport</artifactId>
<version>Build-337</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/ProtocolSupport b337.jar</systemPath>
<!--4.25.dev-->
<version>a4f060dc46</version>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.8.1</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Login Plugins-->
<dependency>
<groupId>fr.xephi</groupId>
<artifactId>authme</artifactId>
<version>5.2-SNAPSHOT</version>
<version>5.3.1</version>
<optional>true</optional>
<exclusions>
<exclusion>
@@ -87,9 +118,24 @@
</dependency>
<dependency>
<groupId>com.github.lenis0012</groupId>
<artifactId>LoginSecurity-2</artifactId>
<version>-9c09e73b7f-1</version>
<groupId>com.lenis0012.bukkit</groupId>
<artifactId>loginsecurity</artifactId>
<version>2.1.6</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>LogIt</artifactId>
<version>9e3581db27</version>
<optional>true</optional>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
@@ -102,6 +148,7 @@
<groupId>com.github.RoyalDev</groupId>
<artifactId>RoyalAuth</artifactId>
<version>-e21354a9b7-1</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>*</groupId>
@@ -149,7 +196,7 @@
<version>2.0.2</version>
<optional>true</optional>
<scope>system</scope>
<systemPath>${project.basedir}/lib/UltraAuth v2.0.2.jar</systemPath>
<systemPath>${project.basedir}/lib/UltraAuth v2.1.2.jar</systemPath>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,101 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import org.apache.commons.lang.ArrayUtils;
/**
* Represents a client connecting to the server.
*
* This session is invalid if the player disconnects or the login was successful
*/
public class BukkitLoginSession extends LoginSession {
private final String serverId;
private final byte[] verifyToken;
private boolean verified;
private String encodedSkinData;
private String skinSignature;
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
, PlayerProfile profile) {
super(username, registered, profile);
this.serverId = serverId;
this.verifyToken = ArrayUtils.clone(verifyToken);
}
//available for BungeeCord
public BukkitLoginSession(String username, boolean registered) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, registered, null);
}
//cracked player
public BukkitLoginSession(String username, PlayerProfile profile) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
}
/**
* Gets the random generated server id. This makes sure the request sent from the client is just for this server.
*
* See this for details http://www.sk89q.com/2011/09/Minecraft-name-spoofing-exploit/
*
* Empty if it's a BungeeCord connection
*
* @return random generated server id
*/
public String getServerId() {
return serverId;
}
/**
* Gets the verify token the server sent to the client.
*
* Empty if it's a BungeeCord connection
*
* @return the verify token from the server
*/
public byte[] getVerifyToken() {
return ArrayUtils.clone(verifyToken);
}
public synchronized String getEncodedSkinData() {
return encodedSkinData;
}
public synchronized String getSkinSignature() {
return skinSignature;
}
/**
* Sets the premium skin property which was retrieved by the session server
*
* @param encodedData
* @param skinSignature
*/
public synchronized void setSkin(String encodedData, String skinSignature) {
this.encodedSkinData = encodedData;
this.skinSignature = skinSignature;
}
/**
* Sets whether the player has a premium (paid account) account and valid session
*
* @param verified whether the player has valid session
*/
public synchronized void setVerified(boolean verified) {
this.verified = verified;
}
/**
* Get whether the player has a premium (paid account) account and valid session
*
* @return whether the player has a valid session
*/
public synchronized boolean isVerified() {
return verified;
}
}

View File

@@ -8,8 +8,7 @@ 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.util.stream.Stream;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -40,17 +39,15 @@ public class EncryptionUtil {
}
}
public static byte[] getServerIdHash(String serverId, PublicKey publicKey, SecretKey secretKey) {
public static byte[] getServerIdHash(String serverId, Key publicKey, Key secretKey) {
return digestOperation("SHA-1"
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded()});
, serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded());
}
private static byte[] digestOperation(String algo, byte[]... content) {
private static byte[] digestOperation(String algorithm, byte[]... content) {
try {
MessageDigest messagedigest = MessageDigest.getInstance(algo);
for (byte[] data : content) {
messagedigest.update(data);
}
MessageDigest messagedigest = MessageDigest.getInstance(algorithm);
Stream.of(content).forEach(messagedigest::update);
return messagedigest.digest();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
@@ -73,7 +70,7 @@ public class EncryptionUtil {
// return null;
// }
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] encryptedSharedKey) {
public static SecretKey decryptSharedKey(Key privateKey, byte[] encryptedSharedKey) {
return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES");
}
@@ -84,8 +81,8 @@ public class EncryptionUtil {
private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
try {
return createCipherInstance(operationMode, key.getAlgorithm(), key).doFinal(data);
} catch (IllegalBlockSizeException | BadPaddingException illegalblocksizeexception) {
illegalblocksizeexception.printStackTrace();
} catch (IllegalBlockSizeException | BadPaddingException ex) {
ex.printStackTrace();
}
System.err.println("Cipher data failed!");
@@ -98,8 +95,8 @@ public class EncryptionUtil {
cipher.init(operationMode, key);
return cipher;
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException invalidkeyexception) {
invalidkeyexception.printStackTrace();
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException ex) {
ex.printStackTrace();
}
System.err.println("Cipher creation failed!");

View File

@@ -1,183 +1,151 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.AsynchronousManager;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.utility.SafeCacheBuilder;
import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.github.games647.fastlogin.bukkit.listener.BukkitJoinListener;
import com.github.games647.fastlogin.bukkit.listener.BungeeCordListener;
import com.github.games647.fastlogin.bukkit.listener.EncryptionPacketListener;
import com.github.games647.fastlogin.bukkit.listener.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.listener.StartPacketListener;
import com.google.common.cache.CacheLoader;
import com.google.common.reflect.ClassPath;
import com.github.games647.fastlogin.bukkit.listener.protocollib.LoginSkinApplyListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ProtocolLibListener;
import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.lang.reflect.Method;
import java.io.Reader;
import java.security.KeyPair;
import java.sql.SQLException;
import java.util.UUID;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.RandomStringUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.messaging.PluginMessageRecipient;
/**
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
*/
public class FastLoginBukkit extends JavaPlugin {
private static final int WORKER_THREADS = 5;
public static UUID parseId(String withoutDashes) {
return UUID.fromString(withoutDashes.substring(0, 8)
+ "-" + withoutDashes.substring(8, 12)
+ "-" + withoutDashes.substring(12, 16)
+ "-" + withoutDashes.substring(16, 20)
+ "-" + withoutDashes.substring(20, 32));
}
public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<CommandSender> {
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
private boolean bungeeCord;
private Storage storage;
private FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
private boolean serverStarted;
//this map is thread-safe for async access (Packet Listener)
//SafeCacheBuilder is used in order to be version independent
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
.expireAfterWrite(1, TimeUnit.MINUTES)
//mapped by ip:port -> PlayerSession
.build(new CacheLoader<String, PlayerSession>() {
@Override
public PlayerSession load(String key) throws Exception {
//A key should be inserted manually on start packet
throw new UnsupportedOperationException("Not supported");
}
});
private BukkitAuthPlugin authPlugin;
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
//1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
private final ConcurrentMap<String, BukkitLoginSession> loginSession = FastLoginCore.buildCache(1, -1);
@Override
public void onEnable() {
saveDefaultConfig();
core = new FastLoginCore<>(this);
core.load();
try {
bungeeCord = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
} catch (ClassNotFoundException notFoundEx) {
//ignore server has no bungee support
} catch (Exception ex) {
getLogger().log(Level.WARNING, "Cannot check bungeecord support. You use a non-spigot build", ex);
}
if (getServer().getOnlineMode()) {
//we need to require offline to prevent a session request for a offline player
//we need to require offline to prevent a loginSession request for a offline player
getLogger().severe("Server have to be in offline mode");
setEnabled(false);
return;
}
try {
if (Bukkit.spigot().getConfig().isBoolean("settings.bungeecord")) {
bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord");
} else {
Method getConfigMethod = FuzzyReflection.fromObject(getServer().spigot(), true)
.getMethodByName("getSpigotConfig");
getConfigMethod.setAccessible(true);
YamlConfiguration spigotConfig = (YamlConfiguration) getConfigMethod.invoke(getServer().spigot());
bungeeCord = spigotConfig.getBoolean("settings.bungeecord");
}
} catch (Exception | NoSuchMethodError ex) {
getLogger().warning("Cannot check bungeecord support. You use a non-spigot build");
}
boolean hookFound = registerHooks();
if (bungeeCord) {
getLogger().info("BungeeCord setting detected. No auth plugin is required");
} else if (!hookFound) {
getLogger().info("No auth plugin were found and bungeecord is deactivated. "
+ "Either one or both of the checks have to pass in order to use this plugin");
setEnabled(false);
return;
}
setServerStarted();
if (bungeeCord) {
//check for incoming messages from the bungeecord version of this plugin
getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this));
getServer().getMessenger().registerOutgoingPluginChannel(this, getName());
//register listeners on success
} else {
String driver = getConfig().getString("driver");
String host = getConfig().getString("host", "");
int port = getConfig().getInt("port", 3306);
String database = getConfig().getString("database");
String username = getConfig().getString("username", "");
String password = getConfig().getString("password", "");
this.storage = new Storage(this, driver, host, port, database, username, password);
try {
storage.createTables();
} catch (SQLException sqlEx) {
getLogger().log(Level.SEVERE, "Failed to create database tables. Disabling plugin...", sqlEx);
if (!core.setupDatabase()) {
setEnabled(false);
return;
}
if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) {
getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this);
} else if (getServer().getPluginManager().isPluginEnabled("ProtocolLib")) {
//they will be created with a static builder, because otherwise it will throw a
//java.lang.NoClassDefFoundError: com/comphenix/protocol/events/PacketListener if ProtocolSupport was
//only found
ProtocolLibListener.register(this);
getServer().getPluginManager().registerEvents(new LoginSkinApplyListener(this), this);
} else {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
//we are performing HTTP request on these so run it async (seperate from the Netty IO threads)
AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager();
StartPacketListener startPacketListener = new StartPacketListener(this, protocolManager);
EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this, protocolManager);
asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS);
asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS);
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().runTaskLater(this, new DelayedAuthHook(this), 5L);
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));
if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
//prevents NoClassDef errors if it's not available
PremiumPlaceholder.register(this);
}
}
@Override
public void onDisable() {
//clean up
session.clear();
loginSession.clear();
if (core != null) {
core.close();
}
//remove old blacklists
for (Player player : getServer().getOnlinePlayers()) {
player.removeMetadata(getName(), this);
}
if (storage != null) {
storage.close();
}
getServer().getOnlinePlayers().forEach(player -> player.removeMetadata(getName(), this));
}
public String generateStringPassword() {
return RandomStringUtils.random(8, true, true);
public FastLoginCore<Player, CommandSender, FastLoginBukkit> getCore() {
return core;
}
public void sendBungeeActivateMessage(CommandSender sender, String target, boolean activate) {
if (sender instanceof Player) {
notifyBungeeCord((Player) sender, target, activate, sender instanceof Player);
} else {
Player firstPlayer = Iterables.getFirst(getServer().getOnlinePlayers(), null);
if (firstPlayer == null) {
getLogger().info("No player online to send a plugin message to the proxy");
return;
}
notifyBungeeCord(firstPlayer, target, activate, sender instanceof Player);
}
}
/**
* 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
* @return a thread-safe loginSession map
*/
public ConcurrentMap<String, PlayerSession> getSessions() {
return session;
public ConcurrentMap<String, BukkitLoginSession> getLoginSessions() {
return loginSession;
}
/**
@@ -189,62 +157,64 @@ public class FastLoginBukkit extends JavaPlugin {
return keyPair;
}
public Storage getStorage() {
return storage;
}
/**
* Gets the auth plugin hook in order to interact with the plugins. This can be null if no supporting auth plugin
* was found.
*
* @return interface to any supported auth plugin
*/
public BukkitAuthPlugin getAuthPlugin() {
return authPlugin;
}
/**
* Gets the a connection in order to access important features from the Mojang API.
*
* @return the connector instance
*/
public MojangApiConnector getApiConnector() {
return mojangApiConnector;
}
private boolean registerHooks() {
BukkitAuthPlugin authPluginHook = null;
try {
String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
//Look through all classes in the hooks package and look for supporting plugins on the server
for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
//remove the hook suffix
String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
Class<?> clazz = clazzInfo.load();
//uses only member classes which uses AuthPlugin interface (skip interfaces)
if (BukkitAuthPlugin.class.isAssignableFrom(clazz)
//check only for enabled plugins. A single plugin could be disabled by plugin managers
&& getServer().getPluginManager().isPluginEnabled(pluginName)) {
authPluginHook = (BukkitAuthPlugin) clazz.newInstance();
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
break;
}
}
} catch (InstantiationException | IllegalAccessException | IOException ex) {
getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
}
if (authPluginHook == null) {
//run this check for exceptions (errors) and not found plugins
getLogger().warning("No support offline Auth plugin found. ");
return false;
}
authPlugin = authPluginHook;
return true;
}
public boolean isBungeeCord() {
return bungeeCord;
}
/**
* 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 void setServerStarted() {
if (!this.serverStarted) {
this.serverStarted = true;
}
}
private void notifyBungeeCord(PluginMessageRecipient sender, String target, boolean activate, boolean isPlayer) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
if (activate) {
dataOutput.writeUTF("ON");
} else {
dataOutput.writeUTF("OFF");
}
dataOutput.writeUTF(target);
dataOutput.writeBoolean(isPlayer);
sender.sendPluginMessage(this, getName(), dataOutput.toByteArray());
}
@Override
public Map<String, Object> loadYamlFile(Reader reader) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(reader);
return config.getValues(true);
}
@Override
public void sendMessage(CommandSender receiver, String message) {
receiver.sendMessage(message);
}
@Override
public String translateColorCodes(char colorChar, String rawMessage) {
return ChatColor.translateAlternateColorCodes(colorChar, rawMessage);
}
@Override
public ThreadFactory getThreadFactory() {
//not required here to make a custom thread factory
return null;
}
@Override
public MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests
, Map<String, Integer> proxies) {
return new MojangApiBukkit(logger, addresses, requests, proxies);
}
}

View File

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

View File

@@ -0,0 +1,91 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
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?" +
"username=%s&serverId=%s";
public MojangApiBukkit(Logger logger, List<String> localAddresses, int rateLimit, Map<String, Integer> proxies) {
super(logger, localAddresses, rateLimit, proxies);
}
@Override
public boolean hasJoinedServer(LoginSession session, String serverId, InetSocketAddress ip) {
BukkitLoginSession playerSession = (BukkitLoginSession) session;
try {
String url = String.format(HAS_JOINED_URL, playerSession.getUsername(), serverId);
if (ip != null) {
url += "&ip=" + URLEncoder.encode(ip.getAddress().getHostAddress(), "UTF-8");
}
HttpURLConnection conn = getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (line != null && !"null".equals(line)) {
//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 ("textures".equals(propertyName)) {
String skinValue = (String) skinProperty.get("value");
String signature = (String) skinProperty.get("signature");
playerSession.setSkin(skinValue, signature);
}
return true;
}
} catch (Exception ex) {
//catch not only io-exceptions also parse and NPE on unexpected json format
logger.log(Level.WARNING, "Failed to verify session", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
@Override
protected String getUUIDFromJson(String json) {
boolean isArray = json.startsWith("[");
JSONObject mojangPlayer;
if (isArray) {
JSONArray array = (JSONArray) JSONValue.parse(json);
mojangPlayer = (JSONObject) array.get(0);
} else {
mojangPlayer = (JSONObject) JSONValue.parse(json);
}
String uuid = (String) mojangPlayer.get("id");
if ("null".equals(uuid)) {
return null;
}
return uuid;
}
}

View File

@@ -1,124 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;
import java.util.logging.Level;
import java.util.regex.Pattern;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
public class MojangApiConnector {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 1 * 1_000;
private static final String USER_AGENT = "Premium-Checker";
//mojang api check to prove a player is logged in minecraft and made a join server request
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
//only premium (paid account) users have a uuid from here
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
//this includes a-zA-Z1-9_
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
//compile the pattern only on plugin enable -> and this have to be threadsafe
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
private final FastLoginBukkit plugin;
public MojangApiConnector(FastLoginBukkit plugin) {
this.plugin = plugin;
}
/**
*
* @param playerName
* @return null on non-premium
*/
public UUID getPremiumUUID(String playerName) {
//check if it's a valid playername
if (playernameMatcher.matcher(playerName).matches()) {
//only make a API call if the name is valid existing mojang account
try {
HttpURLConnection connection = getConnection(UUID_LINK + playerName);
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = reader.readLine();
if (line != null && !line.equals("null")) {
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) userData.get("id");
return parseId(uuid);
}
}
//204 - no content for not found
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
}
return null;
}
public boolean hasJoinedServer(PlayerSession session, String serverId) {
try {
String url = HAS_JOINED_URL + "username=" + session.getUsername() + "&serverId=" + serverId;
HttpURLConnection conn = getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (line != null && !line.equals("null")) {
//validate parsing
//http://wiki.vg/Protocol_Encryption#Server
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) userData.get("id");
session.setUuid(parseId(uuid));
JSONArray properties = (JSONArray) userData.get("properties");
JSONObject skinProperty = (JSONObject) properties.get(0);
String propertyName = (String) skinProperty.get("name");
if (propertyName.equals("textures")) {
String skinValue = (String) skinProperty.get("value");
String signature = (String) skinProperty.get("signature");
session.setSkin(WrappedSignedProperty.fromValues(propertyName, skinValue, signature));
}
return true;
}
} catch (Exception ex) {
//catch not only ioexceptions also parse and NPE on unexpected json format
plugin.getLogger().log(Level.WARNING, "Failed to verify session", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
private UUID parseId(String withoutDashes) {
return UUID.fromString(withoutDashes.substring(0, 8)
+ "-" + withoutDashes.substring(8, 12)
+ "-" + withoutDashes.substring(12, 16)
+ "-" + withoutDashes.substring(16, 20)
+ "-" + withoutDashes.substring(20, 32));
}
private HttpURLConnection getConnection(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(TIMEOUT);
//the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", USER_AGENT);
return connection;
}
}

View File

@@ -1,144 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import java.util.UUID;
import org.apache.commons.lang.ArrayUtils;
/**
* Represents a client connecting to the server.
*
* This session is invalid if the player disconnects or the login was successful
*/
public class PlayerSession {
private final String username;
private final String serverId;
private final byte[] verifyToken;
private UUID uuid;
private WrappedSignedProperty skinProperty;
private boolean verified;
private boolean registered;
public PlayerSession(String username, String serverId, byte[] verifyToken) {
this.username = username;
this.serverId = serverId;
this.verifyToken = ArrayUtils.clone(verifyToken);
}
public PlayerSession(String username) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY);
}
/**
* Gets the random generated server id. This makes sure the request
* sent from the client is just for this server.
*
* See this for details
* http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
*
* Empty if it's a BungeeCord connection
*
* @return random generated server id
*/
public String getServerId() {
return serverId;
}
/**
* Gets the verify token the server sent to the client.
*
* Empty if it's a BungeeCord connection
*
* @return the verify token from the server
*/
public byte[] getVerifyToken() {
return ArrayUtils.clone(verifyToken);
}
/**
* Gets the username the player sent to the server
*
* @return the client sent username
*/
public String getUsername() {
return username;
}
/**
* Gets the premium skin of this player
*
* @return skin property or null if the player has no skin or is a cracked account
*/
public synchronized WrappedSignedProperty getSkin() {
return this.skinProperty;
}
/**
* Sets the premium skin property which was retrieved by the session server
*
* @param skinProperty premium skin property
*/
public synchronized void setSkin(WrappedSignedProperty skinProperty) {
this.skinProperty = skinProperty;
}
/**
* Sets whether the account of this player already exists
*
* @param registered whether the account exists
*/
public synchronized void setRegistered(boolean registered) {
this.registered = registered;
}
/**
* Gets whether the account of this player already exists.
*
* @return whether the account exists
*/
public synchronized boolean needsRegistration() {
return !registered;
}
/**
* Sets whether the player has a premium (paid account) account
* and valid session
*
* @param verified whether the player has valid session
*/
public synchronized void setVerified(boolean verified) {
this.verified = verified;
}
/**
* Get the premium UUID of this player
*
* @return the premium UUID or null if not fetched
*/
public synchronized UUID getUuid() {
return uuid;
}
/**
* Set the online UUID if it's fetched
*
* @param uuid premium UUID
*/
public synchronized void setUuid(UUID uuid) {
this.uuid = uuid;
}
/**
* Get whether the player has a premium (paid account) account
* and valid session
*
* @return whether the player has a valid session
*/
public synchronized boolean isVerified() {
return verified;
}
}

View File

@@ -0,0 +1,40 @@
package com.github.games647.fastlogin.bukkit;
import java.util.List;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.PlaceholderHook;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
public class PremiumPlaceholder extends PlaceholderHook {
private final FastLoginBukkit plugin;
public PremiumPlaceholder(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public String onPlaceholderRequest(Player player, String variable) {
if (player != null && "fastlogin_status".contains(variable)) {
List<MetadataValue> metadata = player.getMetadata(plugin.getName());
if (metadata == null) {
return "unknown";
}
if (!metadata.isEmpty()) {
return "premium";
} else {
return "cracked";
}
}
return null;
}
public static boolean register(FastLoginBukkit plugin) {
return PlaceholderAPI.registerPlaceholderHook(plugin, new PremiumPlaceholder(plugin));
}
}

View File

@@ -1,201 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.utility.SafeCacheBuilder;
import com.google.common.cache.CacheLoader;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class Storage {
private static final String PREMIUM_TABLE = "premium";
private final ConcurrentMap<String, PlayerProfile> profileCache = SafeCacheBuilder
.<String, PlayerProfile>newBuilder()
.concurrencyLevel(20)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build(new CacheLoader<String, PlayerProfile>() {
@Override
public PlayerProfile load(String key) throws Exception {
//should be fetched manually
throw new UnsupportedOperationException("Not supported yet.");
}
});
private final HikariDataSource dataSource;
private final FastLoginBukkit plugin;
public Storage(FastLoginBukkit plugin, String driver, String host, int port, String databasePath
, String user, String pass) {
this.plugin = plugin;
HikariConfig databaseConfig = new HikariConfig();
databaseConfig.setUsername(user);
databaseConfig.setPassword(pass);
databaseConfig.setDriverClassName(driver);
databasePath = databasePath.replace("{pluginDir}", plugin.getDataFolder().getAbsolutePath());
String jdbcUrl = "jdbc:";
if (driver.contains("sqlite")) {
jdbcUrl += "sqlite" + "://" + databasePath;
databaseConfig.setConnectionTestQuery("SELECT 1");
} else {
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
}
databaseConfig.setJdbcUrl(jdbcUrl);
this.dataSource = new HikariDataSource(databaseConfig);
}
public void createTables() throws SQLException {
Connection con = null;
try {
con = dataSource.getConnection();
Statement statement = con.createStatement();
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "`UUID` CHAR(36), "
+ "`Name` VARCHAR(16) NOT NULL, "
+ "`Premium` BOOLEAN NOT NULL, "
+ "`LastIp` VARCHAR(255) NOT NULL, "
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+ "UNIQUE (`UUID`), "
//the premium shouldn't steal the cracked account by changing the name
+ "UNIQUE (`Name`) "
+ ")";
if (dataSource.getJdbcUrl().contains("sqlite")) {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
statement.executeUpdate(createDataStmt);
} finally {
closeQuietly(con);
}
}
public PlayerProfile getProfile(String name, boolean fetch) {
if (profileCache.containsKey(name)) {
return profileCache.get(name);
} else if (fetch) {
Connection con = null;
try {
con = dataSource.getConnection();
PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE
+ " WHERE `Name`=? LIMIT 1");
loadStatement.setString(1, name);
ResultSet resultSet = loadStatement.executeQuery();
if (resultSet.next()) {
long userId = resultSet.getInt(1);
String unparsedUUID = resultSet.getString(2);
UUID uuid;
if (unparsedUUID == null) {
uuid = null;
} else {
uuid = FastLoginBukkit.parseId(unparsedUUID);
}
// String name = resultSet.getString(3);
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
long lastLogin = resultSet.getTimestamp(6).getTime();
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
profileCache.put(name, playerProfile);
return playerProfile;
} else {
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
profileCache.put(name, crackedProfile);
return crackedProfile;
}
} catch (SQLException sqlEx) {
plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
}
}
return null;
}
public boolean save(PlayerProfile playerProfile) {
Connection con = null;
try {
con = dataSource.getConnection();
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
PreparedStatement saveStatement = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
if (uuid == null) {
saveStatement.setString(1, null);
} else {
saveStatement.setString(1, uuid.toString().replace("-", ""));
}
saveStatement.setString(2, playerProfile.getPlayerName());
saveStatement.setBoolean(3, playerProfile.isPremium());
saveStatement.setString(4, playerProfile.getLastIp());
saveStatement.execute();
ResultSet generatedKeys = saveStatement.getGeneratedKeys();
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setUserId(generatedKeys.getInt(1));
}
} else {
PreparedStatement saveStatement = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
if (uuid == null) {
saveStatement.setString(1, null);
} else {
saveStatement.setString(1, uuid.toString().replace("-", ""));
}
saveStatement.setString(2, playerProfile.getPlayerName());
saveStatement.setBoolean(3, playerProfile.isPremium());
saveStatement.setString(4, playerProfile.getLastIp());
// saveStatement.setTimestamp(5, new Timestamp(playerProfile.getLastLogin()));
saveStatement.setLong(5, playerProfile.getUserId());
saveStatement.execute();
}
return true;
} catch (SQLException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
} finally {
closeQuietly(con);
}
return false;
}
public void close() {
dataSource.close();
profileCache.clear();
}
private void closeQuietly(Connection con) {
if (con != null) {
try {
con.close();
} catch (SQLException sqlEx) {
plugin.getLogger().log(Level.SEVERE, "Failed to close connection", sqlEx);
}
}
}
}

View File

@@ -1,12 +1,9 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.PlayerProfile;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.github.games647.fastlogin.core.PlayerProfile;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -14,7 +11,7 @@ import org.bukkit.entity.Player;
public class CrackedCommand implements CommandExecutor {
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
public CrackedCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
@@ -25,57 +22,65 @@ public class CrackedCommand implements CommandExecutor {
if (args.length == 0) {
if (!(sender instanceof Player)) {
//console or command block
sender.sendMessage(ChatColor.DARK_RED + "Only players can remove themselves from the premium list");
sender.sendMessage(plugin.getCore().getMessage("no-console"));
return true;
}
final Player player = (Player) sender;
// UUID uuid = player.getUniqueId();
if (plugin.isBungeeCord()) {
notifiyBungeeCord((Player) sender);
sender.sendMessage(ChatColor.YELLOW + "Sending request...");
plugin.sendBungeeActivateMessage(sender, sender.getName(), false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async if it's not in the cache anymore
final PlayerProfile profile = plugin.getStorage().getProfile(player.getName(), true);
//todo: load async if
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players");
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
profile.setUuid(null);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
plugin.getStorage().save(profile);
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
} else {
sender.sendMessage(ChatColor.DARK_RED + "You are not in the premium list");
plugin.getCore().sendLocaleMessage("not-premium", sender);
}
}
return true;
} else {
sender.sendMessage(ChatColor.DARK_RED + "NOT IMPLEMENTED YET");
//todo:
// String playerName = args[0];
// boolean existed = plugin.getEnabledPremium().remove(playerName);
// if (existed) {
// sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players");
// notifiyBungeeCord((Player) sender);
// } else {
// sender.sendMessage(ChatColor.DARK_RED + "User is not in the premium list");
// }
onCrackedOther(sender, command, args);
}
return true;
}
private void notifiyBungeeCord(Player target) {
private void onCrackedOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
plugin.getCore().sendLocaleMessage("no-permission", sender);
return;
}
if (plugin.isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("OFF");
plugin.sendBungeeActivateMessage(sender, args[0], false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage("Error occurred");
return;
}
target.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
//existing player is already cracked
if (profile.getUserId() != -1 && !profile.isPremium()) {
plugin.getCore().sendLocaleMessage("not-premium-other", sender);
} else {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
}
}
}
}

View File

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

View File

@@ -1,35 +1,74 @@
package com.github.games647.fastlogin.bukkit.hooks;
import fr.xephi.authme.api.NewAPI;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import fr.xephi.authme.api.NewAPI;
import fr.xephi.authme.api.v3.AuthMeApi;
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 {
public class AuthMeHook implements AuthPlugin<Player> {
private final boolean v3APIAvailable;
public AuthMeHook() {
boolean apiAvailable = true;
try {
Class.forName("fr.xephi.authme.api.v3.AuthMeApi");
} catch (ClassNotFoundException classNotFoundEx) {
apiAvailable = false;
}
this.v3APIAvailable = apiAvailable;
}
@Override
public boolean forceLogin(Player player) {
//skips registration and login
NewAPI.getInstance().forceLogin(player);
//commented because the operation above is performed async -> race conditions
// return NewAPI.getInstance().isAuthenticated(player);
if (v3APIAvailable) {
//skips registration and login
if (AuthMeApi.getInstance().isAuthenticated(player)) {
return false;
} else {
AuthMeApi.getInstance().forceLogin(player);
}
} else {
//skips registration and login
if (NewAPI.getInstance().isAuthenticated(player)) {
return false;
} else {
NewAPI.getInstance().forceLogin(player);
}
}
return true;
}
@Override
public boolean isRegistered(String playerName) throws Exception {
if (v3APIAvailable) {
return AuthMeApi.getInstance().isRegistered(playerName);
}
return NewAPI.getInstance().isRegistered(playerName);
}
@Override
public boolean forceRegister(Player player, String password) {
NewAPI.getInstance().forceRegister(player, password);
//this automatically registers the player too
if (v3APIAvailable) {
AuthMeApi.getInstance().forceRegister(player, password);
} else {
NewAPI.getInstance().forceRegister(player, password);
}
return true;
}
}

View File

@@ -1,18 +1,18 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import de.st_ddt.crazylogin.CrazyLogin;
import de.st_ddt.crazylogin.data.LoginPlayerData;
import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
import de.st_ddt.crazylogin.listener.PlayerListener;
import de.st_ddt.crazylogin.metadata.Authenticated;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.apache.commons.lang.reflect.FieldUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -23,47 +23,43 @@ import org.bukkit.entity.Player;
*
* Bukkit: http://dev.bukkit.org/server-mods/crazylogin/
*/
public class CrazyLoginHook implements BukkitAuthPlugin {
public class CrazyLoginHook implements AuthPlugin<Player> {
protected final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
private final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
private final PlayerListener playerListener = getListener();
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(Player player) {
//not thread-safe operation
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin
, new Callable<LoginPlayerData>() {
@Override
public LoginPlayerData call() throws Exception {
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
if (playerData != null) {
//mark the account as logged in
playerData.setLoggedIn(true);
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin, () -> {
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player);
if (playerData != null) {
//mark the account as logged in
playerData.setLoggedIn(true);
String ip = player.getAddress().getAddress().getHostAddress();
//this should be done after login to restore the inventory, unhide players, prevent potential memory leaks...
String ip = player.getAddress().getAddress().getHostAddress();
//this should be done after login to restore the inventory, show players, prevent potential memory leaks...
//from: https://github.com/ST-DDT/CrazyLogin/blob/master/src/main/java/de/st_ddt/crazylogin/CrazyLogin.java#L1948
playerData.resetLoginFails();
player.setFireTicks(0);
playerData.resetLoginFails();
player.setFireTicks(0);
if (playerListener != null) {
playerListener.removeMovementBlocker(player);
playerListener.disableHidenInventory(player);
playerListener.disableSaveLogin(player);
playerListener.unhidePlayer(player);
}
//loginFailuresPerIP.remove(IP);
//illegalCommandUsesPerIP.remove(IP);
//tempBans.remove(IP);
playerData.addIP(ip);
player.setMetadata("Authenticated", new Authenticated(crazyLoginPlugin, player));
crazyLoginPlugin.unregisterDynamicHooks();
return playerData;
if (playerListener != null) {
playerListener.removeMovementBlocker(player);
playerListener.disableHidenInventory(player);
playerListener.disableSaveLogin(player);
playerListener.unhidePlayer(player);
}
return null;
//loginFailuresPerIP.remove(IP);
//illegalCommandUsesPerIP.remove(IP);
//tempBans.remove(IP);
playerData.addIP(ip);
player.setMetadata("Authenticated", new Authenticated(crazyLoginPlugin, player));
crazyLoginPlugin.unregisterDynamicHooks();
return playerData;
}
return null;
});
try {
@@ -87,7 +83,7 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
}
@Override
public boolean forceRegister(final Player player, String password) {
public boolean forceRegister(Player player, String password) {
CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
//this executes a sql query and accesses only thread safe collections so we can run it async
@@ -108,8 +104,8 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
private PlayerListener getListener() {
PlayerListener listener;
try {
listener = FuzzyReflection.getFieldValue(crazyLoginPlugin, PlayerListener.class, true);
} catch (Exception ex) {
listener = (PlayerListener) FieldUtils.readField(crazyLoginPlugin, "playerListener", true);
} catch (IllegalAccessException ex) {
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to get the listener instance for auto login", ex);
listener = null;
}

View File

@@ -0,0 +1,43 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import io.github.lucaseasedup.logit.CancelledState;
import io.github.lucaseasedup.logit.LogItCore;
import io.github.lucaseasedup.logit.account.Account;
import io.github.lucaseasedup.logit.session.SessionManager;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/XziomekX/LogIt
* Project page:
*
* Bukkit: Unknown
* Spigot: Unknown
*/
public class LogItHook implements AuthPlugin<Player> {
@Override
public boolean forceLogin(Player player) {
SessionManager sessionManager = LogItCore.getInstance().getSessionManager();
if (sessionManager.isSessionAlive(player)) {
return true;
}
return sessionManager.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;
}
}

View File

@@ -1,92 +1,45 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.google.common.base.Charsets;
import com.lenis0012.bukkit.ls.LoginSecurity;
import com.lenis0012.bukkit.ls.data.DataManager;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.lenis0012.bukkit.loginsecurity.LoginSecurity;
import com.lenis0012.bukkit.loginsecurity.session.AuthService;
import com.lenis0012.bukkit.loginsecurity.session.PlayerSession;
import com.lenis0012.bukkit.loginsecurity.session.action.LoginAction;
import com.lenis0012.bukkit.loginsecurity.session.action.RegisterAction;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/lenis0012/LoginSecurity-2 Project page:
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/loginsecurity/ Spigot:
* https://www.spigotmc.org/resources/loginsecurity.19362/
*
* on join:
* https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282
* Bukkit: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
* Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
*/
public class LoginSecurityHook implements BukkitAuthPlugin {
public class LoginSecurityHook implements AuthPlugin<Player> {
protected final LoginSecurity securityPlugin = LoginSecurity.instance;
private final FastLoginBukkit plugin = (FastLoginBukkit) Bukkit.getPluginManager().getPlugin("FastLogin");
@Override
public boolean forceLogin(final Player player) {
//Login command of this plugin: (How the plugin logs the player in)
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39
//not thread-safe operation
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(securityPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
String name = player.getName().toLowerCase();
//mark the user as logged in
securityPlugin.authList.remove(name);
//cancel timeout timer
securityPlugin.thread.timeout.remove(name);
//remove effects and restore location
securityPlugin.rehabPlayer(player, name);
return true;
}
});
try {
return future.get();
} catch (InterruptedException | ExecutionException ex) {
securityPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
return false;
public boolean forceLogin(Player player) {
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
if (session.isAuthorized()) {
return true;
}
return session.performAction(new LoginAction(AuthService.PLUGIN, plugin)).isSuccess();
}
@Override
public boolean isRegistered(String playerName) throws Exception {
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L296
DataManager dataManager = securityPlugin.data;
//https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L283
UUID offlineUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(Charsets.UTF_8));
return dataManager.isRegistered(offlineUuid.toString().replace("-", ""));
//check for loginsecurity sessions in order to prevent a sql query?
//sesUse && thread.getSession().containsKey(uuid) && checkLastIp(player)) {
PlayerSession session = LoginSecurity.getSessionManager().getOfflineSession(playerName);
return session.isRegistered();
}
@Override
public boolean forceRegister(final Player player, final String password) {
final DataManager dataManager = securityPlugin.data;
UUID playerUUID = player.getUniqueId();
final String uuidString = playerUUID.toString().replace("-", "");
final InetAddress ipAddress = player.getAddress().getAddress();
final String passwordHash = securityPlugin.hasher.hash(password);
//this executes a sql query without interacting with other parts so we can run it async.
dataManager.register(uuidString, passwordHash, securityPlugin.hasher.getTypeId(), ipAddress.toString());
String storedPassword = dataManager.getPassword(uuidString);
if (storedPassword != null && storedPassword.equals(passwordHash)) {
//the register method silents any excpetion so check if our entry was saved
return forceLogin(player);
}
return false;
public boolean forceRegister(Player player, String password) {
PlayerSession session = LoginSecurity.getSessionManager().getPlayerSession(player);
return session.performAction(new RegisterAction(AuthService.PLUGIN, plugin, password)).isSuccess();
}
}

View File

@@ -1,12 +1,14 @@
package com.github.games647.fastlogin.bukkit.hooks;
import java.util.concurrent.Callable;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.royaldev.royalauth.AuthPlayer;
import org.royaldev.royalauth.Config;
import org.royaldev.royalauth.RoyalAuth;
@@ -18,24 +20,24 @@ import org.royaldev.royalauth.RoyalAuth;
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/royalauth/
*/
public class RoyalAuthHook implements BukkitAuthPlugin {
public class RoyalAuthHook implements AuthPlugin<Player> {
private final RoyalAuth royalAuthPlugin = (RoyalAuth) Bukkit.getPluginManager().getPlugin("RoyalAuth");
private final Plugin royalAuthPlugin = (RoyalAuth) Bukkit.getPluginManager().getPlugin("RoyalAuth");
@Override
public boolean forceLogin(final Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(royalAuthPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
public boolean forceLogin(Player player) {
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(royalAuthPlugin, () -> {
if (authPlayer.isLoggedIn()) {
return true;
}
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdLogin.java#L62
//not thread-safe
authPlayer.login();
//not thread-safe
authPlayer.login();
return authPlayer.isLoggedIn();
}
return authPlayer.isLoggedIn();
});
try {
@@ -58,11 +60,8 @@ public class RoyalAuthHook implements BukkitAuthPlugin {
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
boolean registerSuccess = authPlayer.setPassword(password, Config.passwordHashType);
if (registerSuccess) {
//login in the player after registration
return forceLogin(player);
}
return false;
//login in the player after registration
return registerSuccess && forceLogin(player);
}
}

View File

@@ -1,15 +1,14 @@
package com.github.games647.fastlogin.bukkit.hooks;
import java.util.concurrent.Callable;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import ultraauth.api.UltraAuthAPI;
import ultraauth.main.Main;
import ultraauth.managers.PlayerManager;
@@ -18,21 +17,23 @@ 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 {
public class UltraAuthHook implements AuthPlugin<Player> {
protected final Plugin ultraAuthPlugin = Main.main;
private final Plugin ultraAuthPlugin = Main.main;
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
UltraAuthAPI.authenticatedPlayer(player);
return UltraAuthAPI.isAuthenticated(player);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, () -> {
if (UltraAuthAPI.isAuthenticated(player)) {
return true;
}
UltraAuthAPI.authenticatedPlayer(player);
return UltraAuthAPI.isAuthenticated(player);
});
try {
@@ -45,14 +46,14 @@ public class UltraAuthHook implements BukkitAuthPlugin {
@Override
public boolean isRegistered(String playerName) throws Exception {
return UltraAuthAPI.isRegisterd(new FakePlayer(playerName));
return UltraAuthAPI.isRegisterd(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
//the register method silents any exception so check if our entry was saved
return forceLogin(player);
}

View File

@@ -1,15 +1,15 @@
package com.github.games647.fastlogin.bukkit.hooks;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import de.luricos.bukkit.xAuth.xAuth;
import de.luricos.bukkit.xAuth.xAuthPlayer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
@@ -19,27 +19,28 @@ import org.bukkit.entity.Player;
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/xauth/
*/
public class xAuthHook implements BukkitAuthPlugin {
public class xAuthHook implements AuthPlugin<Player> {
protected final xAuth xAuthPlugin = xAuth.getPlugin();
private final xAuth xAuthPlugin = xAuth.getPlugin();
@Override
public boolean forceLogin(final Player player) {
public boolean forceLogin(Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
//we checked that the player is premium (paid account)
xAuthPlayer.setPremium(true);
//unprotect the inventory, op status...
return xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, () -> {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
if (xAuthPlayer.isAuthenticated()) {
return true;
}
return false;
//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 {
@@ -58,30 +59,24 @@ public class xAuthHook implements BukkitAuthPlugin {
}
@Override
public boolean forceRegister(final Player player, final String password) {
public boolean forceRegister(Player player, final String password) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
//this should run async because the plugin executes a sql query, but the method
//accesses non thread-safe collections :(
boolean registerSuccess = xAuthPlugin.getAuthClass(xAuthPlayer)
.adminRegister(player.getName(), password, null);
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, () -> {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
//this should run async because the plugin executes a sql query, but the method
//accesses non thread-safe collections :(
if (registerSuccess) {
//login in the player after registration
return forceLogin(player);
}
}
return false;
return xAuthPlugin.getAuthClass(xAuthPlayer)
.adminRegister(player.getName(), password, null);
}
return false;
});
try {
return future.get();
//login in the player after registration
return future.get() && forceLogin(player);
} catch (InterruptedException | ExecutionException ex) {
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
return false;

View File

@@ -1,16 +1,16 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.ForceLoginTask;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
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;
/**
@@ -21,37 +21,34 @@ public class BukkitJoinListener implements Listener {
private static final long DELAY_LOGIN = 20L / 2;
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
public BukkitJoinListener(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@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) {
final Player player = joinEvent.getPlayer();
//removing the session because we now use it
final PlayerSession session = plugin.getSessions().get(player.getAddress().toString());
if (session != null && plugin.getConfig().getBoolean("forwardSkin")) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
WrappedSignedProperty skin = session.getSkin();
if (skin != null) {
gameProfile.getProperties().put("textures", skin);
}
}
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);
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player);
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, forceLoginTask, DELAY_LOGIN);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
final Player player = quitEvent.getPlayer();
//prevent memory leaks
Player player = quitEvent.getPlayer();
player.removeMetadata(plugin.getName(), plugin);
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -1,22 +1,23 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.ForceLoginTask;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.google.common.base.Charsets;
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.plugin.messaging.PluginMessageListener;
/**
@@ -29,13 +30,13 @@ public class BungeeCordListener implements PluginMessageListener {
private static final String FILE_NAME = "proxy-whitelist.txt";
protected final FastLoginBukkit plugin;
private final FastLoginBukkit plugin;
//null if whitelist is empty so bungeecord support is disabled
private final UUID proxyId;
private final Set<UUID> proxyIds;
public BungeeCordListener(FastLoginBukkit plugin) {
this.plugin = plugin;
this.proxyId = loadBungeeCordId();
this.proxyIds = loadBungeeCordIds();
}
@Override
@@ -45,19 +46,16 @@ public class BungeeCordListener implements PluginMessageListener {
}
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});
String subChannel = dataInput.readUTF();
plugin.getLogger().log(Level.FINEST, "Received plugin message for sub channel {0} from {1}"
, new Object[]{subChannel, player});
final String playerName = dataInput.readUTF();
String playerName = dataInput.readUTF();
//check if the player is still online or disconnected
final Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
//fail if target player is blacklisted because already authed or wrong bungeecord id
Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
//fail if target player is blacklisted because already authenticated 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();
@@ -65,51 +63,49 @@ public class BungeeCordListener implements PluginMessageListener {
plugin.getLogger().log(Level.FINEST, "Received proxy id {0} from {1}", new Object[]{sourceId, player});
//fail if BungeeCord support is disabled (id = null)
if (sourceId.equals(proxyId)) {
final PlayerSession playerSession = new PlayerSession(playerName);
final String id = '/' + checkedPlayer.getAddress().getAddress().getHostAddress() + ':'
+ checkedPlayer.getAddress().getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
playerSession.setVerified(true);
playerSession.setRegistered(true);
plugin.getSessions().put(id, playerSession);
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
playerSession.setVerified(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin != null && !authPlugin.isRegistered(playerName)) {
plugin.getSessions().put(id, playerSession);
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
}
});
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
if (proxyIds.contains(sourceId)) {
readMessage(checkedPlayer, subChannel, playerName, player);
}
}
}
public UUID loadBungeeCordId() {
File whitelistFile = new File(plugin.getDataFolder(), FILE_NAME);
//create a new folder if it doesn't exist. Fail silently otherwise
whitelistFile.getParentFile().mkdir();
private void readMessage(Player checkedPlayer, String subchannel, String playerName, Player player) {
InetSocketAddress address = checkedPlayer.getAddress();
String id = '/' + address.getAddress().getHostAddress() + ':' + address.getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, true);
playerSession.setVerified(true);
plugin.getLoginSessions().put(id, playerSession);
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin.getCore(), player));
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin == null || !authPlugin.isRegistered(playerName)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, false);
playerSession.setVerified(true);
plugin.getLoginSessions().put(id, playerSession);
new ForceLoginTask(plugin.getCore(), player).run();
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
});
}
}
public Set<UUID> loadBungeeCordIds() {
Path whitelistFile = plugin.getDataFolder().toPath().resolve(FILE_NAME);
try {
if (!whitelistFile.exists()) {
whitelistFile.createNewFile();
if (Files.notExists(whitelistFile)) {
Files.createFile(whitelistFile);
}
String firstLine = Files.readFirstLine(whitelistFile, Charsets.UTF_8);
if (firstLine != null && !firstLine.isEmpty()) {
return UUID.fromString(firstLine.trim());
}
return Files.lines(whitelistFile)
.map(String::trim)
.map(UUID::fromString)
.collect(Collectors.toSet());
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
} catch (Exception ex) {

View File

@@ -1,228 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.UUID;
import java.util.logging.Level;
import javax.crypto.SecretKey;
import org.bukkit.entity.Player;
/**
* Handles incoming encryption responses from connecting clients.
* It prevents them from reaching the server because that cannot handle
* it in offline mode.
*
* Moreover this manages a started premium check from
* this plugin. So check if all data is correct and we can prove him as a
* owner of a paid minecraft account.
*
* Receiving packet information:
* http://wiki.vg/Protocol#Encryption_Response
*
* sharedSecret=encrypted byte array
* verify token=encrypted byte array
*/
public class EncryptionPacketListener extends PacketAdapter {
private final ProtocolManager protocolManager;
//hides the inherit Plugin plugin field, but we need this type
private final FastLoginBukkit plugin;
public EncryptionPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we make api calls to Mojang
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
this.plugin = plugin;
this.protocolManager = protocolManger;
}
/**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
* (Client Auth)
* C->S : Encryption Key Response
* (Server Auth, Both enable encryption)
* S->C : Login Success (*)
*
* On offline logins is Login Start followed by Login Success
*
* Minecraft Server implementation
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
Player player = packetEvent.getPlayer();
//the player name is unknown to ProtocolLib (so getName() doesn't work) - now uses ip:port as key
String uniqueSessionKey = player.getAddress().toString();
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
if (session == null) {
disconnect(packetEvent, "Invalid request", Level.FINE
, "Player {0} tried to send encryption response at invalid state"
, player.getAddress());
return;
}
PrivateKey privateKey = plugin.getServerKey().getPrivate();
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
if (!checkVerifyToken(session, privateKey, packetEvent) || !encryptConnection(player, loginKey, packetEvent)) {
return;
}
//this makes sure the request from the client is for us
//this might be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
String generatedId = session.getServerId();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
//generate the server id based on client and server data
byte[] serverIdHash = EncryptionUtil.getServerIdHash(generatedId, plugin.getServerKey().getPublic(), loginKey);
String serverId = (new BigInteger(serverIdHash)).toString(16);
String username = session.getUsername();
if (plugin.getApiConnector().hasJoinedServer(session, serverId)) {
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
session.setVerified(true);
setPremiumUUID(session, player);
receiveFakeStartPacket(username, player);
} else {
//user tried to fake a authentication
disconnect(packetEvent, "Invalid session", Level.FINE
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), player.getAddress(), serverId);
}
//this is a fake packet; it shouldn't be send to the server
packetEvent.setCancelled(true);
}
private void setPremiumUUID(PlayerSession session, Player player) {
UUID uuid = session.getUuid();
if (plugin.getConfig().getBoolean("premiumUuid") && uuid != null) {
try {
Object networkManager = getNetworkManager(player);
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
Field spoofField = FuzzyReflection.fromObject(networkManager).getFieldByType("spoofedUUID", UUID.class);
spoofField.set(networkManager, uuid);
} catch (ReflectiveOperationException reflectiveOperationException) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", reflectiveOperationException);
}
}
}
private boolean checkVerifyToken(PlayerSession session, PrivateKey privateKey, PacketEvent packetEvent) {
byte[] requestVerify = session.getVerifyToken();
//encrypted verify token
byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1);
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) {
//check if the verify token are equal to the server sent one
disconnect(packetEvent, "Invalid token", Level.FINE
, "Player {0} ({1}) tried to login with an invalid verify token. "
+ "Server: {2} Client: {3}"
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
}
return true;
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager(Player player)
throws IllegalAccessException, NoSuchFieldException {
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
Object rawInjector = injectorField.get(socketInjector);
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
injectorField.setAccessible(true);
return injectorField.get(rawInjector);
}
private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent)
throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager(player);
//try to detect the method by parameters
Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager)
.getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptConnectionMethod.invoke(networkManager, loginKey);
} catch (ReflectiveOperationException ex) {
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
return false;
}
return true;
}
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
, Object... arguments) {
plugin.getLogger().log(logLevel, logMessage, arguments);
kickPlayer(packetEvent.getPlayer(), kickReason);
//cancel the event in order to prevent the server receiving an invalid packet
packetEvent.setCancelled(true);
}
private void kickPlayer(Player player, String reason) {
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
protocolManager.sendServerPacket(player, kickPacket);
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Error sending kickpacket", ex);
}
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username, Player from) {
//see StartPacketListener for packet information
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
protocolManager.recieveClientPacket(from, startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
//cancel the event in order to prevent the server receiving an invalid packet
kickPlayer(from, "Error occured");
}
}
}

View File

@@ -1,84 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.PlayerProfile;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import java.net.InetSocketAddress;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import protocolsupport.api.events.PlayerLoginStartEvent;
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
public class ProtocolSupportListener implements Listener {
protected final FastLoginBukkit plugin;
public ProtocolSupportListener(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onLoginStart(PlayerLoginStartEvent loginStartEvent) {
if (loginStartEvent.isLoginDenied()) {
return;
}
String username = loginStartEvent.getName();
//remove old data every time on a new login in order to keep the session only for one person
plugin.getSessions().remove(username);
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
if (playerProfile != null) {
if (playerProfile.isPremium()) {
if (playerProfile.getUserId() != -1) {
startPremiumSession(username, loginStartEvent, true);
}
} else if (playerProfile.getUserId() == -1) {
//user not exists in the db
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
try {
if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username);
if (premiumUUID != null) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
startPremiumSession(username, loginStartEvent, false);
}
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
}
}
}
@EventHandler(ignoreCancelled = true)
public void onPropertiesResolve(PlayerPropertiesResolveEvent propertiesResolveEvent) {
//skin was resolved -> premium player
if (propertiesResolveEvent.hasProperty("textures")) {
InetSocketAddress address = propertiesResolveEvent.getAddress();
PlayerSession session = plugin.getSessions().get(address.toString());
if (session != null) {
session.setVerified(true);
}
}
}
private void startPremiumSession(String playerName, PlayerLoginStartEvent loginStartEvent, boolean registered) {
loginStartEvent.setOnlineMode(true);
InetSocketAddress address = loginStartEvent.getAddress();
PlayerSession playerSession = new PlayerSession(playerName, null, null);
playerSession.setRegistered(registered);
plugin.getSessions().put(address.toString(), playerSession);
if (plugin.getConfig().getBoolean("premiumUuid")) {
loginStartEvent.setUseOnlineModeUUID(true);
}
}
}

View File

@@ -1,149 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.PlayerProfile;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import java.lang.reflect.InvocationTargetException;
import java.security.PublicKey;
import java.util.Random;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.entity.Player;
/**
* Handles incoming start packets from connecting clients. It
* checks if we can start checking if the player is premium and
* start a request to the client that it should start online mode
* login.
*
* Receiving packet information:
* http://wiki.vg/Protocol#Login_Start
*
* String=Username
*/
public class StartPacketListener extends PacketAdapter {
private static final int VERIFY_TOKEN_LENGTH = 4;
private final ProtocolManager protocolManager;
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
private final FastLoginBukkit plugin;
//just create a new once on plugin enable. This used for verify token generation
private final Random random = new Random();
public StartPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we are making api calls to Mojang
super(params(plugin, PacketType.Login.Client.START).optionAsync());
this.plugin = plugin;
this.protocolManager = protocolManger;
}
/**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
* (Client Auth)
* C->S : Encryption Key Response
* (Server Auth, Both enable encryption)
* S->C : Login Success (*)
*
* On offline logins is Login Start followed by Login Success
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
final Player player = packetEvent.getPlayer();
//this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
String sessionKey = player.getAddress().toString();
//remove old data every time on a new login in order to keep the session only for one person
plugin.getSessions().remove(sessionKey);
//player.getName() won't work at this state
PacketContainer packet = packetEvent.getPacket();
String username = packet.getGameProfiles().read(0).getName();
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
, new Object[]{sessionKey, username});
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
if (playerProfile != null) {
if (playerProfile.isPremium()) {
if (playerProfile.getUserId() != -1) {
enablePremiumLogin(username, sessionKey, player, packetEvent, true);
}
} else if (playerProfile.getUserId() == -1) {
//user not exists in the db
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
try {
if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) {
UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username);
if (premiumUUID != null) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
enablePremiumLogin(username, sessionKey, player, packetEvent, false);
}
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
}
}
}
}
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
private void enablePremiumLogin(String username, String sessionKey, Player player, PacketEvent packetEvent
, boolean registered) {
//randomized server id to make sure the request is for our server
//this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
String serverId = Long.toString(random.nextLong(), 16);
//generate a random token which should be the same when we receive it from the client
byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(verifyToken);
boolean success = sentEncryptionRequest(player, serverId, verifyToken);
if (success) {
PlayerSession playerSession = new PlayerSession(username, serverId, verifyToken);
playerSession.setRegistered(registered);
plugin.getSessions().put(sessionKey, playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
packetEvent.setCancelled(true);
}
}
private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) {
try {
/**
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
*
* ServerID="" (String)
* key=public server key
* verifyToken=random 4 byte array
*/
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN);
newPacket.getStrings().write(0, serverId);
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic());
newPacket.getByteArrays().write(0, verifyToken);
//serverId is a empty string
protocolManager.sendServerPacket(player, newPacket);
return true;
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
}
return false;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.PacketType.Login.Client;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.util.Random;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class ProtocolLibListener extends PacketAdapter {
private static final int WORKER_THREADS = 3;
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 ProtocolLibListener(FastLoginBukkit plugin) {
//run async in order to not block the server, because we are making api calls to Mojang
super(params().plugin(plugin)
.types(PacketType.Login.Client.START, PacketType.Login.Client.ENCRYPTION_BEGIN)
.optionAsync());
this.plugin = plugin;
}
public static void register(FastLoginBukkit plugin) {
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin))
.start(WORKER_THREADS);
}
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
if (packetEvent.isCancelled()
|| plugin.getCore().getAuthPluginHook()== null
|| !plugin.isServerFullyStarted()) {
return;
}
Player sender = packetEvent.getPlayer();
PacketType packetType = packetEvent.getPacketType();
if (packetType == Client.START) {
onLogin(packetEvent, sender);
} else {
onEncryptionBegin(packetEvent, sender);
}
}
private void onEncryptionBegin(PacketEvent packetEvent, Player sender) {
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret);
Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask);
}
private void onLogin(PacketEvent packetEvent, Player player) {
//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.getLoginSessions().remove(sessionKey);
//player.getName() won't work at this state
PacketContainer packet = packetEvent.getPacket();
String username = packet.getGameProfiles().read(0).getName();
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting", new Object[]{sessionKey, username});
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username);
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
}
}

View File

@@ -0,0 +1,99 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.util.Random;
import org.bukkit.entity.Player;
public class ProtocolLibLoginSource implements LoginSource {
private static final int VERIFY_TOKEN_LENGTH = 4;
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final Player player;
private final Random random;
private String serverId;
private final byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
public ProtocolLibLoginSource(FastLoginBukkit plugin, PacketEvent packetEvent, Player player, Random random) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.player = player;
this.random = random;
}
@Override
public void setOnlineMode() throws Exception {
//randomized server id to make sure the request is for our server
//this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
serverId = Long.toString(random.nextLong(), 16);
//generate a random token which should be the same when we receive it from the client
random.nextBytes(verifyToken);
sentEncryptionRequest();
}
@Override
public void kick(String message) throws Exception {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(message));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
protocolManager.sendServerPacket(player, kickPacket);
} finally {
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
}
}
@Override
public InetSocketAddress getAddress() {
return packetEvent.getPlayer().getAddress();
}
private void sentEncryptionRequest() throws InvocationTargetException {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
/**
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
*
* ServerID="" (String) key=public server key verifyToken=random 4 byte array
*/
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN);
newPacket.getStrings().write(0, serverId);
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic());
newPacket.getByteArrays().write(0, verifyToken);
//serverId is a empty string
protocolManager.sendServerPacket(player, newPacket);
}
public String getServerId() {
return serverId;
}
public byte[] getVerifyToken() {
return verifyToken;
}
}

View File

@@ -0,0 +1,203 @@
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.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.Key;
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.getLoginSessions().get(fromPlayer.getAddress().toString());
if (session == null) {
disconnect(plugin.getCore().getMessage("invalid-request"), true
, "Player {0} tried to send encryption response at invalid state", fromPlayer.getAddress());
} else {
verifyResponse(session);
}
} finally {
//this is a fake packet; it shouldn't be send to the server
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
private void verifyResponse(BukkitLoginSession session) {
PrivateKey privateKey = plugin.getServerKey().getPrivate();
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) {
return;
}
//this makes sure the request from the client is for us
//this might be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
String generatedId = session.getServerId();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
//generate the server id based on client and server data
byte[] serverIdHash = EncryptionUtil.getServerIdHash(generatedId, plugin.getServerKey().getPublic(), loginKey);
String serverId = (new BigInteger(serverIdHash)).toString(16);
String username = session.getUsername();
if (plugin.getCore().getApiConnector().hasJoinedServer(session, serverId, fromPlayer.getAddress())) {
plugin.getLogger().log(Level.INFO, "Player {0} has a verified premium account", username);
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), fromPlayer.getAddress(), serverId);
}
}
private void setPremiumUUID(UUID premiumUUID) {
if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) {
try {
Object networkManager = getNetworkManager();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
FieldUtils.writeField(networkManager, "spoofedUUID", premiumUUID, true);
} catch (Exception exc) {
plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", exc);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session, Key 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, ClassNotFoundException {
Object injectorContainer = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer);
//ChannelInjector
Class<?> injectorClass = Class.forName("com.comphenix.protocol.injector.netty.Injector");
Object rawInjector = FuzzyReflection.getFieldValue(injectorContainer, injectorClass, true);
return FieldUtils.readField(rawInjector, "networkManager", true);
}
private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager();
//try to detect the method by parameters
Method encryptMethod = FuzzyReflection
.fromObject(networkManager).getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptMethod.invoke(networkManager, loginKey);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Couldn't enable encryption", ex);
disconnect(plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption");
return false;
}
return true;
}
private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) {
if (debug) {
plugin.getLogger().log(Level.FINE, logMessage, arguments);
} else {
plugin.getLogger().log(Level.SEVERE, logMessage, arguments);
}
kickPlayer(packetEvent.getPlayer(), kickReason);
}
private void kickPlayer(Player player, String reason) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
protocolManager.sendServerPacket(player, kickPacket);
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Error sending kick packet", ex);
}
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
//see StartPacketListener for packet information
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
protocolManager.recieveClientPacket(fromPlayer, startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
//cancel the event in order to prevent the server receiving an invalid packet
kickPlayer(fromPlayer, plugin.getCore().getMessage("error-kick"));
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
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.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.ForceLoginManagement;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.metadata.FixedMetadataValue;
public class ForceLoginTask extends ForceLoginManagement<Player, CommandSender, BukkitLoginSession, FastLoginBukkit> {
public ForceLoginTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player) {
super(core, player);
}
@Override
public void run() {
//remove the bungeecord identifier if there is ones
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
session = core.getPlugin().getLoginSessions().remove(id);
//blacklist this target player for BungeeCord Id brute force attacks
FastLoginBukkit plugin = core.getPlugin();
player.setMetadata(core.getPlugin().getName(), new FixedMetadataValue(plugin, true));
super.run();
}
@Override
public void onForceActionSuccess(LoginSession session) {
if (core.getPlugin().isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("SUCCESS");
player.sendPluginMessage(core.getPlugin(), core.getPlugin().getName(), dataOutput.toByteArray());
}
}
@Override
public String getName(Player player) {
return player.getName();
}
@Override
public boolean isOnline(Player player) {
try {
//the playerlist isn't thread-safe
return Bukkit.getScheduler().callSyncMethod(core.getPlugin(), player::isOnline).get();
} catch (InterruptedException | ExecutionException ex) {
core.getPlugin().getLogger().log(Level.SEVERE, "Failed to perform thread-safe online check", ex);
return false;
}
}
@Override
public boolean isOnlineMode() {
return session.isVerified() && player.getName().equals(session.getUsername());
}
}

View File

@@ -1,73 +0,0 @@
# FastLogin config
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
# Source code: https://github.com/games647/FastLogin
#
# You can access the newest config here:
# https://github.com/games647/FastLogin/blob/master/bukkit/src/main/resources/config.yml
# Request a premium login without forcing the player to type a command
#
# If you activate autoRegister, this plugin will check/do these points on login:
# 1. An existing cracked account shouldn't exist
# -> paid accounts cannot steal the existing account of cracked players
# - (Already registered players could still use the /premium command to activate premium checks)
# 2. Automatically registers an account with a strong random generated password
# -> cracked player cannot register an account for the premium player and so cannot the steal the account
#
# Furthermore the premium player check have to be made based on the player name
# This means if a cracked player connects to the server and we request a paid account login from this player
# the player just disconnect and sees the message: 'bad login' or 'invalid session'
# There is no way to change this message
# For more information: https://github.com/games647/FastLogin#why-do-players-have-to-invoke-a-command
autoRegister: false
# If this plugin detected that a player has a premium, it can also set the associated
# uuid from that account. So if the players changes their usernames, they will still have
# the same playerdata (inventory, permissions, ...)
#
# Warning: This also means that the UUID will be different if the player is connecting
# through a offline mode connection. This **could** cause plugin compatibility issues.
#
# This is a example and doesn't apply for every plugin.
# Example: If you want to ban players who aren't online at the moment, the ban plugin will look
# after a offline uuid associated to the player, because the server is in offline mode. Then the premium
# players could still join the server, because they have different UUID.
#
# Moreover you may want to convert the offline UUID to a premium UUID. This will ensure that the player
# will have the same inventory, permissions, ... if they switched to premium authentification from offline/cracked
# authentification.
#
# This feature requires Cauldron, Spigot or a fork of Spigot (PaperSpigot, TacoSpigot)
premiumUuid: false
# If your players have a premium account and a skin associated to their account, this plugin
# can download the data and set it to the online player.
#
# Keep in mind that this will only works if the player:
# * is the owner of the premium account
# * the serverconnection is established through a premium connection (paid account authentification)
# * has a skin
#
# This means this plugin doesn't need to create a new connection to the Mojang servers, because
# the skin data is included in the Auth-Verification-Response sent by Mojang. If you want to use for other
# players like cracked player, you have to use other plugins.
#
# If you want to use skins for your cracked player, you need an additional plugin like
# ChangeSkin, SkinRestoer, ...
forwardSkin: true
# Database configuration
# Recommened is the use of MariaDB (a better version of MySQL)
# Single file SQLite database
driver: org.sqlite.JDBC
# File location
database: '{pluginDir}/FastLogin.db'
# MySQL and SQLite
#driver: com.mysql.jdbc.Driver
#host: localhost
#port: 3306
#database: fastlogin
#username: myUser
#password: myPassword

View File

@@ -1,28 +1,24 @@
# project informations for Bukkit in order to register our plugin with all it components
# project data 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}
version: ${project.version}-git${git.commit.id}
main: ${project.groupId}.${project.artifactId}.${project.name}
# meta informations for plugin managers
# meta data for plugin managers
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
description: |
${project.description}
website: ${project.url}
dev-url: ${project.url}
# Without Protocollib the plugin does not work at all
depend: [ProtocolLib]
# Load the plugin as early as possible to inject it for all players
load: STARTUP
softdepend:
# We depend either ProtocolLib or ProtocolSupport
- ProtocolSupport
# Auth plugins
- xAuth
- AuthMe
- CrazyLogin
- LoginSecurity
- RoyalAuth
- UltraAuth
- ProtocolLib
- PlaceholderAPI
commands:
${project.parent.name}:
@@ -31,11 +27,11 @@ commands:
usage: /<command> [player]
permission: ${project.artifactId}.command.premium
unpremium:
cracked:
description: 'Label the invoker or the player specified as cracked if he was marked premium before'
aliases: [cracked]
aliases: [unpremium]
usage: /<command> [player]
permission: ${project.artifactId}.command.unpremium
permission: ${project.artifactId}.command.cracked
permissions:
${project.artifactId}.command.premium:
@@ -47,11 +43,11 @@ permissions:
children:
${project.artifactId}.command.premium: true
${project.artifactId}.command.unpremium:
${project.artifactId}.command.cracked:
description: 'Label themselves as cracked'
default: true
${project.artifactId}.command..unpremium.other:
${project.artifactId}.command.cracked.other:
description: 'Label others as cracked'
children:
${project.artifactId}.command.unpremium: true
${project.artifactId}.command.cracked: true

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.1</version>
<version>1.10</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -17,44 +17,43 @@
<name>FastLoginBungee</name>
<repositories>
<!--Waterfall-->
<!-- <repository>
<id>ellune-releases</id>
<url>https://repo.ellune.net/content/repositories/snapshots/</url>
</repository>-->
<!--BungeeCord with also the part outside the API-->
<repository>
<id>RYRED-REPO</id>
<url>http://mvn.ryred.co/repository/snapshots/</url>
<id>luck-repo</id>
<url>https://ci.lucko.me/plugin/repository/everything</url>
</repository>
<!--Github automatic maven builds-->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<id>vik1395-repo</id>
<url>http://repo.vik1395.me/repositories</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.9-SNAPSHOT</version>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<!-- <dependency>
<groupId>io.github.waterfallmc</groupId>
<artifactId>waterfall-api</artifactId>
<version>1.9-SNAPSHOT</version>
<type>jar</type>
<scope>provided</scope>
</dependency>-->
<dependency>
<groupId>com.github.MatteCarra</groupId>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.12-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.vik1395</groupId>
<artifactId>BungeeAuth</artifactId>
<version>-1.2.1-gc367d92-8</version>
<version>1.4</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@@ -1,22 +1,29 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthHook;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.google.common.cache.CacheBuilder;
import com.github.games647.fastlogin.bungee.listener.PlayerConnectionListener;
import com.github.games647.fastlogin.bungee.listener.PluginMessageListener;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.MojangApiConnector;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Random;
import java.util.UUID;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.concurrent.ThreadFactory;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
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;
@@ -24,67 +31,23 @@ import net.md_5.bungee.config.YamlConfiguration;
/**
* BungeeCord version of FastLogin. This plugin keeps track on online mode connections.
*/
public class FastLoginBungee extends Plugin {
public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSender> {
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = Maps.newConcurrentMap();
public static UUID parseId(String withoutDashes) {
return Util.getUUID(withoutDashes);
}
private BungeeAuthPlugin bungeeAuthPlugin;
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
private Storage storage;
private Configuration configuration;
private final Random random = new Random();
private final ConcurrentMap<PendingConnection, Object> pendingAutoRegister = CacheBuilder
.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.<PendingConnection, Object>build().asMap();
private FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core;
@Override
public void onEnable() {
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
File configFile = new File(getDataFolder(), "config.yml");
if (!configFile.exists()) {
try (InputStream in = getResourceAsStream("config.yml")) {
Files.copy(in, configFile.toPath());
} catch (IOException ioExc) {
getLogger().log(Level.SEVERE, "Error saving default config", ioExc);
}
}
try {
configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
String driver = configuration.getString("driver");
String host = configuration.getString("host", "");
int port = configuration.getInt("port", 3306);
String database = configuration.getString("database");
String username = configuration.getString("username", "");
String password = configuration.getString("password", "");
storage = new Storage(this, driver, host, port, database, username, password);
try {
storage.createTables();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
return;
}
} catch (IOException ioExc) {
getLogger().log(Level.SEVERE, "Error loading config. Disabling plugin...", ioExc);
core = new FastLoginCore<>(this);
core.load();
if (!core.setupDatabase()) {
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());
@@ -92,52 +55,62 @@ public class FastLoginBungee extends Plugin {
registerHook();
}
public String generateStringPassword() {
StringBuilder generatedPassword = new StringBuilder(8);
for (int i = 1; i <= 8; i++) {
generatedPassword.append(CHARACTERS[random.nextInt(CHARACTERS.length - 1)]);
}
return generatedPassword.toString();
}
@Override
public void onDisable() {
if (storage != null) {
storage.close();
if (core != null) {
core.close();
}
}
public Configuration getConfiguration() {
return configuration;
public FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> getCore() {
return core;
}
public Storage getStorage() {
return storage;
}
public MojangApiConnector getMojangApiConnector() {
return mojangApiConnector;
}
public ConcurrentMap<PendingConnection, Object> getPendingAutoRegister() {
return pendingAutoRegister;
}
/**
* Get the auth plugin hook for BungeeCord
*
* @return the auth hook for BungeeCord. null if none found
*/
public BungeeAuthPlugin getBungeeAuthPlugin() {
return bungeeAuthPlugin;
public ConcurrentMap<PendingConnection, BungeeLoginSession> getSession() {
return session;
}
private void registerHook() {
Plugin plugin = getProxy().getPluginManager().getPlugin("BungeeAuth");
if (plugin != null) {
bungeeAuthPlugin = new BungeeAuthHook();
core.setAuthPluginHook(new BungeeAuthHook());
getLogger().info("Hooked into BungeeAuth");
}
}
@Override
public String getName() {
return getDescription().getName();
}
@Override
public Map<String, Object> loadYamlFile(Reader reader) {
ConfigurationProvider configProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);
Configuration config = configProvider.load(reader);
return config.getKeys().stream()
.filter(key -> config.get(key) != null)
.collect(Collectors.toMap(Function.identity(), config::get));
}
@Override
public void sendMessage(CommandSender receiver, String message) {
receiver.sendMessage(TextComponent.fromLegacyText(message));
}
@Override
public String translateColorCodes(char colorChar, String rawMessage) {
return ChatColor.translateAlternateColorCodes(colorChar, rawMessage);
}
@Override
@SuppressWarnings("deprecation")
public ThreadFactory getThreadFactory() {
return new GroupedThreadFactory(this, getName());
}
@Override
public MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests
, Map<String, Integer> proxies) {
return new MojangApiBungee(logger, addresses, requests, proxies);
}
}

View File

@@ -1,74 +0,0 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.UUID;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
public class ForceLoginTask implements Runnable {
private final FastLoginBungee plugin;
private final ProxiedPlayer player;
private final Server server;
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player, Server server) {
this.plugin = plugin;
this.player = player;
this.server = server;
}
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
//force login only on success
if (player.getPendingConnection().isOnlineMode()) {
boolean autoRegister = plugin.getPendingAutoRegister().remove(player.getPendingConnection()) != null;
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
if (authPlugin == null) {
sendBukkitLoginNotification(autoRegister);
} else if (player.isConnected()) {
if (autoRegister) {
String password = plugin.generateStringPassword();
if (authPlugin.forceRegister(player, password)) {
sendBukkitLoginNotification(autoRegister);
}
} else if (authPlugin.forceLogin(player)) {
sendBukkitLoginNotification(autoRegister);
}
}
} else {
//cracked player
//update only on success to prevent corrupt data
playerProfile.setPremium(false);
plugin.getStorage().save(playerProfile);
}
}
private void sendBukkitLoginNotification(boolean autoRegister) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
//subchannel name
if (autoRegister) {
dataOutput.writeUTF("AUTO_REGISTER");
} else {
dataOutput.writeUTF("AUTO_LOGIN");
}
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
dataOutput.writeUTF(player.getName());
//proxy identifier to check if it's a acceptable proxy
UUID proxyId = UUID.fromString(plugin.getProxy().getConfig().getUuid());
dataOutput.writeLong(proxyId.getMostSignificantBits());
dataOutput.writeLong(proxyId.getLeastSignificantBits());
if (server != null) {
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
}
}
}

View File

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

View File

@@ -1,80 +0,0 @@
package com.github.games647.fastlogin.bungee;
import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;
import java.util.logging.Level;
import java.util.regex.Pattern;
import net.md_5.bungee.BungeeCord;
public class MojangApiConnector {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 1 * 1_000;
private static final String USER_AGENT = "Premium-Checker";
//mojang api check to prove a player is logged in minecraft and made a join server request
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
//only premium (paid account) users have a uuid from here
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
//this includes a-zA-Z1-9_
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
//compile the pattern only on plugin enable -> and this have to be threadsafe
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
private final FastLoginBungee plugin;
private final Gson gson = new Gson();
public MojangApiConnector(FastLoginBungee plugin) {
this.plugin = plugin;
}
/**
*
* @param playerName
* @return null on non-premium
*/
public UUID getPremiumUUID(String playerName) {
//check if it's a valid playername
if (playernameMatcher.matcher(playerName).matches()) {
//only make a API call if the name is valid existing mojang account
try {
HttpURLConnection connection = getConnection(UUID_LINK + playerName);
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = reader.readLine();
if (line != null && !line.equals("null")) {
MojangPlayer mojangPlayer = BungeeCord.getInstance().gson.fromJson(line, MojangPlayer.class);
return FastLoginBungee.parseId(mojangPlayer.getId());
}
}
//204 - no content for not found
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
}
return null;
}
private HttpURLConnection getConnection(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(TIMEOUT);
//the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", USER_AGENT);
return connection;
}
}

View File

@@ -1,204 +0,0 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Level;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.connection.LoginResult.Property;
import net.md_5.bungee.event.EventHandler;
/**
* Enables online mode logins for specified users and sends
* plugin message to the Bukkit version of this plugin in
* order to clear that the connection is online mode.
*/
public class PlayerConnectionListener implements Listener {
protected final FastLoginBungee plugin;
public PlayerConnectionListener(FastLoginBungee plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPreLogin(final PreLoginEvent preLoginEvent) {
if (preLoginEvent.isCancelled()) {
return;
}
preLoginEvent.registerIntent(plugin);
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
PendingConnection connection = preLoginEvent.getConnection();
String username = connection.getName();
try {
PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true);
if (playerProfile != null) {
if (playerProfile.isPremium()) {
if (playerProfile.getUserId() != -1) {
connection.setOnlineMode(true);
}
} else if (playerProfile.getUserId() == -1) {
//user not exists in the db
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
if (plugin.getConfiguration().getBoolean("autoRegister")
&& (authPlugin == null || !authPlugin.isRegistered(username))) {
UUID premiumUUID = plugin.getMojangApiConnector().getPremiumUUID(username);
if (premiumUUID != null) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
connection.setOnlineMode(true);
plugin.getPendingAutoRegister().put(connection, new Object());
}
}
}
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to check premium state", ex);
} finally {
preLoginEvent.completeIntent(plugin);
}
}
});
}
@EventHandler
public void onLogin(PostLoginEvent loginEvent) {
ProxiedPlayer player = loginEvent.getPlayer();
PendingConnection connection = player.getPendingConnection();
String username = connection.getName();
if (connection.isOnlineMode()) {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
playerProfile.setUuid(player.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
InitialHandler initialHandler = (InitialHandler) connection;
if (!plugin.getConfiguration().getBoolean("premiumUuid")) {
try {
UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(Charsets.UTF_8));
Field idField = initialHandler.getClass().getDeclaredField("uniqueId");
idField.setAccessible(true);
idField.set(connection, offlineUUID);
//bungeecord doesn't support overriding the premium uuid
// connection.setUniqueId(offlineUUID);
} catch (NoSuchFieldException | IllegalAccessException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to set offline uuid", ex);
}
}
if (!plugin.getConfiguration().getBoolean("forwardSkin")) {
//this is null on offline mode
LoginResult loginProfile = initialHandler.getLoginProfile();
if (loginProfile != null) {
loginProfile.setProperties(new Property[]{});
}
}
}
}
@EventHandler
public void onServerConnected(ServerConnectedEvent serverConnectedEvent) {
ProxiedPlayer player = serverConnectedEvent.getPlayer();
ForceLoginTask loginTask = new ForceLoginTask(plugin, player, serverConnectedEvent.getServer());
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent pluginMessageEvent) {
String channel = pluginMessageEvent.getTag();
if (pluginMessageEvent.isCancelled() || !plugin.getDescription().getName().equals(channel)) {
return;
}
//the client shouldn't be able to read the messages in order to know something about server internal states
//moreover the client shouldn't be able fake a running premium check by sending the result message
pluginMessageEvent.setCancelled(true);
//check if the message is sent from the server
if (Server.class.isAssignableFrom(pluginMessageEvent.getSender().getClass())) {
byte[] data = pluginMessageEvent.getData();
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
String subchannel = dataInput.readUTF();
final ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
if ("ON".equals(subchannel)) {
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), true);
if (playerProfile.isPremium()) {
if (forPlayer.isConnected()) {
TextComponent textComponent = new TextComponent("You are already on the premium list");
textComponent.setColor(ChatColor.DARK_RED);
forPlayer.sendMessage(textComponent);
}
return;
}
playerProfile.setPremium(true);
//todo: set uuid
plugin.getStorage().save(playerProfile);
}
});
} else if ("OFF".equals(subchannel)) {
ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() {
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), true);
if (!playerProfile.isPremium()) {
if (forPlayer.isConnected()) {
TextComponent textComponent = new TextComponent("You are not in the premium list");
textComponent.setColor(ChatColor.DARK_RED);
forPlayer.sendMessage(textComponent);
}
return;
}
playerProfile.setPremium(false);
playerProfile.setUuid(null);
//todo: set uuid
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Added to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
});
} else if ("SUCCESS".equals(subchannel)) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), false);
playerProfile.setPremium(true);
//we override this in the loginevent
// playerProfile.setUuid(forPlayer.getUniqueId());
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Removed to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
package com.github.games647.fastlogin.bungee;
import java.util.UUID;
public class PlayerProfile {
private final String playerName;
private long userId;
private UUID uuid;
private boolean premium;
private String lastIp;
private long lastLogin;
public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium
, String lastIp, long lastLogin) {
this.userId = userId;
this.uuid = uuid;
this.playerName = playerName;
this.premium = premium;
this.lastIp = lastIp;
this.lastLogin = lastLogin;
}
public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
this.userId = -1;
this.uuid = uuid;
this.playerName = playerName;
this.premium = premium;
this.lastIp = lastIp;
}
public String getPlayerName() {
return playerName;
}
public synchronized long getUserId() {
return userId;
}
protected synchronized void setUserId(long generatedId) {
this.userId = generatedId;
}
public synchronized UUID getUuid() {
return uuid;
}
public synchronized void setUuid(UUID uuid) {
this.uuid = uuid;
}
public synchronized boolean isPremium() {
return premium;
}
public synchronized void setPremium(boolean premium) {
this.premium = premium;
}
public synchronized String getLastIp() {
return lastIp;
}
public synchronized void setLastIp(String lastIp) {
this.lastIp = lastIp;
}
public synchronized long getLastLogin() {
return lastLogin;
}
public synchronized void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
}

View File

@@ -1,201 +0,0 @@
package com.github.games647.fastlogin.bungee;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class Storage {
private static final String PREMIUM_TABLE = "premium";
private final ConcurrentMap<String, PlayerProfile> profileCache = CacheBuilder
.<String, PlayerProfile>newBuilder()
.concurrencyLevel(20)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build(new CacheLoader<String, PlayerProfile>() {
@Override
public PlayerProfile load(String key) throws Exception {
//should be fetched manually
throw new UnsupportedOperationException("Not supported yet.");
}
}).asMap();
private final HikariDataSource dataSource;
private final FastLoginBungee plugin;
public Storage(FastLoginBungee plugin, String driver, String host, int port, String databasePath
, String user, String pass) {
this.plugin = plugin;
HikariConfig databaseConfig = new HikariConfig();
databaseConfig.setUsername(user);
databaseConfig.setPassword(pass);
databaseConfig.setDriverClassName(driver);
databasePath = databasePath.replace("{pluginDir}", plugin.getDataFolder().getAbsolutePath());
String jdbcUrl = "jdbc:";
if (driver.contains("sqlite")) {
jdbcUrl += "sqlite" + "://" + databasePath;
databaseConfig.setConnectionTestQuery("SELECT 1");
} else {
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
}
databaseConfig.setJdbcUrl(jdbcUrl);
this.dataSource = new HikariDataSource(databaseConfig);
}
public void createTables() throws SQLException {
Connection con = null;
try {
con = dataSource.getConnection();
Statement statement = con.createStatement();
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "`UUID` CHAR(36), "
+ "`Name` VARCHAR(16) NOT NULL, "
+ "`Premium` BOOLEAN NOT NULL, "
+ "`LastIp` VARCHAR(255) NOT NULL, "
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+ "UNIQUE (`UUID`), "
//the premium shouldn't steal the cracked account by changing the name
+ "UNIQUE (`Name`) "
+ ")";
if (dataSource.getJdbcUrl().contains("sqlite")) {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
statement.executeUpdate(createDataStmt);
} finally {
closeQuietly(con);
}
}
public PlayerProfile getProfile(String name, boolean fetch) {
if (profileCache.containsKey(name)) {
return profileCache.get(name);
} else if (fetch) {
Connection con = null;
try {
con = dataSource.getConnection();
PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE
+ " WHERE `Name`=? LIMIT 1");
loadStatement.setString(1, name);
ResultSet resultSet = loadStatement.executeQuery();
if (resultSet.next()) {
long userId = resultSet.getInt(1);
String unparsedUUID = resultSet.getString(2);
UUID uuid;
if (unparsedUUID == null) {
uuid = null;
} else {
uuid = FastLoginBungee.parseId(unparsedUUID);
}
// String name = resultSet.getString(3);
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
long lastLogin = resultSet.getTimestamp(6).getTime();
PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
profileCache.put(name, playerProfile);
return playerProfile;
} else {
PlayerProfile crackedProfile = new PlayerProfile(null, name, false, "");
profileCache.put(name, crackedProfile);
return crackedProfile;
}
} catch (SQLException sqlEx) {
plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
}
}
return null;
}
public boolean save(PlayerProfile playerProfile) {
Connection con = null;
try {
con = dataSource.getConnection();
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
PreparedStatement saveStatement = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
if (uuid == null) {
saveStatement.setString(1, null);
} else {
saveStatement.setString(1, uuid.toString().replace("-", ""));
}
saveStatement.setString(2, playerProfile.getPlayerName());
saveStatement.setBoolean(3, playerProfile.isPremium());
saveStatement.setString(4, playerProfile.getLastIp());
saveStatement.execute();
ResultSet generatedKeys = saveStatement.getGeneratedKeys();
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setUserId(generatedKeys.getInt(1));
}
} else {
PreparedStatement saveStatement = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
if (uuid == null) {
saveStatement.setString(1, null);
} else {
saveStatement.setString(1, uuid.toString().replace("-", ""));
}
saveStatement.setString(2, playerProfile.getPlayerName());
saveStatement.setBoolean(3, playerProfile.isPremium());
saveStatement.setString(4, playerProfile.getLastIp());
// saveStatement.setTimestamp(5, new Timestamp(playerProfile.getLastLogin()));
saveStatement.setLong(5, playerProfile.getUserId());
saveStatement.execute();
}
return true;
} catch (SQLException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex);
} finally {
closeQuietly(con);
}
return false;
}
public void close() {
dataSource.close();
profileCache.clear();
}
private void closeQuietly(Connection con) {
if (con != null) {
try {
con.close();
} catch (SQLException sqlEx) {
plugin.getLogger().log(Level.SEVERE, "Failed to close connection", sqlEx);
}
}
}
}

View File

@@ -1,104 +1,40 @@
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 com.github.games647.fastlogin.core.hooks.AuthPlugin;
import me.vik1395.BungeeAuth.ListenerClass;
import me.vik1395.BungeeAuth.Main;
import me.vik1395.BungeeAuth.Password.PasswordHandler;
import me.vik1395.BungeeAuth.Tables;
import me.vik1395.BungeeAuthAPI.RequestHandler;
import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* Github: https://github.com/MatteCarra/BungeeAuth
* Github: https://github.com/vik1395/BungeeAuth-Minecraft
*
* Project page:
*
* Spigot: https://www.spigotmc.org/resources/bungeeauth.493/
*/
public class BungeeAuthHook implements BungeeAuthPlugin {
public class BungeeAuthHook implements AuthPlugin<ProxiedPlayer> {
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L32
private final Tables databaseConnection = new Tables();
private final RequestHandler requestHandler = new RequestHandler();
@Override
public boolean forceLogin(final ProxiedPlayer player) {
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Login.java#L92-95
Main.plonline.add(player.getName());
//renamed from ct to databaseConnection
// databaseConnection.setStatus(player.getName(), "online");
final Class<?>[] parameterTypes = new Class<?>[]{String.class, String.class};
final Object[] arguments = new Object[]{player.getName(), "online"};
try {
callProtected("setStatus", parameterTypes, arguments);
ListenerClass.movePlayer(player, false);
//proparly not thread-safe
ListenerClass.prelogin.get(player.getName()).cancel();
} catch (Exception ex) {
Main.plugin.getLogger().severe("[BungeeAuth] Error force loging in player");
return false;
public boolean forceLogin(ProxiedPlayer player) {
String playerName = player.getName();
if (Main.plonline.contains(playerName)) {
return true;
}
return true;
return requestHandler.forceLogin(playerName);
}
@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);
return requestHandler.isRegistered(playerName);
}
@Override
public boolean forceRegister(final ProxiedPlayer player, String password) {
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L102
PasswordHandler ph = new PasswordHandler();
Random rand = new Random();
int maxp = 7; //Total Password Hashing methods.
Date dNow = new Date();
SimpleDateFormat ft = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
String Pw = password;
String pType = "" + rand.nextInt(maxp + 1);
String regdate = ft.format(dNow);
//https://github.com/MatteCarra/BungeeAuth/blob/master/src/me/vik1395/BungeeAuth/Register.java#L60
String lastip = player.getAddress().getAddress().getHostAddress();
String lastseen = regdate;
String hash = ph.newHash(Pw, pType);
//creates a new SQL entry with the player's details.
//renamed t to databaseConnection
// databaseConnection.newPlayerEntry(player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen);
final Class<?>[] parameterTypes = new Class<?>[] {String.class, String.class, String.class, String.class
, String.class, String.class, String.class, String.class};
final Object[] arguments = new Object[] {player.getName(), hash, pType, "", lastip, regdate, lastip, lastseen};
try {
callProtected("newPlayerEntry", parameterTypes, arguments);
//proparly not thread-safe
forceLogin(player);
} catch (Exception ex) {
Main.plugin.getLogger().severe("[BungeeAuth] Error when creating a new player in the Database");
return false;
}
return true;
}
//pail ;(
private void callProtected(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
Class<? extends Tables> tableClass = databaseConnection.getClass();
Method method = tableClass.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
method.invoke(databaseConnection, arguments);
public boolean forceRegister(ProxiedPlayer player, String password) {
return requestHandler.forceRegister(player, password);
}
}

View File

@@ -1,55 +0,0 @@
package com.github.games647.fastlogin.bungee.hooks;
import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* Represents a supporting authentication plugin in BungeeCord/Waterfall/... servers
*/
public interface BungeeAuthPlugin {
/**
* Login the premium (paid account) player after
* the player joined successfully a server.
*
* @param player the player that needs to be logged in
* @return if the operation was successful
*/
boolean forceLogin(ProxiedPlayer player);
/**
* Checks whether an account exists for this player name.
*
* This check should check if a cracked player account exists
* so we can be sure the premium player doesn't steal the account
* of that player.
*
* This operation will be performed async while the player is
* connecting
*
* @param playerName player name
* @return if the player has an account
* @throws Exception if an error occurred
*/
boolean isRegistered(String playerName) throws Exception;
/**
* Forces a register in order to protect the paid account.
* The method will be invoked after the player joined a server.
*
* After a successful registration the player should be logged
* in too.
*
* The method will be called only for premium accounts.
* So it's recommended to set additionally premium property
* if possible.
*
* If we don't register an account, cracked players
* could steal the unregistered account from the paid
* player account
*
* @param player the premium account
* @param password a strong random generated password
* @return if the operation was successful
*/
boolean forceRegister(ProxiedPlayer player, String password);
}

View File

@@ -0,0 +1,114 @@
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.PlayerProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
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.connection.Server;
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 {
private final FastLoginBungee plugin;
public PlayerConnectionListener(FastLoginBungee plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPreLogin(PreLoginEvent preLoginEvent) {
if (preLoginEvent.isCancelled()) {
return;
}
preLoginEvent.registerIntent(plugin);
PendingConnection connection = preLoginEvent.getConnection();
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
ProxyServer.getInstance().getScheduler().runAsync(plugin, asyncPremiumCheck);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onLogin(LoginEvent loginEvent) {
if (loginEvent.isCancelled()) {
return;
}
//use the login event 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();
InitialHandler initialHandler = (InitialHandler) connection;
String username = initialHandler.getLoginRequest().getData();
if (connection.isOnlineMode()) {
LoginSession session = plugin.getSession().get(connection);
session.setUuid(connection.getUniqueId());
PlayerProfile playerProfile = session.getProfile();
playerProfile.setUuid(connection.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
if (!plugin.getCore().getConfig().get("premiumUuid", true)) {
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.getCore().getConfig().get("forwardSkin", true)) {
//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();
Server server = serverConnectedEvent.getServer();
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
ProxiedPlayer player = disconnectEvent.getPlayer();
plugin.getSession().remove(player.getPendingConnection());
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -0,0 +1,97 @@
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.github.games647.fastlogin.core.shared.FastLoginCore;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import java.util.Arrays;
import net.md_5.bungee.api.CommandSender;
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 {
private 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())) {
//so that we can safely process this in the background
byte[] data = Arrays.copyOf(pluginMessageEvent.getData(), pluginMessageEvent.getData().length);
ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> readMessage(forPlayer, data));
}
}
private void readMessage(ProxiedPlayer forPlayer, byte[] data) {
FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core = plugin.getCore();
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
String subchannel = dataInput.readUTF();
if ("SUCCESS".equals(subchannel)) {
onSuccessMessage(forPlayer);
} else if ("ON".equals(subchannel)) {
String playerName = dataInput.readUTF();
boolean isPlayerSender = dataInput.readBoolean();
if (playerName.equals(forPlayer.getName()) && plugin.getCore().getConfig().get("premium-warning", true)
&& !core.getPendingConfirms().contains(forPlayer.getUniqueId())) {
String message = core.getMessage("premium-warning");
forPlayer.sendMessage(TextComponent.fromLegacyText(message));
core.getPendingConfirms().add(forPlayer.getUniqueId());
return;
}
core.getPendingConfirms().remove(forPlayer.getUniqueId());
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, true, isPlayerSender);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else if ("OFF".equals(subchannel)) {
String playerName = dataInput.readUTF();
boolean isPlayerSender = dataInput.readBoolean();
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, false, isPlayerSender);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
}
}
private void onSuccessMessage(ProxiedPlayer forPlayer) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
BungeeLoginSession loginSession = plugin.getSession().get(forPlayer.getPendingConnection());
PlayerProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
}
}
}
}

View File

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

View File

@@ -0,0 +1,73 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
public class AsyncToggleMessage implements Runnable {
private final FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core;
private final ProxiedPlayer sender;
private final String targetPlayer;
private final boolean toPremium;
private final boolean isPlayerSender;
public AsyncToggleMessage(FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core,
ProxiedPlayer sender, String playerName, boolean toPremium, boolean playerSender) {
this.core = core;
this.sender = sender;
this.targetPlayer = playerName;
this.toPremium = toPremium;
this.isPlayerSender = playerSender;
}
@Override
public void run() {
if (toPremium) {
activatePremium();
} else {
turnOffPremium();
}
}
private void turnOffPremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
//existing player is already cracked
if (playerProfile.getUserId() != -1 && !playerProfile.isPremium()) {
sendMessage("not-premium");
return;
}
playerProfile.setPremium(false);
playerProfile.setUuid(null);
core.getStorage().save(playerProfile);
sendMessage("remove-premium");
}
private void activatePremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
if (playerProfile.isPremium()) {
sendMessage("already-exists");
return;
}
playerProfile.setPremium(true);
core.getStorage().save(playerProfile);
sendMessage("add-premium");
}
private void sendMessage(String localeId) {
String message = core.getMessage(localeId);
if (isPlayerSender) {
sender.sendMessage(TextComponent.fromLegacyText(message));
} else {
CommandSender console = ProxyServer.getInstance().getConsole();
console.sendMessage(TextComponent.fromLegacyText(message));
}
}
}

View File

@@ -0,0 +1,101 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.ForceLoginManagement;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.UUID;
import net.md_5.bungee.api.CommandSender;
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 extends ForceLoginManagement<ProxiedPlayer, CommandSender, BungeeLoginSession, FastLoginBungee> {
private final Server server;
public ForceLoginTask(FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core,
ProxiedPlayer player, Server server) {
super(core, player);
this.server = server;
}
@Override
public void run() {
PendingConnection pendingConnection = player.getPendingConnection();
session = core.getPlugin().getSession().get(pendingConnection);
if (session == null) {
return;
}
super.run();
if (!isOnlineMode()) {
session.setAlreadySaved(true);
}
}
@Override
public boolean forceLogin(ProxiedPlayer player) {
if (session.isAlreadyLogged()) {
return true;
}
session.setAlreadyLogged(true);
return super.forceLogin(player);
}
@Override
public boolean forceRegister(ProxiedPlayer player) {
if (session.isAlreadyLogged()) {
return true;
}
return super.forceRegister(player);
}
@Override
public void onForceActionSuccess(LoginSession session) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
//sub channel name
if (session.needsRegistration()) {
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(core.getPlugin().getProxy().getConfig().getUuid());
dataOutput.writeLong(proxyId.getMostSignificantBits());
dataOutput.writeLong(proxyId.getLeastSignificantBits());
if (server != null) {
server.sendData(core.getPlugin().getName(), dataOutput.toByteArray());
}
}
@Override
public String getName(ProxiedPlayer player) {
return player.getName();
}
@Override
public boolean isOnline(ProxiedPlayer player) {
return player.isConnected();
}
@Override
public boolean isOnlineMode() {
return player.getPendingConnection().isOnlineMode();
}
}

View File

@@ -1,16 +1,16 @@
# project informations for BungeeCord
# project data 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}
version: ${project.version}-git${git.commit.id}
author: games647, http://github.com/games647/FastLogin/graphs/contributors
softdepends:
softDepends:
# BungeeCord auth plugins
- BungeeAuth
description: |
${project.description}
${project.description}

39
core/pom.xml Normal file
View File

@@ -0,0 +1,39 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.10</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.6.3</version>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,265 @@
package com.github.games647.fastlogin.core;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
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.Properties;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import javax.sql.DataSource;
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, boolean useSSL) {
this.core = core;
HikariConfig config = new HikariConfig();
config.setUsername(user);
config.setPassword(pass);
config.setDriverClassName(driver);
//a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647
Properties properties = new Properties();
properties.setProperty("date_string_format", "yyyy-MM-dd HH:mm:ss");
properties.setProperty("useSSL", String.valueOf(useSSL));
config.setDataSourceProperties(properties);
ThreadFactory platformThreadFactory = core.getPlugin().getThreadFactory();
if (platformThreadFactory != null) {
config.setThreadFactory(new ThreadFactoryBuilder()
.setNameFormat(core.getPlugin().getName() + " Database Pool Thread #%1$d")
//Hikari create daemons by default
.setDaemon(true)
.setThreadFactory(platformThreadFactory)
.build());
}
databasePath = databasePath.replace("{pluginDir}", core.getPlugin().getDataFolder().getAbsolutePath());
String jdbcUrl = "jdbc:";
if (driver.contains("sqlite")) {
jdbcUrl += "sqlite" + "://" + databasePath;
config.setConnectionTestQuery("SELECT 1");
} else {
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
}
config.setJdbcUrl(jdbcUrl);
this.dataSource = new HikariDataSource(config);
}
public DataSource getDataSource() {
return dataSource;
}
public void createTables() throws SQLException {
Connection con = null;
Statement createStmt = null;
try {
con = dataSource.getConnection();
createStmt = con.createStatement();
String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " ("
+ "UserID INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "UUID CHAR(36), "
+ "Name VARCHAR(16) NOT NULL, "
+ "Premium BOOLEAN NOT NULL, "
+ "LastIp VARCHAR(255) NOT NULL, "
+ "LastLogin TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
//the premium shouldn't steal the cracked account by changing the name
+ "UNIQUE (Name) "
+ ')';
if (dataSource.getJdbcUrl().contains("sqlite")) {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
createStmt.executeUpdate(createDataStmt);
//drop the old unique uuid index
try {
if (dataSource.getJdbcUrl().contains("sqlite")) {
String tempTableCreate = createDataStmt.replace(PREMIUM_TABLE, PREMIUM_TABLE + "_TEMP")
//if we already imported the table fail here
.replace("IF NOT EXISTS", "");
//create a temp table insert it there and then back
createStmt.executeUpdate(tempTableCreate);
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + "_TEMP SELECT * FROM " + PREMIUM_TABLE);
createStmt.executeUpdate("DROP TABLE " + PREMIUM_TABLE);
createStmt.executeUpdate(createDataStmt);
//insert it back into the new table
createStmt.executeUpdate("INSERT INTO " + PREMIUM_TABLE + " SELECT * FROM " + PREMIUM_TABLE + "_TEMP");
} else {
createStmt.executeUpdate("ALTER TABLE premium DROP INDEX UUID");
}
} catch (SQLException sqlEx) {
//silent - we already migrated
}
try {
createStmt.executeUpdate("CREATE INDEX uuid_idx on premium (UUID)");
} catch (SQLException sqlEx) {
//silent - we already migrated
}
} finally {
closeQuietly(con);
closeQuietly(createStmt);
}
}
public PlayerProfile loadProfile(String name) {
Connection con = null;
PreparedStatement loadStmt = null;
ResultSet resultSet = null;
try {
con = dataSource.getConnection();
loadStmt = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + " WHERE Name=? LIMIT 1");
loadStmt.setString(1, name);
resultSet = loadStmt.executeQuery();
if (resultSet.next()) {
long userId = resultSet.getInt(1);
UUID uuid = FastLoginCore.parseId(resultSet.getString(2));
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
long lastLogin = resultSet.getTimestamp(6).getTime();
return new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
} else {
return new PlayerProfile(null, name, false, "");
}
} catch (SQLException sqlEx) {
core.getPlugin().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();
return new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin);
}
} catch (SQLException sqlEx) {
core.getPlugin().getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
closeQuietly(loadStmt);
closeQuietly(resultSet);
}
return null;
}
public boolean save(PlayerProfile playerProfile) {
Connection con = null;
PreparedStatement updateStmt = null;
PreparedStatement saveStmt = null;
ResultSet generatedKeys = null;
try {
con = dataSource.getConnection();
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
saveStmt = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) ", Statement.RETURN_GENERATED_KEYS);
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
}
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
saveStmt.execute();
generatedKeys = saveStmt.getGeneratedKeys();
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setUserId(generatedKeys.getInt(1));
}
} else {
saveStmt = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
}
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
saveStmt.setLong(5, playerProfile.getUserId());
saveStmt.execute();
}
return true;
} catch (SQLException ex) {
core.getPlugin().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.getPlugin().getLogger().log(Level.SEVERE, "Failed to close connection", closeEx);
}
}
}
}

View File

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

View File

@@ -0,0 +1,321 @@
package com.github.games647.fastlogin.core;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.RemovalListener;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Represents a Guava CacheBuilder that is compatible with both Guava 10 (Minecraft 1.7.X) and 13
*/
public class CompatibleCacheBuilder<K, V> {
private static Method BUILD_METHOD;
private static Method AS_MAP_METHOD;
/**
* Construct a new safe cache builder.
*
* @param <K> Key type
* @param <V> Value type
*
* @return A new cache builder.
*/
public static <K, V> CompatibleCacheBuilder<K, V> newBuilder() {
return new CompatibleCacheBuilder<>();
}
private final CacheBuilder<K, V> builder;
@SuppressWarnings("unchecked")
private CompatibleCacheBuilder() {
builder = (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
/**
* Guides the allowed concurrency among update operations. Used as a hint for internal sizing. The table is
* internally partitioned to try to permit the indicated number of concurrent updates without contention. Because
* assignment of entries to these partitions is not necessarily uniform, the actual concurrency observed may vary.
* Ideally, you should choose a value to accommodate as many threads as will ever concurrently modify the table.
* Using a significantly higher value than you need can waste space and time, and a significantly lower value can
* lead to thread contention. But overestimates and underestimates within an order of magnitude do not usually have
* much noticeable impact. A value of one permits only one thread to modify the cache at a time, but since read
* operations can proceed concurrently, this still yields higher concurrency than full synchronization. Defaults to
* 4.
*
* <p>
* <b>Note:</b>The default may change in the future. If you care about this value, you should always choose it
* explicitly.
*
* @param concurrencyLevel New concurrency level
* @return This for chaining
*
* @throws IllegalArgumentException if {@code concurrencyLevel} is non-positive
* @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 absence 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 absence 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;
if (BUILD_METHOD == null) {
try {
BUILD_METHOD = builder.getClass().getDeclaredMethod("build", CacheLoader.class);
BUILD_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find CacheBuilder.build(CacheLoader)", e);
}
}
// Attempt to build the Cache
try {
cache = BUILD_METHOD.invoke(builder, loader);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + BUILD_METHOD + " on " + builder, e);
}
if (AS_MAP_METHOD == null) {
try {
AS_MAP_METHOD = cache.getClass().getMethod("asMap");
AS_MAP_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find Cache.asMap() in " + cache, e);
}
}
// Retrieve it as a map
try {
return (ConcurrentMap<K1, V1>) AS_MAP_METHOD.invoke(cache);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + AS_MAP_METHOD + " on " + cache, e);
}
}
}

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
package com.github.games647.fastlogin.bukkit.hooks;
import org.bukkit.entity.Player;
package com.github.games647.fastlogin.core.hooks;
/**
* Represents a supporting authentication plugin in Bukkit/Spigot/... servers
* Represents a supporting authentication plugin in BungeeCord and Bukkit/Spigot/... servers
*
* @param <P> either org.bukkit.entity.Player for Bukkit or net.md_5.bungee.api.connection.ProxiedPlayer for BungeeCord
*/
public interface BukkitAuthPlugin {
public interface AuthPlugin<P> {
/**
* Login the premium (paid account) player after
* the player joined successfully the server.
* 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>
@@ -17,23 +16,7 @@ public interface BukkitAuthPlugin {
* @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;
boolean forceLogin(P player);
/**
* Forces a register in order to protect the paid account.
@@ -56,5 +39,21 @@ public interface BukkitAuthPlugin {
* @param password a strong random generated password
* @return if the operation was successful
*/
boolean forceRegister(Player player, String password);
boolean forceRegister(P player, String password);
/**
* Checks whether an account exists for this player name.
*
* This check should check if a cracked player account exists
* so we can be sure the premium player doesn't steal the account
* of that player.
*
* This operation will be performed async while the player is
* connecting.
*
* @param playerName player name
* @return if the player has an account
* @throws Exception if an error occurred
*/
boolean isRegistered(String playerName) throws Exception;
}

View File

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

View File

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

View File

@@ -0,0 +1,232 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.CompatibleCacheBuilder;
import com.github.games647.fastlogin.core.SharedConfig;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
* @param <P> Player class
* @param <C> CommandSender
* @param <T> Plugin class
*/
public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
CompatibleCacheBuilder<Object, Object> builder = CompatibleCacheBuilder.newBuilder();
if (expireAfterWrite > 0) {
builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);
}
if (maxSize > 0) {
builder.maximumSize(maxSize);
}
return builder.build(CacheLoader.from(() -> {
throw new UnsupportedOperationException();
}));
}
public static UUID parseId(String withoutDashes) {
if (withoutDashes == null) {
return null;
}
return UUID.fromString(withoutDashes.substring(0, 8)
+ '-' + withoutDashes.substring(8, 12)
+ '-' + withoutDashes.substring(12, 16)
+ '-' + withoutDashes.substring(16, 20)
+ '-' + withoutDashes.substring(20, 32));
}
protected final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> pendingLogin = FastLoginCore.buildCache(5, -1);
private final Set<UUID> pendingConfirms = Sets.newHashSet();
private final T plugin;
private SharedConfig sharedConfig;
private MojangApiConnector apiConnector;
private AuthStorage storage;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
public FastLoginCore(T plugin) {
this.plugin = plugin;
}
public void load() {
saveDefaultFile("messages.yml");
saveDefaultFile("config.yml");
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("config.yml")));
sharedConfig = new SharedConfig(plugin.loadYamlFile(reader));
reader.close();
reader = Files.newBufferedReader(plugin.getDataFolder().toPath().resolve("config.yml"));
sharedConfig.getConfigValues().putAll(plugin.loadYamlFile(reader));
reader.close();
reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("messages.yml")));
reader = Files.newBufferedReader(plugin.getDataFolder().toPath().resolve("messages.yml"));
Map<String, Object> messageConfig = plugin.loadYamlFile(reader);
reader.close();
reader = Files.newBufferedReader(plugin.getDataFolder().toPath().resolve("messages.yml"));
messageConfig.putAll(plugin.loadYamlFile(reader));
for (Entry<String, Object> entry : messageConfig.entrySet()) {
String message = plugin.translateColorCodes('&', (String) entry.getValue());
if (!message.isEmpty()) {
localeMessages.put(entry.getKey(), message);
}
}
reader.close();
} catch (IOException ioEx) {
plugin.getLogger().log(Level.INFO, "Failed to load yaml files", ioEx);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, null, ex);
}
}
}
List<String> ipAddresses = sharedConfig.get("ip-addresses");
int requestLimit = sharedConfig.get("mojang-request-limit");
List<String> proxyList = sharedConfig.get("proxies", Lists.newArrayList());
Map<String, Integer> proxies = proxyList.stream()
.collect(Collectors
.toMap(line -> line.split(":")[0], line -> Integer.parseInt(line.split(":")[1])));
this.apiConnector = plugin.makeApiConnector(plugin.getLogger(), ipAddresses, requestLimit, proxies);
}
public MojangApiConnector getApiConnector() {
return apiConnector;
}
public AuthStorage getStorage() {
return storage;
}
public T getPlugin() {
return plugin;
}
public void sendLocaleMessage(String key, C receiver) {
String message = localeMessages.get(key);
if (message != null) {
plugin.sendMessage(receiver, message);
}
}
public String getMessage(String key) {
return localeMessages.get(key);
}
public boolean setupDatabase() {
String driver = sharedConfig.get("driver");
String host = sharedConfig.get("host", "");
int port = sharedConfig.get("port", 3306);
String database = sharedConfig.get("database");
String user = sharedConfig.get("username", "");
String password = sharedConfig.get("password", "");
boolean useSSL = sharedConfig.get("useSSL", false);
storage = new AuthStorage(this, driver, host, port, database, user, password, useSSL);
try {
storage.createTables();
return true;
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
return false;
}
}
public SharedConfig getConfig() {
return sharedConfig;
}
public PasswordGenerator<P> getPasswordGenerator() {
return passwordGenerator;
}
public void setPasswordGenerator(PasswordGenerator<P> passwordGenerator) {
this.passwordGenerator = passwordGenerator;
}
public ConcurrentMap<String, Object> getPendingLogin() {
return pendingLogin;
}
public Collection<UUID> getPendingConfirms() {
return pendingConfirms;
}
public AuthPlugin<P> getAuthPluginHook() {
return authPlugin;
}
public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
this.authPlugin = authPlugin;
}
public void saveDefaultFile(String fileName) {
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdir();
}
Path configFile = plugin.getDataFolder().toPath().resolve(fileName);
if (Files.notExists(configFile)) {
InputStream in = getClass().getClassLoader().getResourceAsStream(fileName);
try {
Files.copy(in, configFile);
} catch (IOException ioExc) {
plugin.getLogger().log(Level.SEVERE, "Error saving default " + fileName, ioExc);
} finally {
try {
in.close();
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, null, ex);
}
}
}
}
public void close() {
if (storage != null) {
storage.close();
}
}
}

View File

@@ -0,0 +1,106 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.logging.Level;
public abstract class ForceLoginManagement<P extends C, C, L extends LoginSession, T extends PlatformPlugin<C>>
implements Runnable {
protected final FastLoginCore<P, C, T> core;
protected final P player;
protected L session;
public ForceLoginManagement(FastLoginCore<P, C, T> core, P player) {
this.core = core;
this.player = player;
}
@Override
public void run() {
if (!isOnline(player) || session == null) {
return;
}
AuthStorage storage = core.getStorage();
PlayerProfile playerProfile = session.getProfile();
try {
if (isOnlineMode()) {
//premium player
AuthPlugin<P> authPlugin = core.getAuthPluginHook();
if (authPlugin == null) {
//maybe only bungeecord plugin
onForceActionSuccess(session);
} else {
boolean success = true;
String playerName = getName(player);
if (core.getConfig().get("autoLogin", true)) {
if (session.needsRegistration()
|| (core.getConfig().get("auto-register-unknown", false)
&& !authPlugin.isRegistered(playerName))) {
success = forceRegister(player);
} else {
success = forceLogin(player);
}
}
if (success) {
//update only on success to prevent corrupt data
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
playerProfile.setPremium(true);
storage.save(playerProfile);
}
onForceActionSuccess(session);
}
}
} else if (playerProfile != null) {
//cracked player
playerProfile.setUuid(null);
playerProfile.setPremium(false);
storage.save(playerProfile);
}
} catch (Exception ex) {
core.getPlugin().getLogger().log(Level.WARNING, "ERROR ON FORCE LOGIN", ex);
}
}
public boolean forceRegister(P player) {
core.getPlugin().getLogger().log(Level.INFO, "Register player {0}", getName(player));
String generatedPassword = core.getPasswordGenerator().getRandomPassword(player);
boolean success = core.getAuthPluginHook().forceRegister(player, generatedPassword);
String message = core.getMessage("auto-register");
if (success && message != null) {
message = message.replace("%password", generatedPassword);
core.getPlugin().sendMessage(player, message);
}
return success;
}
public boolean forceLogin(P player) {
core.getPlugin().getLogger().log(Level.INFO, "Logging player {0} in", getName(player));
boolean success = core.getAuthPluginHook().forceLogin(player);
if (success) {
core.sendLocaleMessage("auto-login", player);
}
return success;
}
public abstract void onForceActionSuccess(LoginSession session);
public abstract String getName(P player);
public abstract boolean isOnline(P player);
public abstract boolean isOnlineMode();
}

View File

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

View File

@@ -0,0 +1,56 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.util.UUID;
public abstract class LoginSession {
private final String username;
private final PlayerProfile profile;
private UUID uuid;
protected boolean registered;
public LoginSession(String username, boolean registered, PlayerProfile profile) {
this.username = username;
this.registered = registered;
this.profile = profile;
}
public String getUsername() {
return username;
}
/**
* This value is always false if we authenticate the player with a cracked authentication
*
* @return
*/
public boolean needsRegistration() {
return !registered;
}
public PlayerProfile getProfile() {
return profile;
}
/**
* Get the premium UUID of this player
*
* @return the premium UUID or null if not fetched
*/
public synchronized UUID getUuid() {
return uuid;
}
/**
* Set the online UUID if it's fetched
*
* @param uuid premium UUID
*/
public synchronized void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

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

View File

@@ -0,0 +1,161 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.BalancedSSLFactory;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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";
//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 thread-safe
private final Pattern nameMatcher = Pattern.compile(VALID_PLAYERNAME);
private final Iterator<Proxy> proxies;
private final ConcurrentMap<Object, Object> requests = FastLoginCore.buildCache(10, -1);
private final BalancedSSLFactory sslFactory;
private final int rateLimit;
private long lastRateLimit;
protected final Logger logger;
public MojangApiConnector(Logger logger, Collection<String> localAddresses, int rateLimit
, Map<String, Integer> proxies) {
this.logger = logger;
this.rateLimit = Math.max(rateLimit, 600);
this.sslFactory = buildAddresses(logger, localAddresses);
List<Proxy> proxyBuilder = Lists.newArrayList();
for (Entry<String, Integer> proxy : proxies.entrySet()) {
proxyBuilder.add(new Proxy(Type.HTTP, new InetSocketAddress(proxy.getKey(), proxy.getValue())));
}
this.proxies = Iterables.cycle(proxyBuilder).iterator();
}
/**
* @return null on non-premium
*/
public UUID getPremiumUUID(String playerName) {
if (!nameMatcher.matcher(playerName).matches()) {
//check if it's a valid player name
return null;
}
try {
HttpsURLConnection connection;
if (requests.size() >= rateLimit || System.currentTimeMillis() - lastRateLimit < 1_000 * 60 * 10) {
synchronized (proxies) {
if (proxies.hasNext()) {
connection = getConnection(UUID_LINK + playerName, proxies.next());
} else {
return null;
}
}
} else {
requests.put(new Object(), new Object());
connection = getConnection(UUID_LINK + playerName);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = reader.readLine();
if (!"null".equals(line)) {
return FastLoginCore.parseId(getUUIDFromJson(line));
}
} else if (connection.getResponseCode() == RATE_LIMIT_CODE) {
logger.info("RATE_LIMIT REACHED");
lastRateLimit = System.currentTimeMillis();
if (!connection.usingProxy()) {
return getPremiumUUID(playerName);
}
}
//204 - no content for not found
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to check if player has a paid account", ex);
}
return null;
}
public abstract boolean hasJoinedServer(LoginSession session, String serverId, InetSocketAddress ip);
protected abstract String getUUIDFromJson(String json);
protected HttpsURLConnection getConnection(String url, Proxy proxy) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(proxy);
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);
//this connection doesn't need to be closed. So can make use of keep alive in java
if (sslFactory != null) {
connection.setSSLSocketFactory(sslFactory);
}
return connection;
}
protected HttpsURLConnection getConnection(String url) throws IOException {
return getConnection(url, Proxy.NO_PROXY);
}
private BalancedSSLFactory buildAddresses(Logger logger, Collection<String> localAddresses) {
if (localAddresses.isEmpty()) {
return null;
} else {
Set<InetAddress> addresses = Sets.newHashSet();
for (String localAddress : localAddresses) {
try {
InetAddress address = InetAddress.getByName(localAddress);
if (!address.isAnyLocalAddress()) {
logger.log(Level.WARNING, "Submitted IP-Address is not local {0}", address);
continue;
}
addresses.add(address);
} catch (UnknownHostException ex) {
logger.log(Level.SEVERE, "IP-Address is unknown to us", ex);
}
}
return new BalancedSSLFactory(HttpsURLConnection.getDefaultSSLSocketFactory(), addresses);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.github.games647.fastlogin.core.shared;
import java.io.File;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;
public interface PlatformPlugin<C> {
String getName();
File getDataFolder();
Logger getLogger();
Map<String, Object> loadYamlFile(Reader reader);
void sendMessage(C receiver, String message);
ThreadFactory getThreadFactory();
String translateColorCodes(char colorChar, String rawMessage);
MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests
, Map<String, Integer> proxies);
}

View File

@@ -0,0 +1,187 @@
# FastLogin config
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
# Source code: https://github.com/games647/FastLogin
#
# You can access the newest config here:
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/config.yml
# Request a premium login without forcing the player to type a command
#
# If you activate autoRegister, this plugin will check/do these points on login:
# 1. An existing cracked account shouldn't exist
# -> paid accounts cannot steal the existing account of cracked players
# - (Already registered players could still use the /premium command to activate premium checks)
# 2. Automatically registers an account with a strong random generated password
# -> cracked player cannot register an account for the premium player and so cannot the steal the account
#
# Furthermore the premium player check have to be made based on the player name
# This means if a cracked player connects to the server and we request a paid account login from this player
# the player just disconnect and sees the message: 'bad login' or 'invalid session'
# There is no way to change this message
# For more information: https://github.com/games647/FastLogin#why-do-players-have-to-invoke-a-command
autoRegister: false
# This is extra configuration option to the feature above. If we request a premium authentication from a player who
# isn't actual premium but used a premium username, the player will disconnect with the reason "invalid session" or
# "bad login".
#
# If you activate this, we are remembering this player and do not force another premium authentication if the player
# tries to join again, so the player could join as cracked player.
secondAttemptCracked: false
# New cracked players will be kicked from server. Good if you want switch from offline-mode to online-mode without
# losing players!
#
# Existing cracked and premium players could still join your server. Moreover you could add playernames to a whitelist.
# So that these cracked players could join too although they are new players.
switchMode: false
# If this plugin detected that a player has a premium, it can also set the associated
# uuid from that account. So if the player changes the username, they will still have
# the same player data (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 authentication from offline/cracked
# authentication.
#
# This feature requires Cauldron, Spigot or a fork of Spigot (Paper)
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 successful 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 server connection is established through a premium connection (paid account authentication)
# * 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, SkinRestorer, ...
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-addresses you specify them here. FastLogin will then use it in rotating
# order --> 5 different IP-addresses 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 to the Mojang API for Name -> UUID requests. Some other plugins as well
# as the head minecraft block make such requests as well. Using this option you can limit the amount requests this
# plugin should make.
#
# If you lower this value, other plugins could still make requests while FastLogin cannot.
# Mojang limits the amount of request to 600 per 10 minutes per IPv4-address.
mojang-request-limit: 600
# This option automatically registers players which are in the FastLogin database, but not in the auth plugin database.
# This can happen if you switch your auth plugin or cleared the database of the auth plugin.
# https://github.com/games647/FastLogin/issues/85
auto-register-unknown: false
# This disables the auto login from fastlogin. So a premium (like a paid account) authentication is requested, but
# the player won't be auto logged into the account.
#
# This can be used as 2Factor authentication for better security of your accounts. A hacker then needs both passwords.
# The password of your minecraft and the password to login in with your auth plugin
autoLogin: true
# Database configuration
# Recommended 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/MariaDB
#driver: com.mysql.jdbc.Driver
#host: localhost
#port: 3306
#database: fastlogin
#username: myUser
#password: myPassword
# It's strongly recommended to enable SSL and setup a SSL certificate if the MySQL server isn't running on the same
# machine
#useSSL: false
# HTTP proxies for connecting to the Mojang servers in order to check if the username of a player is premium.
# This is a workaround to prevent rate-limiting by Mojang. These proxies will only be used once your server hit
# the rate-limit or the custom value above.
# Please make sure you use reliable proxies.
proxies:
# 'IP:Port' or 'Domain:Port'
# - 'xyz.com:1337'
# - 'test.com:5131'

View File

@@ -0,0 +1,98 @@
# FastLogin localization
# Project site: https://www.spigotmc.org/resources/fastlogin.14153
# Source code: https://github.com/games647/FastLogin
#
# You can access the newest locale here:
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/messages.yml
#
# You want to have language template? Visit the Github Wiki here:
# https://github.com/games647/FastLogin/wiki/English
# In order to split a message into separate lines you could just make a new line, but keep the '
# Example:
# bla: '&aFirst line
# Second line
# Third line'
# If you want to disable a message, you can just set it to a empty value.
# In this case no message will be sent
# Example:
# bla: ''
# ========= Shared (BungeeCord and Bukkit) ============
# Switch mode is activated and a new (non-whitelist) cracked player tries to join
switch-kick-message: '&4Only paid minecraft whitelisted accounts are allowed to join this server'
# Player activated premium login in order to skip offline authentication
add-premium: '&2Added to the list of premium players'
# Player activated premium login in order to skip offline authentication
add-premium-other: '&2Player has been added to the premium list'
# Player is already set be a paid account
already-exists: '&4You are already on the premium list'
# Player is already set be a paid account
already-exists-other: '&4Player is already on the premium list'
# Player was changed to be cracked
remove-premium: '&2Removed from the list of premium players'
# Player is already set to be cracked
not-premium: '&4You are not in the premium list'
# Player is already set to be cracked
not-premium-other: '&4Player is not in the premium list'
# Admin wanted to change the premium of a user that isn't known to the plugin
player-unknown: '&4Player not in the database'
# ========= Bukkit/Spigot ================
# 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 occurred'
# The server sends a verify token within the premium authentication request. 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
# account. Only modified clients would do this.
invalid-session: '&4Invalid session'
# The client sent a malicious 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 ================================

64
pom.xml
View File

@@ -8,7 +8,7 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.1</version>
<version>1.10</version>
<inceptionYear>2015</inceptionYear>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>
@@ -19,25 +19,16 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--Possibility to deploy directly to the plugins folder-->
<outputDir>${basedir}/target</outputDir>
<git.commit.id>Unknown</git.commit.id>
</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>
</issueManagement>
<scm>
<url>https://github.com/games647/FastLogin</url>
<connection>scm:git:git://github.com/games647/FastLogin.git</connection>
<developerConnection>scm:git:ssh://git@github.com:games647/FastLogin.git</developerConnection>
</scm>
<build>
<defaultGoal>install</defaultGoal>
<!--Just use the project name to replace an old version of the plugin if the user does only copy-paste-->
@@ -47,22 +38,28 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<version>3.6.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version>
<configuration>
<outputDirectory>${outputDir}</outputDirectory>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
</configuration>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
@@ -72,31 +69,6 @@
<!--Replace variables-->
<filtering>true</filtering>
</resource>
<!--Add the license to jar in order to see it in the final jar-->
<resource>
<!--Parent folder-->
<directory>${basedir}/..</directory>
<includes>
<include>LICENSE</include>
</includes>
</resource>
</resources>
</build>
<dependencies>
<!--Database pooling-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.4.6</version>
</dependency>
<!--Logging framework implements slf4j which is required by hikari-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.20</version>
</dependency>
</dependencies>
</project>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.1</version>
<version>1.10</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -22,7 +22,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<version>3.0.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
@@ -47,6 +47,12 @@
</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>