Compare commits

..

135 Commits

Author SHA1 Message Date
games647
5676e99dec Require player re-join to confirm auto-login request
Fixes #159
Fixes #242
Fixes #256
Fixes #286
2020-03-20 16:06:42 +01:00
games647
65469ed579 Make scheduler platform dependent 2020-03-20 13:44:58 +01:00
games647
8ecb5657d3 Drop nullable annotation usage to compile
Thanks CI
2020-03-14 19:06:05 +01:00
games647
b0cf6e39c7 Drop forced dependency 2020-03-14 19:01:09 +01:00
games647
88a526b5bf Drop support for Minecraft 1.7 2020-03-14 18:51:27 +01:00
games647
57b84509da Limit the total number of running threads
(Related #304)
2020-03-14 18:20:34 +01:00
games647
e0bc7d914c Load the after auth plugins
Fixes #306

There seems to be some capability problems
that it doesn't correctly load ProtocolLib before
this plugin. Furthermore if we use
events for our hooks they print out a warning.
2020-03-10 09:59:31 +01:00
games647
f1933f735a Merge pull request #305 from games647/dependabot/maven/pl.project13.maven-git-commit-id-plugin-4.0.0
Bump git-commit-id-plugin from 3.0.0 to 4.0.0
2020-03-09 19:18:30 +01:00
dependabot-preview[bot]
f69154418e Bump git-commit-id-plugin from 3.0.0 to 4.0.0
Bumps [git-commit-id-plugin](https://github.com/git-commit-id/maven-git-commit-id-plugin) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/git-commit-id/maven-git-commit-id-plugin/releases)
- [Commits](https://github.com/git-commit-id/maven-git-commit-id-plugin/compare/v3.0.0...v4.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-09 16:17:00 +00:00
ㄗㄠˋ ㄑㄧˊ
74b13231f8 bump craftapi to 0.3 (#303) 2020-03-08 12:15:37 +01:00
dependabot-preview[bot]
79627e3b60 Bump slf4j-jdk14 from 1.7.26 to 1.7.30 (#301)
Bumps [slf4j-jdk14](https://github.com/qos-ch/slf4j) from 1.7.26 to 1.7.30.
- [Release notes](https://github.com/qos-ch/slf4j/releases)
- [Commits](https://github.com/qos-ch/slf4j/compare/v_1.7.26...v_1.7.30)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-06 19:28:02 +01:00
dependabot-preview[bot]
1722ab3267 Bump maven-shade-plugin from 3.2.0 to 3.2.2 (#302)
Bumps [maven-shade-plugin](https://github.com/apache/maven-shade-plugin) from 3.2.0 to 3.2.2.
- [Release notes](https://github.com/apache/maven-shade-plugin/releases)
- [Commits](https://github.com/apache/maven-shade-plugin/compare/maven-shade-plugin-3.2.0...maven-shade-plugin-3.2.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-06 19:27:19 +01:00
games647
fdc0485f05 [CI-SKIP] Use HTTPS and newer URLs in Maven 2020-03-03 10:50:48 +01:00
games647
1068ddbadd Enable Maven cache for runner 2020-03-03 10:47:55 +01:00
games647
b0d5bd606c Rework issue templates 2020-03-03 10:47:37 +01:00
games647
1e59101ada Enable MySQL storage caching 2020-03-03 10:46:43 +01:00
games647
46ac3c74e4 Fix Java setup action version 2020-02-17 15:28:58 +01:00
games647
d22af0f42e Fix unique name 2020-02-17 15:24:45 +01:00
games647
2cc078773b Migrate to GitHub actions 2020-02-17 15:18:58 +01:00
games647
c1b8b60bf7 Use HTTPS for code comments too 2020-02-12 10:59:11 +01:00
games647
c54d6eecfc Merge branch 'JLLeitschuh/fix/JLL/use_https_to_resolve_dependencies' 2020-02-12 10:57:30 +01:00
Jonathan Leitschuh
0cf6c4c188 Use HTTPS instead of HTTP to resolve dependencies
This fixes a security vulnerability in this project where the `pom.xml`
files were configuring Maven to resolve dependencies over HTTP instead of
HTTPS.

Signed-off-by: Jonathan Leitschuh <Jonathan.Leitschuh@gmail.com>
2020-02-12 10:55:08 +01:00
games647
282467d21b Set a default kick reason if nothing is specified 2020-01-08 10:44:10 +01:00
games647
e8a5dc7433 Use BungeeCord event methods for kicking the player 2020-01-07 20:35:16 +01:00
games647
4b45932d6a Migrate to asynchronous calls to LoginSecurity
(Fixes #290)
2020-01-06 10:04:44 +01:00
games647
1375cf3997 Fix ProtocolLib version 2019-12-29 13:35:33 +01:00
games647
5a5cf016d9 Remove on blacklist status on join too 2019-12-29 13:33:18 +01:00
games647
2c7e569653 Fix force login events being sync instead of async (Fixes #278) 2019-11-09 13:57:12 +01:00
games647
94c5fe302e Merge pull request #277 from FearGames/master
Fix build and add events
2019-11-02 16:41:43 +01:00
Gabriele C
c80012ddb1 Allow multi-line messages 2019-10-25 11:54:26 +02:00
Gabriele C
17361b1b54 Implement PreLogin and AutoLogin events 2019-10-23 14:45:51 +02:00
Gabriele C
426b458a58 Fix BungeeAuth dependency 2019-10-23 12:50:40 +02:00
games647
62a8b939cc Use 'TEXT' for strings in the configuration file 2019-08-16 14:59:14 +02:00
games647
327c8c4c9d Dump dependencies 2019-07-09 12:56:14 +02:00
games647
fbbe7a735a Initialize the decryption cipher only once
(Related #259)
2019-07-09 12:52:54 +02:00
games647
4110ce2fa2 Fix creating logging session in ProtocolSupport environments
Fixes #251
2019-05-11 21:16:38 +02:00
games647
28743d23bf Remove duplicate repository 2019-05-06 17:39:21 +02:00
games647
145bd95679 Drop usage of deprecated apache lang library 2019-05-06 17:37:13 +02:00
games647
3fe17cf8d9 Merge pull request #250 from lenis0012/patch-2
Update LoginSecurity maven repository
2019-05-06 17:36:15 +02:00
lenis0012
3446d4c443 Update LoginSecurity maven repository 2019-05-06 11:35:39 +02:00
games647
c528433079 Allow username only database logins
Related #247
2019-04-15 20:35:56 +02:00
games647
25858ea11f Enable travis caching 2019-04-14 10:16:53 +02:00
games647
204ffbb2ee Document network requests 2019-04-14 10:16:33 +02:00
games647
9c1ba81cbe Fix running force actions in LoginSecurity thread-safe 2019-04-14 10:16:19 +02:00
games647
15c5857c4f Correctly set the offline UUID in ProtocolSupport (Thanks to @Shevchik) 2018-08-30 12:32:42 +02:00
games647
4b0ad3b186 Update ProtocolSupport hook 2018-08-29 19:49:10 +02:00
games647
101f7207a9 Limit plugin channels to 16 characters for 1.7 support
Fixes #223
2018-08-07 13:41:30 +02:00
games647
64175fe9e8 Update to CraftApi 0.2 2018-07-29 18:47:07 +02:00
games647
f7a10d86eb Don't transform because it's already compatible with 1.13 2018-07-25 16:55:52 +02:00
games647
542aabad73 Lowercase the namespace correctly 2018-07-24 14:33:33 +02:00
games647
260b93a565 Channel names should be lowercase according to the spec
Related #217, #218
2018-07-24 10:17:21 +02:00
games647
6604cca8bd Use Plugin:Subchannel for channel messages
This is required to follow 1.13 spec.
(Related #216, #215)
2018-07-23 14:11:40 +02:00
games647
c172b1ec84 Prevent duplicate message fetching for kick in PLib (Fixes #212) 2018-06-27 18:59:40 +02:00
games647
9b0b8f5fcb Fix repository link of ProtocolLib 2018-06-06 20:17:24 +02:00
games647
20104b2b00 Update ProtocolLib to fix building 2018-06-06 09:33:05 +02:00
games647
fd76d2448e Add advanced options for the connection pool (Fixes #210) 2018-06-04 21:28:19 +02:00
games647
53a1821a9d Fix ProtocolLib repository 2018-06-04 21:27:23 +02:00
games647
1c6f4e82e0 Fix NPE for cracked players on non-bungee environments 2018-05-23 19:14:20 +02:00
games647
084afef899 Update premium UUID on verification (Related #208) 2018-05-04 19:45:43 +02:00
games647
8a9eed3a74 Add /newline variable 2018-05-04 18:54:26 +02:00
games647
1ea6d929b1 Clarify how to configure MariaDB/MySQL correctly 2018-04-26 10:09:40 +02:00
games647
ddc3aa9279 Replace deprecated PropertiesResolveEvent with LoginFinishEvent
affects only ProtocolSupport
2018-04-26 10:08:56 +02:00
games647
2a79a9511b Fix auto register type in BungeeCord not being sent 2018-04-07 16:14:46 +02:00
games647
791df26702 Fix defaults overriding config 2018-04-05 18:42:50 +02:00
games647
cdf1988f2f Fix comping after craftapi update 2018-04-05 17:38:16 +02:00
games647
f476c091bb Fix default message loading, because default values are ignored by .getKeys() 2018-04-05 17:33:32 +02:00
games647
352c72df64 Add note about developments builds 2018-04-02 14:43:43 +02:00
games647
2cd0b194aa We are SNAPSHOT build not a release candidate
Maven versions plugin is great for multi-modules.
Run mvn version:set -DnewVersion=... and that's it.
2018-03-31 10:38:57 +02:00
games647
f2e42019d6 Mention the new FastLogin module names in the setup guide 2018-03-31 10:34:26 +02:00
games647
82ec71e8d0 Update premium status for non-bungeecord setups (Related #200) 2018-03-27 20:43:53 +02:00
games647
6d207d62ba Fix BungeeCord blacklist condition checking 2018-03-25 15:44:51 +02:00
games647
889dab3152 Migrate to PlaceholderExpansion from PlaceholderAPI 2018-03-21 11:02:55 +01:00
games647
71c1f4f12e Remove session in ProtocolSupport directly without expiring 2018-03-18 16:22:03 +01:00
games647
3651c4873c Dump craftapi version 2018-03-17 16:57:05 +01:00
games647
8b613a48cc Forward skin from ProtocolLib verification response 2018-03-16 16:40:12 +01:00
games647
78f8fa1f05 Fix NPE on nullable uuid column 2018-03-16 16:34:31 +01:00
games647
ac5820bb75 Encode enums as integers 2018-03-16 15:15:54 +01:00
games647
c1cb28c996 Add basic API (Fixes #200) 2018-03-16 15:15:03 +01:00
games647
b534765ff8 Always forward premium status to spigot 2018-03-16 15:14:35 +01:00
games647
5bcfdfeb32 Cancel restore session events if it's a premium player (Related #201) 2018-03-16 14:44:35 +01:00
games647
b7c0fd549c Add an explicit warning about the BungeeCord setup guide 2018-03-11 12:27:43 +01:00
games647
61c1364506 Simplify command handling 2018-03-11 11:47:02 +01:00
games647
a29dd849f9 Move shared Mojang client into independant project 2018-03-09 14:39:02 +01:00
games647
3f9eba69ba Generate a public key only for ProtocolLib listener 2018-03-09 13:57:51 +01:00
games647
f250f8071f Optional migration 2018-03-05 21:27:48 +01:00
games647
8272aeac69 Switch to the codemc repo for BungeeCord 2018-03-05 17:37:40 +01:00
games647
4d470be712 Dump AuthMe version 2018-03-05 17:35:33 +01:00
games647
e2c04f2c26 Add isSaved helper 2018-03-02 19:56:17 +01:00
games647
86694982c7 Minor refactoring 2018-03-02 18:29:38 +01:00
games647
04b00f4f22 Add driver available check for more readable error messages 2018-02-24 20:45:22 +01:00
games647
48c2355745 Remove copy-paste misleading package name 2018-02-24 20:45:22 +01:00
games647
cff25c958d Extract BungeeCord message in dedicated classes 2018-02-24 20:45:20 +01:00
games647
06bb4b80dd Add toString methods to all relevant classes 2018-02-24 20:43:18 +01:00
games647
2bdd051a41 Remove universal jar building for a smaller jar footprint and less conflicts with provided dependencies 2018-02-24 20:43:18 +01:00
games647
526a8a9d51 Log invalid proxy id messages 2018-02-24 10:45:52 +01:00
games647
8cbdb66625 Relocate HikariCP and slf4j too to prevent conflicts 2018-02-08 13:24:42 +01:00
games647
e5e815a885 Cancel autologin for AuthMe sessions (Fixes #189, #148, #103) 2018-02-05 15:01:28 +01:00
games647
d0d5bd300b Use static imports for Colectors.* 2018-02-05 12:54:35 +01:00
games647
0c550edb05 Shade the gson dependency to fix compatibility with Minecraft 1.7.10
(Fixes #190)
2018-01-30 13:15:48 +01:00
games647
181ea71222 Readd SSLFactory for rate-limit load balance because direct proxies doesn't work at all 2018-01-28 13:25:10 +01:00
games647
c38692e237 Use ChangeSkins rate-limit message here too 2018-01-28 12:25:25 +01:00
games647
dcef62fa57 Fix FileAlreadyExistsException for sym linked folders 2018-01-27 21:49:32 +01:00
games647
856613a8c7 Update Hikari dependency 2018-01-27 18:47:19 +01:00
games647
3beb8beaeb Migrate tests to assertThat 2018-01-27 18:47:01 +01:00
games647
f3ea7ecbbe Update development builds link 2018-01-27 18:46:43 +01:00
games647
25c725f237 [ci skip] Update LoginSecurity to 2.1.7 to fix compiling 2017-12-01 09:35:37 +01:00
games647
ffe4eb7364 Clarify BungeeCord plugin installation on Spigot 2017-11-25 09:34:38 +01:00
games647
82a258097d Use SecureRandom for passwords 2017-10-30 17:57:01 +01:00
games647
57eff4b3ec Fix NPE for skin apply in ProtocolLib mode (Related #182) 2017-10-15 17:57:24 +02:00
games647
4858049c2a Use direct proxies instead of ssl factories for multiple IP-addresses 2017-10-14 18:25:12 +02:00
games647
bb2cc1b42a Remove local address check (Related #181) 2017-10-12 09:59:16 +02:00
games647
2512c5cf67 Convert local IP addr '-' to . (Related #179) 2017-10-09 10:33:38 +02:00
games647
c7c0782071 Fix address rotating for contacting the Mojang API 2017-10-07 19:48:29 +02:00
games647
df945146b8 Fix debug logging 2017-10-07 19:19:45 +02:00
games647
e32b0232e9 Fix logger init (Fixes #178) 2017-10-04 09:22:21 +02:00
games647
6daa654af8 Fix NPE for Mojang API connector 2017-10-03 15:14:37 +02:00
games647
0f01002564 Optimize issue template 2017-10-03 14:19:34 +02:00
games647
28a20a46fa Fix NPE parsing Mojang uuid 2017-10-03 14:19:02 +02:00
games647
105e00b7e8 Use Instant for timestamps 2017-10-01 17:11:06 +02:00
games647
dce44295d0 Migrate SLF4J logging (Fixes #177) 2017-09-29 16:54:29 +02:00
games647
1f917f3a8d Use Optionals for nullable values 2017-09-24 19:50:42 +02:00
games647
e6c23a4bb5 Use Gson's TypeAdapter for more type safety 2017-09-23 13:56:28 +02:00
games647
66b808c999 Fix compile 2017-09-22 21:41:24 +02:00
games647
2932de5588 Add support for IPv6 proxies 2017-09-22 21:08:24 +02:00
games647
16f7461568 Fix message loading was interacting with the normal config 2017-09-22 20:11:58 +02:00
games647
2f0eb81735 Shade the Bungee-Config implementation because it's platform independent 2017-09-22 20:07:04 +02:00
games647
bb80521ab6 Thermos supports GSON so we could share the json parsing 2017-09-22 18:17:35 +02:00
games647
109508dae6 Clean up using IDE inspections 2017-09-21 15:00:39 +02:00
games647
5bf9b05d30 Fix BungeeAuth Maven repository 2017-09-13 12:34:56 +02:00
games647
7839804a4c Drop support for deprecated AuthMe API 2017-09-12 17:05:18 +02:00
games647
ca58c55eca Remove legacy database migration code 2017-09-08 11:33:14 +02:00
games647
10453fd637 Drop support for RoyalAuth, because it doesn't seem to be supported anymore 2017-09-08 11:30:24 +02:00
games647
d18b734550 Update dependencies 2017-09-08 11:17:05 +02:00
games647
7f51659cc7 Version dump 2017-09-03 20:06:00 +02:00
games647
bb240d3aa0 Refactor encryption implementation
* Simplify utility class and make it more independent from the vendor code
* Create only one cipher object for verification
2017-08-28 12:17:47 +02:00
95 changed files with 2928 additions and 2617 deletions

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,41 @@
---
name: Bug report
about: Something isn't working
title: ''
labels: 'bug'
assignees: ''
---
[//]: # (Lines in this format are considered as comments and will not be displayed.)
[//]: # (Before reporting make sure you're running the **latest build** of the plugin and checked for existing issues!)
### What behaviour is observed:
[//]: # (What happened?)
### What behaviour is expected:
[//]: # (What did you expect?)
### Steps/models to reproduce:
[//]: # (The actions that cause the issue. Please explain it in detail)
### Screenshots (if applicable)
[//]: # (You can drop the files here directly)
### Plugin list:
[//]: # (This can be found by running `/pl`)
### Environment description
[//]: # (Server software with exact version number, Minecraft version, SQLite/MySQL/MariaDB, ...)
### Plugin version or build number (don't write latest):
[//]: # (This can be found by running `/version plugin-name`.)
### Server Log:
[//]: # (No images please - only the textual representation)
[Hastebin](https://hastebin.com/) / [Gist](https://gist.github.com/) link of the error, stacktrace or the complete log (if any)
### Configuration:
[//]: # (No images please - only the textual representation)
[//]: # (remember to delete any sensitive data)
[Hastebin](https://hastebin.com/) / [Gist](https://gist.github.com/) link of your config.yml file

View File

@@ -0,0 +1,22 @@
---
name: Enhancement request
about: New feature or change request
title: ''
labels: 'enhancement'
assignees: ''
---
[//]: # (Lines in this format are considered as comments and will not be displayed.)
### Is your feature request related to a problem? Please describe.
[//]: # (A clear and concise description of what the problem is. Ex. I'm always frustrated when [...])
### Describe the solution you'd like
[//]: # (A clear and concise description of what you want to happen.)
### Describe alternatives you've considered
[//]: # (A clear and concise description of any alternative solutions or features you've considered.)
### Additional context
[//]: # (Add any other context or screenshots about the feature request here.)

10
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Question
about: You want to ask something
title: ''
labels: 'question'
assignees: ''
---

8
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,8 @@
[//]: # (Lines in this format are considered as comments and will not be displayed.)
[//]: # (If your work is in progress, please consider making a draft pull request.)
### Summary of your change
[//]: # (Example: motiviation, enhancement)
### Related issue
[//]: # (Reference it using '#NUMBER'. Ex: Fixes/Related #...)

42
.github/workflows/maven.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
# Human readable name
name: Java CI
# Build on every push and pull request regardless of the branch
# Wiki: https://help.github.com/en/actions/reference/events-that-trigger-workflows
on:
- push
- pull_request
jobs:
# job id
build_and_test:
# Environment image - always newest OS
runs-on: ubuntu-latest
# Run steps
steps:
# Pull changes
- uses: actions/checkout@v2
# Cache artifacts - however this has the downside that we don't get notified of
# artifact resolution failures like invalid repository
# Nevertheless the repositories should be more stable and it makes no sense to pull
# a same version every time
# A dry run would make more sense
- uses: actions/cache@v1
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# Setup Java
- name: Set up JDK
uses: actions/setup-java@v1.3.0
with:
# Use Java 8, because it's minimum required version
java-version: 8
# Build and test (included in package)
- name: Build with Maven and test
# Run non-interactive, package (with compile+test),
# ignore snapshot updates, because they are likely to have breaking changes, enforce checksums to validate posssible errors in depdendencies
run: mvn --batch-mode package --no-snapshot-updates --strict-checksums --file pom.xml

59
.gitignore vendored
View File

@@ -1,38 +1,22 @@
# Eclipse stuff
/.classpath
/.project
/.settings
# Eclipse
.classpath
.project
.settings/
# netbeans
/nbproject
# NetBeans
nbproject/
nb-configuration.xml
/bukkit/nbproject/
# maven
/target
# vim
.*.sw[a-p]
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# various other potential build files
/build
/bin
/dist
/manifest.mf
*.log
# Mac filesystem dust
.DS_Store
# intellij
# IntelliJ
*.iml
*.ipr
*.iws
.idea/
# Maven
target/
pom.xml.versionsBackup
# Gradle
.gradle
@@ -42,8 +26,19 @@ gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Project module targets
bukkit/target
universal/target
bungee/target
core/target
# various other potential build files
build/
bin/
dist/
manifest.mf
*.log
# Vim
.*.sw[a-p]
# virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Mac filesystem dust
.DS_Store

View File

@@ -1,12 +0,0 @@
# Use https://travis-ci.org/ for automatic tests
# speed up testing https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
sudo: false
# This is a java project
language: java
script: mvn compile test
# We run on 8
jdk: [oraclejdk8]

View File

@@ -1,17 +1,39 @@
### 1.11
* Use direct proxies instead of ssl factories for multiple IP-addresses
* Remove local address check for multiple IP-addresses
* Fix parsing of local IP-addresses
* Fix address rotating for contacting the Mojang API
* Optimize issue template
* Use Instant for timestamps
* Migrate SLF4J logging (Fixes #177)
* Use Gson's TypeAdapter for more type safety
* Add support for IPv6 proxies
* Shared configuration implementation for easier maintained code
* Use Gson for json parsing, because it's supported on all platforms and removes code duplicates
* Clean up project code
* Drop support for deprecated AuthMe API
* Remove legacy database migration code
* Drop support for RoyalAuth, because it doesn't seem to be supported anymore
* Clean up client-server encryption -> use only one cipher per connection, simplify code
### 1.10
* Prevent authentication proxies
* Drop database importer
* More logging by default
* 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
* 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
* Finally update the IP column on every login
* No duplicate session login
* Fix timestamp parsing in newer versions of SQLite
* Fix Spigot console command invocation sends result to in game players
@@ -24,7 +46,7 @@
* 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 platform specific auth-plugin. 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
@@ -47,7 +69,7 @@
### 1.7.1
* Fix BungeeCord autoRegister (Fixes #46)
* Fix protocollsupport autoregister
* Fix ProtocolSupport auto-register
### 1.7
@@ -161,7 +183,7 @@
* Removes the need of an Bukkit auth plugin if you use a bungeecord one
* Optimize performance and thread-safety
* Fixed BungeeCord support
* Changed config option autologin to autoregister to clarify the usage
* Changed config option auto-login to auto-register to clarify the usage
### 0.6
@@ -171,19 +193,19 @@
### 0.5
* Added cracked command
* Added autologin - See config
* Added auto-login - See config
* Added config
* Added isRegistered API method
* Added forceRegister API method
* Fixed CrazyLogin player data restore -> Fixes memory leaks with this plugin
* Fixed premium name check to protocolsupport
* Fixed premium name check to ProtocolSupport
* Improved permissions management
### 0.4
* Added forward premium skin
* Added plugin support for protocolsupport
* Added plugin support for ProtocolSupport
### 0.3.2
@@ -209,7 +231,7 @@
### 0.2.3
* Remove useless AuthMe forcelogin code
* Remove useless AuthMe force-login code
* Send a kick message to the client instead of just "Disconnect"
* Reformat source code
* Fix thread safety for fake start packets (Bukkit.getOfflinePlayer doesn't look like to be thread-safe)

View File

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

175
README.md
View File

@@ -3,7 +3,7 @@
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)
@@ -20,163 +20,84 @@ So they don't need to enter passwords. This is also called auto login (auto-logi
* Good performance by using async non blocking operations
* Locale messages
* Import the database from similar plugins
* Free
* Open source
## Development builds
Development builds of this project can be acquired at the provided CI (continuous integration) server. It contains the
latest changes from the Source-Code in preparation for the following release. This means they could contain new
features, bug fixes and other changes since the last release.
Nevertheless builds are only tested using a small set of automated and a few manual tests. Therefore they **could**
contain new bugs and are likely to be less stable than released versions.
https://ci.codemc.org/job/Games647/job/FastLogin/changes
***
### 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
## Commands
### Permissions:
* fastlogin.bukkit.command.premium
* fastlogin.bukkit.command.cracked
* fastlogin.command.premium.other
* fastlogin.command.cracked.other
* fastlogin.command.import
/premium [player] Label the invoker or the argument as paid account
/cracked [player] Label the invoker or the argument as cracked account
### 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+
## Permissions
fastlogin.bukkit.command.premium
fastlogin.bukkit.command.cracked
fastlogin.command.premium.other
fastlogin.command.cracked.other
## Requirements
* Plugin:
* [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or
* [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/)
* [Spigot](https://www.spigotmc.org) 1.8.8+
* Java 8+
* Run Spigot and/or BungeeCord/Waterfall in offline mode (see server.properties or config.yml)
* An auth plugin. Supported plugins
#### Bukkit/Spigot/Paper
### Bukkit/Spigot/Paper
* [AuthMe (both 5.X and 3.X)](https://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
* [AuthMe (5.X)](https://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
* [xAuth](https://dev.bukkit.org/bukkit-plugins/xauth/)
* [LogIt](https://github.com/XziomekX/LogIt)
* [LogIt](https://github.com/games647/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
## Network requests
https://www.spigotmc.org/resources/fastlogin.14153/history
This plugin performs network requests to:
* https://api.mojang.com - retrieving uuid data to decide if we should activate premium login
* https://sessionserver.mojang.com - verify if the player is the owner of that account
***
### How to install
## How to install
#### Bukkit/Spigot/Paper
### Bukkit/Spigot/Paper
1. Download and install ProtocolLib
2. Download and install FastLogin
1. Download and install ProtocolLib/ProtocolSupport
2. Download and install FastLogin (or FastLoginBukkit for newer versions)
3. Set your server in offline mode by setting the value onlinemode in your server.properties to false
#### BungeeCord/Waterfall
### 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
5. Download and Install FastLogin (or FastLoginBungee in newer versions) on BungeeCord AND Spigot
(on the servers where your login plugin is or where player should be able to execute the commands of FastLogin)
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)
5. [Why do players have to invoke a command?](#why-do-players-have-to-invoke-a-command)
6. [What happens if a paid account joins with a used username?](#what-happens-if-a-paid-account-joins-with-a-used-username)
7. [Does the plugin have BungeeCord support?](#does-the-plugin-have-bungeecord-support)
8. [Could premium players have a premium UUID and Skin?](#could-premium-players-have-a-premium-uuid-and-skin)
9. [Is this plugin compatible with Cauldron?](#is-this-plugin-compatible-with-cauldron)
#### How does minecraft logins work?
###### Online Mode
1. Client -> Server: I want to login, here is my username
2. Server -> Client: Okay. I'm in online mode so here is my public key for encryption and my 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. 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
In offline mode step 2-7 is skipped. So a login request is directly followed by 8.
###### More details
http://wiki.vg/Protocol#Login
#### How does this plugin work?
By using ProtocolLib, this plugin works as a proxy between the client and server. This plugin will fake that the server
runs in online mode. It does everything an online mode server would do. This will be for example, generating keys or
checking for valid sessions. Because everything is the same compared to an offline mode login after an encrypted
connection, we will intercept only **login** packets of **premium** players.
1. Player is connecting to the server.
2. Plugin checks if the username we received activated the fast login method (i.e. using command)
3. Run a check if the username is currently used by a paid account.
(We don't know yet if the client connecting is premium)
4. Request an Mojang Session Server authentication
5. On response check if all data is correct
6. Encrypt the connection
7. On success intercept all related login packets and fake a new login packet as a normal offline login
#### Why does the plugin require offline mode?
1. As you can see in the question "how does minecraft login works", offline mode is equivalent to online mode except of
the encryption and session checks on login. So we can intercept and cancel the first packets for premium players and
enable an encrypted connection. Then we send a new fake packet in order to pretend that this a new login request from
a offline mode player. The server will handle the rest.
2. Some plugins check if the server is in online mode. If so, they could process the real offline (cracked) accounts
incorrectly. For example, a plugin tries to fetch the UUID from Mojang, but the name of the player is not associated to
a paid account.
3. Servers, who allow cracked players and just speed up logins for premium players, are **already** in offline mode.
#### Can cracked player join with premium usernames?
Yes, indeed. Therefore the command for toggling the fast login method exists.
#### Why do players have to invoke a command?
1. It's a secure way to make sure a person with a paid account cannot steal the account
of a cracked player that has the same username. The player have to proof first that it's his own account.
2. We only receive the username from the player on login. We could check if that username is associated
to a paid account but if we request a online mode login from a cracked player (who uses a username from
a paid account), the player will disconnect with the reason "bad login" or "Invalid session". There is no way to change
that message on the server side (without client modifications), because it's a connection between the Client and the
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?
The player on the server have to activate the feature of this plugin by command. If a person buys the username
of his own account, it's still secured. A normal offline mode login makes sure he's the owner of the server account
and Mojang account. Then the command can be executed. So someone different cannot steal the account of cracked player
by buying the username.
#### Does the plugin have BungeeCord support?
Yes it has. See the how to install above.
#### Could premium players have a premium UUID and Skin?
Since 0.7 both features are implemented. You can check the config.yml in order to activate it.
#### Is this plugin compatible with Cauldron?
It's not tested yet, but all needed methods also exists in Cauldron so it could work together.
***
### Useful Links:
* [Login Protocol](http://wiki.vg/Protocol#Login)
* [Protocol Encryption](http://wiki.vg/Protocol_Encryption)
8. You should *always* firewall your Spigot server that it's only accessible through BungeeCord
* https://www.spigotmc.org/wiki/bungeecord-installation/#post-installation
* BungeeCord doesn't support SQLite per default, so you should change the configuration to MySQL or MariaDB

View File

@@ -1,11 +1,11 @@
<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">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>
<version>1.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -15,63 +15,104 @@
<name>FastLoginBukkit</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
<relocations>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>fastlogin.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.slf4j</pattern>
<shadedPattern>fastlogin.slf4j</shadedPattern>
</relocation>
<relocation>
<pattern>net.md_5.bungee.config</pattern>
<shadedPattern>fastlogin.config</shadedPattern>
</relocation>
<relocation>
<pattern>com.google.gson</pattern>
<shadedPattern>fastlogin.gson</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<!--Bukkit-Server-API -->
<!-- Bukkit-Server-API -->
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
<!-- Disable snapshot release policy to speed up, when finding a artifact -->
<releases>
<enabled>false</enabled>
</releases>
</repository>
<!--LoginSecurity-->
<repository>
<id>lenis0012-repo</id>
<url>http://ci.lenis0012.com/plugin/repository/everything/</url>
</repository>
<!--ProtocolLib-->
<!-- ProtocolLib -->
<repository>
<id>dmulloy2-repo</id>
<url>http://repo.dmulloy2.net/content/groups/public/</url>
<url>https://repo.dmulloy2.net/nexus/repository/public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!--AuthMe Reloaded-->
<!-- AuthMe Reloaded, xAuth and LoginSecurity -->
<repository>
<id>xephi-repo</id>
<url>https://ci.xephi.fr/plugin/repository/everything/</url>
<id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-public/</url>
</repository>
<!--xAuth-->
<repository>
<id>luricos.de-repo</id>
<url>http://repo.luricos.de/bukkit-plugins/</url>
</repository>
<!--Github automatic maven builds-->
<!-- GitHub automatic maven builds -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!--PlaceholderAPI -->
<!-- PlaceholderAPI -->
<repository>
<id>placeholderapi</id>
<url>http://repo.extendedclip.com/content/repositories/placeholderapi/</url>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!--Common plugin component-->
<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.12-pre2-SNAPSHOT</version>
<version>1.12.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -79,20 +120,24 @@
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.3.0</version>
<version>4.5.0</version>
<scope>provided</scope>
</dependency>
<!--Changing onlinemode on login process-->
<dependency>
<groupId>com.github.ProtocolSupport</groupId>
<artifactId>ProtocolSupport</artifactId>
<!--4.25.dev-->
<version>a4f060dc46</version>
<!--4.29.dev after commit about API improvements-->
<version>3a80c661fe</version>
<scope>provided</scope>
</dependency>
<!--Provide premium placeholders-->
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.8.1</version>
<version>2.10.4</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
@@ -107,7 +152,8 @@
<dependency>
<groupId>fr.xephi</groupId>
<artifactId>authme</artifactId>
<version>5.3.1</version>
<version>5.4.0</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
<exclusion>
@@ -120,7 +166,8 @@
<dependency>
<groupId>com.lenis0012.bukkit</groupId>
<artifactId>loginsecurity</artifactId>
<version>2.1.6</version>
<version>3.0.1</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
<exclusion>
@@ -134,20 +181,7 @@
<groupId>com.github.games647</groupId>
<artifactId>LogIt</artifactId>
<version>9e3581db27</version>
<optional>true</optional>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.RoyalDev</groupId>
<artifactId>RoyalAuth</artifactId>
<version>-e21354a9b7-1</version>
<optional>true</optional>
<exclusions>
<exclusion>
@@ -161,6 +195,7 @@
<groupId>de.luricos.bukkit</groupId>
<artifactId>xAuth</artifactId>
<version>2.6</version>
<scope>provided</scope>
<optional>true</optional>
<!--These artifacts produce conflicts on downloading-->
<exclusions>

View File

@@ -1,9 +1,10 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import org.apache.commons.lang.ArrayUtils;
import java.util.Optional;
/**
* Represents a client connecting to the server.
@@ -12,43 +13,44 @@ import org.apache.commons.lang.ArrayUtils;
*/
public class BukkitLoginSession extends LoginSession {
private static final byte[] EMPTY_ARRAY = {};
private final String serverId;
private final byte[] verifyToken;
private boolean verified;
private String encodedSkinData;
private String skinSignature;
private SkinProperty skinProperty;
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
, PlayerProfile profile) {
, StoredProfile profile) {
super(username, registered, profile);
this.serverId = serverId;
this.verifyToken = ArrayUtils.clone(verifyToken);
this.verifyToken = verifyToken.clone();
}
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, StoredProfile profile) {
// confirmation login
super(username, profile);
this.serverId = serverId;
this.verifyToken = verifyToken.clone();
}
//available for BungeeCord
public BukkitLoginSession(String username, boolean registered) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, registered, null);
this(username, "", EMPTY_ARRAY, registered, null);
}
//cracked player
public BukkitLoginSession(String username, PlayerProfile profile) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
public BukkitLoginSession(String username, StoredProfile profile) {
this(username, "", EMPTY_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;
//ProtocolSupport
public BukkitLoginSession(String username, boolean registered, StoredProfile profile) {
this(username, "", EMPTY_ARRAY, registered, profile);
}
/**
@@ -58,27 +60,23 @@ public class BukkitLoginSession extends LoginSession {
*
* @return the verify token from the server
*/
public byte[] getVerifyToken() {
return ArrayUtils.clone(verifyToken);
public synchronized byte[] getVerifyToken() {
return verifyToken.clone();
}
public synchronized String getEncodedSkinData() {
return encodedSkinData;
}
public synchronized String getSkinSignature() {
return skinSignature;
/**
* @return premium skin if available
*/
public synchronized Optional<SkinProperty> getSkin() {
return Optional.ofNullable(skinProperty);
}
/**
* Sets the premium skin property which was retrieved by the session server
*
* @param encodedData
* @param skinSignature
* @param skinProperty premium skin
*/
public synchronized void setSkin(String encodedData, String skinSignature) {
this.encodedSkinData = encodedData;
this.skinSignature = skinSignature;
public synchronized void setSkinProperty(SkinProperty skinProperty) {
this.skinProperty = skinProperty;
}
/**

View File

@@ -0,0 +1,27 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.AsyncScheduler;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.slf4j.Logger;
public class BukkitScheduler extends AsyncScheduler {
private final Plugin plugin;
private final Executor syncExecutor;
public BukkitScheduler(Plugin plugin, Logger logger, ThreadFactory threadFactory) {
super(logger, threadFactory);
this.plugin = plugin;
syncExecutor = r -> Bukkit.getScheduler().runTask(plugin, r);
}
public Executor getSyncExecutor() {
return syncExecutor;
}
}

View File

@@ -1,35 +1,43 @@
package com.github.games647.fastlogin.bukkit;
import com.google.common.base.Charsets;
import java.security.InvalidKeyException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.stream.Stream;
import java.security.PublicKey;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Encryption and decryption minecraft util for connection between servers
* and paid minecraft account clients.
* and paid Minecraft account clients.
*
* Source: https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java
*
* Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8
* @see net.minecraft.server.MinecraftEncryption
*/
public class EncryptionUtil {
public static final int VERIFY_TOKEN_LENGTH = 4;
public static final String KEY_PAIR_ALGORITHM = "RSA";
private EncryptionUtil() {
//utility
}
/**
* Generate a RSA key pair
*
* @return The RSA key pair.
*/
public static KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM);
keyPairGenerator.initialize(1_024);
return keyPairGenerator.generateKeyPair();
@@ -39,82 +47,70 @@ public class EncryptionUtil {
}
}
public static byte[] getServerIdHash(String serverId, Key publicKey, Key secretKey) {
return digestOperation("SHA-1"
, serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded());
/**
* Generate a random token. This is used to verify that we are communicating with the same player
* in a login session.
*
* @param random random generator
* @return an error with 4 bytes long
*/
public static byte[] generateVerifyToken(Random random) {
byte[] token = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(token);
return token;
}
private static byte[] digestOperation(String algorithm, byte[]... content) {
/**
* Generate the server id based on client and server data.
*
* @param sessionId session for the current login attempt
* @param sharedSecret shared secret between the client and the server
* @param publicKey public key of the server
* @return the server id formatted as a hexadecimal string.
*/
public static String getServerIdHashString(String sessionId, Key sharedSecret, PublicKey publicKey) {
try {
MessageDigest messagedigest = MessageDigest.getInstance(algorithm);
Stream.of(content).forEach(messagedigest::update);
return messagedigest.digest();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
nosuchalgorithmexception.printStackTrace();
return null;
}
}
// public static PublicKey decodePublicKey(byte[] encodedKey) {
// try {
// KeyFactory keyfactory = KeyFactory.getInstance("RSA");
//
// X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
// return keyfactory.generatePublic(x509encodedkeyspec);
// } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
// //ignore
// }
//
// System.err.println("Public key reconstitute failed!");
// return null;
// }
public static SecretKey decryptSharedKey(Key privateKey, byte[] encryptedSharedKey) {
return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES");
}
public static byte[] decryptData(Key key, byte[] data) {
return cipherOperation(Cipher.DECRYPT_MODE, key, data);
}
private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
try {
return createCipherInstance(operationMode, key.getAlgorithm(), key).doFinal(data);
} catch (IllegalBlockSizeException | BadPaddingException ex) {
ex.printStackTrace();
byte[] serverHash = getServerIdHash(sessionId, sharedSecret, publicKey);
return (new BigInteger(serverHash)).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
System.err.println("Cipher data failed!");
return null;
return "";
}
private static Cipher createCipherInstance(int operationMode, String cipherName, Key key) {
try {
Cipher cipher = Cipher.getInstance(cipherName);
cipher.init(operationMode, key);
return cipher;
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException ex) {
ex.printStackTrace();
}
System.err.println("Cipher creation failed!");
return null;
/**
* Decrypts the content and extracts the key spec.
*
* @param cipher decryption cipher initialized with the private key
* @param sharedKey the encrypted shared key
* @return shared secret key
* @throws GeneralSecurityException if it fails to decrypt the data
*/
public static SecretKey decryptSharedKey(Cipher cipher, byte[] sharedKey) throws GeneralSecurityException {
return new SecretKeySpec(decrypt(cipher, sharedKey), "AES");
}
//
// public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
// try {
// Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
//
// cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
// return cipher;
// } catch (GeneralSecurityException generalsecurityexception) {
// throw new RuntimeException(generalsecurityexception);
// }
// }
private EncryptionUtil() {
//utility
/**
* Decrypted the given data using the cipher.
*
* @param cipher decryption cypher initialized with the private key
* @param data the encrypted data
* @return clear text data
* @throws GeneralSecurityException if it fails to decrypt the data
*/
public static byte[] decrypt(Cipher cipher, byte[] data) throws GeneralSecurityException {
return cipher.doFinal(data);
}
private static byte[] getServerIdHash(String sessionId, Key sharedSecret, PublicKey publicKey)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sessionId.getBytes(StandardCharsets.ISO_8859_1));
digest.update(sharedSecret.getEncoded());
digest.update(publicKey.getEncoded());
return digest.digest();
}
}

View File

@@ -1,108 +1,118 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.BukkitJoinListener;
import com.github.games647.fastlogin.bukkit.listener.BungeeCordListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.LoginSkinApplyListener;
import com.github.games647.fastlogin.bukkit.command.CrackedCommand;
import com.github.games647.fastlogin.bukkit.command.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.BungeeListener;
import com.github.games647.fastlogin.bukkit.listener.ConnectionListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ProtocolLibListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SkinApplyListener;
import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
import com.github.games647.fastlogin.bukkit.task.DelayedAuthHook;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.PremiumStatus;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
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.Reader;
import java.security.KeyPair;
import java.util.List;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.messaging.PluginMessageRecipient;
import org.slf4j.Logger;
import static com.github.games647.fastlogin.core.message.ChangePremiumMessage.CHANGE_CHANNEL;
import static com.github.games647.fastlogin.core.message.SuccessMessage.SUCCESS_CHANNEL;
/**
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
*/
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 FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
private boolean serverStarted;
//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);
private final ConcurrentMap<String, BukkitLoginSession> loginSession = CommonUtil.buildCache(1, -1);
private final Map<UUID, PremiumStatus> premiumPlayers = new ConcurrentHashMap<>();
private final Logger logger;
private boolean serverStarted;
private boolean bungeeCord;
private final BukkitScheduler scheduler;
private FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
public FastLoginBukkit() {
this.logger = CommonUtil.createLoggerFromJDK(getLogger());
this.scheduler = new BukkitScheduler(this, logger, getThreadFactory());
}
@Override
public void onEnable() {
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);
logger.warn("Cannot check bungeecord support. You use a non-Spigot build", ex);
}
if (getServer().getOnlineMode()) {
//we need to require offline to prevent a loginSession request for a offline player
getLogger().severe("Server have to be in offline mode");
logger.error("Server have to be in offline mode");
setEnabled(false);
return;
}
PluginManager pluginManager = getServer().getPluginManager();
if (bungeeCord) {
setServerStarted();
//check for incoming messages from the bungeecord version of this plugin
getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this));
getServer().getMessenger().registerOutgoingPluginChannel(this, getName());
//register listeners on success
// check for incoming messages from the bungeecord version of this plugin
String forceChannel = NamespaceKey.getCombined(getName(), LoginActionMessage.FORCE_CHANNEL);
getServer().getMessenger().registerIncomingPluginChannel(this, forceChannel, new BungeeListener(this));
// outgoing
String successChannel = new NamespaceKey(getName(), SUCCESS_CHANNEL).getCombinedName();
String changeChannel = new NamespaceKey(getName(), CHANGE_CHANNEL).getCombinedName();
getServer().getMessenger().registerOutgoingPluginChannel(this, successChannel);
getServer().getMessenger().registerOutgoingPluginChannel(this, changeChannel);
} else {
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
if (pluginManager.isPluginEnabled("ProtocolSupport")) {
pluginManager.registerEvents(new ProtocolSupportListener(this), this);
} else if (pluginManager.isPluginEnabled("ProtocolLib")) {
ProtocolLibListener.register(this);
getServer().getPluginManager().registerEvents(new LoginSkinApplyListener(this), this);
pluginManager.registerEvents(new SkinApplyListener(this), this);
} else {
getLogger().warning("Either ProtocolLib or ProtocolSupport have to be installed "
+ "if you don't use BungeeCord");
logger.warn("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);
pluginManager.registerEvents(new ConnectionListener(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")) {
if (pluginManager.isPluginEnabled("PlaceholderAPI")) {
//prevents NoClassDef errors if it's not available
PremiumPlaceholder.register(this);
}
@@ -111,6 +121,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
@Override
public void onDisable() {
loginSession.clear();
premiumPlayers.clear();
if (core != null) {
core.close();
@@ -124,20 +135,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
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)
@@ -148,24 +145,32 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
return loginSession;
}
/**
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between the client and server
*
* @return the server KeyPair
*/
public KeyPair getServerKey() {
return keyPair;
public Map<UUID, PremiumStatus> getPremiumPlayers() {
return premiumPlayers;
}
public boolean isBungeeCord() {
public boolean isBungeeEnabled() {
return bungeeCord;
}
/**
* Fetches the premium status of an online player.
*
* @param onlinePlayer
* @return the online status or unknown if an error happened, the player isn't online or BungeeCord doesn't send
* us the status message yet (This means you cannot check the login status on the PlayerJoinEvent).
* @deprecated this method could be removed in future versions and exists only as a temporarily solution
*/
@Deprecated
public PremiumStatus getStatus(UUID onlinePlayer) {
return premiumPlayers.getOrDefault(onlinePlayer, PremiumStatus.UNKNOWN);
}
/**
* Wait before the server is fully started. This is workaround, because connections right on startup are not
* injected by ProtocolLib
*
* @return
* @return true if ProtocolLib can now intercept packets
*/
public boolean isServerFullyStarted() {
return serverStarted;
@@ -177,44 +182,33 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
}
}
private void notifyBungeeCord(PluginMessageRecipient sender, String target, boolean activate, boolean isPlayer) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
if (activate) {
dataOutput.writeUTF("ON");
} else {
dataOutput.writeUTF("OFF");
}
public void sendPluginMessage(PluginMessageRecipient player, ChannelMessage message) {
if (player != null) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
message.writeTo(dataOutput);
dataOutput.writeUTF(target);
dataOutput.writeBoolean(isPlayer);
sender.sendPluginMessage(this, getName(), dataOutput.toByteArray());
NamespaceKey channel = new NamespaceKey(getName(), message.getChannelName());
player.sendPluginMessage(this, channel.getCombinedName(), dataOutput.toByteArray());
}
}
@Override
public Map<String, Object> loadYamlFile(Reader reader) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(reader);
return config.getValues(true);
public Path getPluginFolder() {
return getDataFolder().toPath();
}
@Override
public Logger getLog() {
return logger;
}
@Override
public BukkitScheduler getScheduler() {
return scheduler;
}
@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,91 +0,0 @@
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,14 +1,15 @@
package com.github.games647.fastlogin.bukkit;
import java.util.List;
import java.util.stream.Collectors;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.PlaceholderHook;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
public class PremiumPlaceholder extends PlaceholderHook {
public class PremiumPlaceholder extends PlaceholderExpansion {
private static final String PLACEHOLDER_VARIABLE = "fastlogin_status";
private final FastLoginBukkit plugin;
@@ -16,25 +17,37 @@ public class PremiumPlaceholder extends PlaceholderHook {
this.plugin = plugin;
}
public static void register(FastLoginBukkit plugin) {
PremiumPlaceholder placeholderHook = new PremiumPlaceholder(plugin);
PlaceholderAPI.registerPlaceholderHook(PLACEHOLDER_VARIABLE, placeholderHook);
}
@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";
}
if (player != null && PLACEHOLDER_VARIABLE.equals(variable)) {
return plugin.getStatus(player.getUniqueId()).name();
}
return null;
return "";
}
public static boolean register(FastLoginBukkit plugin) {
return PlaceholderAPI.registerPlaceholderHook(plugin, new PremiumPlaceholder(plugin));
@Override
public String getIdentifier() {
return PLACEHOLDER_VARIABLE;
}
@Override
public String getPlugin() {
return plugin.getName();
}
@Override
public String getAuthor() {
return plugin.getDescription().getAuthors().stream().collect(Collectors.joining(", "));
}
@Override
public String getVersion() {
return plugin.getName();
}
}

View File

@@ -0,0 +1,87 @@
package com.github.games647.fastlogin.bukkit.command;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.StoredProfile;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
public class CrackedCommand extends ToggleCommand {
public CrackedCommand(FastLoginBukkit plugin) {
super(plugin);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
onCrackedSelf(sender, command, args);
} else {
onCrackedOther(sender, command, args);
}
return true;
}
private void onCrackedSelf(CommandSender sender, Command cmd, String[] args) {
if (isConsole(sender)) {
return;
}
if (forwardCrackedCommand(sender, sender.getName())) {
return;
}
if (plugin.isBungeeEnabled()) {
sendBungeeActivateMessage(sender, sender.getName(), false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async if
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
profile.setId(null);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
} else {
plugin.getCore().sendLocaleMessage("not-premium", sender);
}
}
}
private void onCrackedOther(CommandSender sender, Command command, String[] args) {
if (!hasOtherPermission(sender, command)) {
return;
}
if (forwardCrackedCommand(sender, args[0])) {
return;
}
//todo: load async
StoredProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage("Error occurred");
return;
}
//existing player is already cracked
if (profile.isSaved() && !profile.isPremium()) {
plugin.getCore().sendLocaleMessage("not-premium-other", sender);
} else {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
}
}
private boolean forwardCrackedCommand(CommandSender sender, String target) {
return forwardBungeeCommand(sender, target, false);
}
}

View File

@@ -0,0 +1,110 @@
package com.github.games647.fastlogin.bukkit.command;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Let users activate fast login by command. This only be accessible if
* the user has access to it's account. So we can make sure that not another
* person with a paid account and the same username can steal his account.
*/
public class PremiumCommand extends ToggleCommand {
public PremiumCommand(FastLoginBukkit plugin) {
super(plugin);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
onPremiumSelf(sender, command, args);
} else {
onPremiumOther(sender, command, args);
}
return true;
}
private void onPremiumSelf(CommandSender sender, Command cmd, String[] args) {
if (isConsole(sender)) {
return;
}
Player player = (Player) sender;
String playerName = sender.getName();
if (forwardPremiumCommand(sender, playerName)) {
return;
}
// non-bungee mode
if (plugin.getConfig().getBoolean("premium-confirm")) {
ConfirmationState state = plugin.getCore().getPendingConfirms().get(playerName);
if (state == null) {
// no pending confirmation
plugin.getCore().getPendingConfirms().put(playerName, ConfirmationState.REQUIRE_RELOGIN);
player.kickPlayer(plugin.getCore().getMessage("premium-confirm"));
} else if (state == ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN) {
// player logged in successful using premium authentication
activate(sender, playerName);
}
} else {
activate(sender, playerName);
}
}
private void activate(CommandSender sender, String playerName) {
plugin.getCore().getPendingConfirms().remove(playerName);
//todo: load async
StoredProfile profile = plugin.getCore().getStorage().loadProfile(playerName);
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("already-exists", sender);
} else {
//todo: resolve uuid
profile.setPremium(true);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
}
private void onPremiumOther(CommandSender sender, Command command, String[] args) {
if (!hasOtherPermission(sender, command)) {
return;
}
if (forwardPremiumCommand(sender, args[0])) {
return;
}
//todo: load async
StoredProfile 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);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
plugin.getCore().sendLocaleMessage("add-premium-other", sender);
}
}
private boolean forwardPremiumCommand(CommandSender sender, String target) {
return forwardBungeeCommand(sender, target, true);
}
}

View File

@@ -0,0 +1,69 @@
package com.github.games647.fastlogin.bukkit.command;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.message.ChangePremiumMessage;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import java.util.Optional;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageRecipient;
public abstract class ToggleCommand implements CommandExecutor {
protected final FastLoginBukkit plugin;
public ToggleCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
}
protected boolean hasOtherPermission(CommandSender sender, Command cmd) {
if (!sender.hasPermission(cmd.getPermission() + ".other")) {
plugin.getCore().sendLocaleMessage("no-permission", sender);
return false;
}
return true;
}
protected boolean forwardBungeeCommand(CommandSender sender, String target, boolean activate) {
if (plugin.isBungeeEnabled()) {
sendBungeeActivateMessage(sender, target, activate);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
return true;
}
return false;
}
protected boolean isConsole(CommandSender sender) {
if (sender instanceof Player) {
return false;
}
//console or command block
sender.sendMessage(plugin.getCore().getMessage("no-console"));
return true;
}
protected void sendBungeeActivateMessage(CommandSender invoker, String target, boolean activate) {
if (invoker instanceof PluginMessageRecipient) {
ChannelMessage message = new ChangePremiumMessage(target, activate, true);
plugin.sendPluginMessage((PluginMessageRecipient) invoker, message);
} else {
Optional<? extends Player> optPlayer = Bukkit.getServer().getOnlinePlayers().stream().findFirst();
if (!optPlayer.isPresent()) {
plugin.getLog().info("No player online to send a plugin message to the proxy");
return;
}
Player sender = optPlayer.get();
ChannelMessage message = new ChangePremiumMessage(target, activate, false);
plugin.sendPluginMessage(sender, message);
}
}
}

View File

@@ -1,86 +0,0 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class CrackedCommand implements CommandExecutor {
private final FastLoginBukkit plugin;
public CrackedCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
if (!(sender instanceof Player)) {
//console or command block
sender.sendMessage(plugin.getCore().getMessage("no-console"));
return true;
}
if (plugin.isBungeeCord()) {
plugin.sendBungeeActivateMessage(sender, sender.getName(), false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async if
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
profile.setUuid(null);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
} else {
plugin.getCore().sendLocaleMessage("not-premium", sender);
}
}
return true;
} else {
onCrackedOther(sender, command, args);
}
return true;
}
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()) {
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;
}
//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,102 +0,0 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Let users activate fast login by command. This only be accessible if
* the user has access to it's account. So we can make sure that not another
* person with a paid account and the same username can steal his account.
*/
public class PremiumCommand implements CommandExecutor {
private final FastLoginBukkit plugin;
public PremiumCommand(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
if (!(sender instanceof Player)) {
//console or command block
plugin.getCore().sendLocaleMessage("no-console", sender);
return true;
}
if (plugin.isBungeeCord()) {
plugin.sendBungeeActivateMessage(sender, sender.getName(), true);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
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()) {
plugin.getCore().sendLocaleMessage("already-exists", sender);
} else {
//todo: resolve uuid
profile.setPremium(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
}
return true;
} else {
onPremiumOther(sender, command, args);
}
return true;
}
private void onPremiumOther(CommandSender sender, Command command, String[] args) {
if (!sender.hasPermission(command.getPermission() + ".other")) {
plugin.getCore().sendLocaleMessage("no-permission", sender);
return ;
}
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

@@ -0,0 +1,52 @@
package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class BukkitFastLoginAutoLoginEvent extends Event implements FastLoginAutoLoginEvent, Cancellable {
private static final HandlerList handlers = new HandlerList();
private final LoginSession session;
private final StoredProfile profile;
private boolean cancelled;
public BukkitFastLoginAutoLoginEvent(LoginSession session, StoredProfile profile) {
super(true);
this.session = session;
this.profile = profile;
}
@Override
public LoginSession getSession() {
return session;
}
@Override
public StoredProfile getProfile() {
return profile;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@@ -0,0 +1,47 @@
package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSource;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class BukkitFastLoginPreLoginEvent extends Event implements FastLoginPreLoginEvent {
private static final HandlerList handlers = new HandlerList();
private final String username;
private final LoginSource source;
private final StoredProfile profile;
public BukkitFastLoginPreLoginEvent(String username, LoginSource source, StoredProfile profile) {
super(true);
this.username = username;
this.source = source;
this.profile = profile;
}
@Override
public String getUsername() {
return username;
}
@Override
public LoginSource getSource() {
return source;
}
@Override
public StoredProfile getProfile() {
return profile;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@@ -0,0 +1,65 @@
package com.github.games647.fastlogin.bukkit.hook;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import fr.xephi.authme.api.v3.AuthMeApi;
import fr.xephi.authme.events.RestoreSessionEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
/**
* GitHub: https://github.com/Xephi/AuthMeReloaded/
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/authme-reloaded/
* <p>
* Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
*/
public class AuthMeHook implements AuthPlugin<Player>, Listener {
private final FastLoginBukkit plugin;
public AuthMeHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onSessionRestore(RestoreSessionEvent restoreSessionEvent) {
Player player = restoreSessionEvent.getPlayer();
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
BukkitLoginSession session = plugin.getLoginSessions().get(id);
if (session != null && session.isVerified()) {
restoreSessionEvent.setCancelled(true);
}
}
@Override
public boolean forceLogin(Player player) {
if (AuthMeApi.getInstance().isAuthenticated(player)) {
return false;
}
//skips registration and login
AuthMeApi.getInstance().forceLogin(player);
return true;
}
@Override
public boolean isRegistered(String playerName) {
return AuthMeApi.getInstance().isRegistered(playerName);
}
@Override
public boolean forceRegister(Player player, String password) {
//this automatically login the player too
AuthMeApi.getInstance().forceRegister(player, password);
return true;
}
}

View File

@@ -1,5 +1,7 @@
package com.github.games647.fastlogin.bukkit.hooks;
package com.github.games647.fastlogin.bukkit.hook;
import com.comphenix.protocol.reflect.FieldUtils;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import de.st_ddt.crazylogin.CrazyLogin;
@@ -8,30 +10,38 @@ import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
import de.st_ddt.crazylogin.listener.PlayerListener;
import de.st_ddt.crazylogin.metadata.Authenticated;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.apache.commons.lang.reflect.FieldUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/ST-DDT/CrazyLogin
*
* GitHub: https://github.com/ST-DDT/CrazyLogin
* <p>
* Project page:
*
* Bukkit: http://dev.bukkit.org/server-mods/crazylogin/
* <p>
* Bukkit: https://dev.bukkit.org/server-mods/crazylogin/
*/
public class CrazyLoginHook implements AuthPlugin<Player> {
private final CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
private final PlayerListener playerListener = getListener();
private final FastLoginBukkit plugin;
private final CrazyLogin crazyLoginPlugin;
private final PlayerListener playerListener;
public CrazyLoginHook(FastLoginBukkit plugin) {
this.plugin = plugin;
crazyLoginPlugin = CrazyLogin.getPlugin();
playerListener = getListener();
}
@Override
public boolean forceLogin(Player player) {
//not thread-safe operation
Future<LoginPlayerData> future = Bukkit.getScheduler().callSyncMethod(crazyLoginPlugin, () -> {
Future<Optional<LoginPlayerData>> future = Bukkit.getScheduler().callSyncMethod(plugin, () -> {
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player);
if (playerData != null) {
//mark the account as logged in
@@ -56,21 +66,21 @@ public class CrazyLoginHook implements AuthPlugin<Player> {
playerData.addIP(ip);
player.setMetadata("Authenticated", new Authenticated(crazyLoginPlugin, player));
crazyLoginPlugin.unregisterDynamicHooks();
return playerData;
return Optional.of(playerData);
}
return null;
return Optional.empty();
});
try {
LoginPlayerData result = future.get();
if (result != null && result.isLoggedIn()) {
Optional<LoginPlayerData> result = future.get().filter(LoginPlayerData::isLoggedIn);
if (result.isPresent()) {
//SQL-Queries should run async
crazyLoginPlugin.getCrazyDatabase().saveWithoutPassword(result);
crazyLoginPlugin.getCrazyDatabase().saveWithoutPassword(result.get());
return true;
}
} catch (InterruptedException | ExecutionException ex) {
crazyLoginPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
plugin.getLog().error("Failed to forceLogin player: {}", player, ex);
return false;
}
@@ -78,7 +88,7 @@ public class CrazyLoginHook implements AuthPlugin<Player> {
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String playerName) {
return crazyLoginPlugin.getPlayerData(playerName) != null;
}
@@ -92,9 +102,7 @@ public class CrazyLoginHook implements AuthPlugin<Player> {
//create a fake account - this will be saved to the database with the password=FAILEDLOADING
//user cannot login with that password unless the admin uses plain text
//this automatically marks the player as logged in
playerData = new LoginPlayerData(player);
crazyDatabase.save(playerData);
crazyDatabase.save(new LoginPlayerData(player));
return forceLogin(player);
}
@@ -106,7 +114,7 @@ public class CrazyLoginHook implements AuthPlugin<Player> {
try {
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);
plugin.getLog().error("Failed to get the listener instance for auto login", ex);
listener = null;
}

View File

@@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit.hooks;
package com.github.games647.fastlogin.bukkit.hook;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
@@ -6,13 +6,18 @@ 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 java.time.Instant;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/XziomekX/LogIt
* GitHub: https://github.com/XziomekX/LogIt
* <p>
* Project page:
*
* <p>
* Bukkit: Unknown
* <p>
* Spigot: Unknown
*/
public class LogItHook implements AuthPlugin<Player> {
@@ -20,15 +25,12 @@ 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;
return sessionManager.isSessionAlive(player)
|| sessionManager.startSession(player) == CancelledState.NOT_CANCELLED;
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String playerName) {
return LogItCore.getInstance().getAccountManager().isRegistered(playerName);
}
@@ -36,8 +38,10 @@ public class LogItHook implements AuthPlugin<Player> {
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);
Instant now = Instant.now();
account.setLastActiveDate(now.getEpochSecond());
account.setRegistrationDate(now.getEpochSecond());
return LogItCore.getInstance().getAccountManager().insertAccount(account) == CancelledState.NOT_CANCELLED;
}
}

View File

@@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit.hooks;
package com.github.games647.fastlogin.bukkit.hook;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
@@ -8,31 +8,34 @@ import com.lenis0012.bukkit.loginsecurity.session.PlayerSession;
import com.lenis0012.bukkit.loginsecurity.session.action.LoginAction;
import com.lenis0012.bukkit.loginsecurity.session.action.RegisterAction;
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/
* GitHub: https://github.com/lenis0012/LoginSecurity-2
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/loginsecurity/
* <p>
* Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
*/
public class LoginSecurityHook implements AuthPlugin<Player> {
private final FastLoginBukkit plugin = (FastLoginBukkit) Bukkit.getPluginManager().getPlugin("FastLogin");
private final FastLoginBukkit plugin;
public LoginSecurityHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
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();
return session.isAuthorized()
|| session.performAction(new LoginAction(AuthService.PLUGIN, plugin)).isSuccess();
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String playerName) {
PlayerSession session = LoginSecurity.getSessionManager().getOfflineSession(playerName);
return session.isRegistered();
}

View File

@@ -1,10 +1,10 @@
package com.github.games647.fastlogin.bukkit.hooks;
package com.github.games647.fastlogin.bukkit.hook;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
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;
@@ -15,19 +15,24 @@ import ultraauth.managers.PlayerManager;
/**
* Project page:
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
*
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
* <p>
* Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
*/
public class UltraAuthHook implements AuthPlugin<Player> {
private final Plugin ultraAuthPlugin = Main.main;
private final FastLoginBukkit plugin;
public UltraAuthHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public boolean forceLogin(Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(ultraAuthPlugin, () -> {
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(plugin, () -> {
if (UltraAuthAPI.isAuthenticated(player)) {
return true;
}
@@ -39,24 +44,20 @@ public class UltraAuthHook implements AuthPlugin<Player> {
try {
return future.get();
} catch (InterruptedException | ExecutionException ex) {
ultraAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
plugin.getLog().error("Failed to forceLogin player: {}", player, ex);
return false;
}
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String 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 exception so check if our entry was saved
return forceLogin(player);
}
return false;
//the register method silents any exception so check if our entry was saved
return PlayerManager.getInstance().checkPlayerPassword(player, password) && forceLogin(player);
}
}

View File

@@ -1,5 +1,6 @@
package com.github.games647.fastlogin.bukkit.hooks;
package com.github.games647.fastlogin.bukkit.hook;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import de.luricos.bukkit.xAuth.xAuth;
@@ -7,26 +8,30 @@ import de.luricos.bukkit.xAuth.xAuthPlayer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/LycanDevelopment/xAuth/
*
* GitHub: https://github.com/LycanDevelopment/xAuth/
* <p>
* Project page:
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/xauth/
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/xauth/
*/
public class xAuthHook implements AuthPlugin<Player> {
private final xAuth xAuthPlugin = xAuth.getPlugin();
private final FastLoginBukkit plugin;
public xAuthHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public boolean forceLogin(Player player) {
//not thread-safe
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(xAuthPlugin, () -> {
Future<Boolean> future = Bukkit.getScheduler().callSyncMethod(plugin, () -> {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
if (xAuthPlayer.isAuthenticated()) {
@@ -46,13 +51,13 @@ public class xAuthHook implements AuthPlugin<Player> {
try {
return future.get();
} catch (InterruptedException | ExecutionException ex) {
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
plugin.getLog().error("Failed to forceLogin player: {}", player, ex);
return false;
}
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String playerName) {
//this will load the player if it's not in the cache
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(playerName);
return xAuthPlayer != null && xAuthPlayer.isRegistered();
@@ -63,22 +68,18 @@ public class xAuthHook implements AuthPlugin<Player> {
//not thread-safe
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 :(
//this should run async because the plugin executes a sql query, but the method
//accesses non thread-safe collections :(
return xAuthPlayer != null
&& xAuthPlugin.getAuthClass(xAuthPlayer).adminRegister(player.getName(), password, null);
return xAuthPlugin.getAuthClass(xAuthPlayer)
.adminRegister(player.getName(), password, null);
}
return false;
});
try {
//login in the player after registration
return future.get() && forceLogin(player);
} catch (InterruptedException | ExecutionException ex) {
xAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
plugin.getLog().error("Failed to forceRegister player: {}", player, ex);
return false;
}
}

View File

@@ -1,74 +0,0 @@
package com.github.games647.fastlogin.bukkit.hooks;
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 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) {
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) {
//this automatically registers the player too
if (v3APIAvailable) {
AuthMeApi.getInstance().forceRegister(player, password);
} else {
NewAPI.getInstance().forceRegister(player, password);
}
return true;
}
}

View File

@@ -1,67 +0,0 @@
package com.github.games647.fastlogin.bukkit.hooks;
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;
/**
* Github: https://github.com/RoyalDev/RoyalAuth
*
* Project page:
*
* Bukkit: http://dev.bukkit.org/bukkit-plugins/royalauth/
*/
public class RoyalAuthHook implements AuthPlugin<Player> {
private final Plugin royalAuthPlugin = (RoyalAuth) Bukkit.getPluginManager().getPlugin("RoyalAuth");
@Override
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();
return authPlayer.isLoggedIn();
});
try {
return future.get();
} catch (InterruptedException | ExecutionException ex) {
royalAuthPlugin.getLogger().log(Level.SEVERE, "Failed to forceLogin", ex);
return false;
}
}
@Override
public boolean isRegistered(String playerName) throws Exception {
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(playerName);
return authPlayer.isRegistered();
}
@Override
public boolean forceRegister(Player player, String password) {
//https://github.com/RoyalDev/RoyalAuth/blob/master/src/main/java/org/royaldev/royalauth/commands/CmdRegister.java#L50
AuthPlayer authPlayer = AuthPlayer.getAuthPlayer(player);
boolean registerSuccess = authPlayer.setPassword(password, Config.passwordHashType);
//login in the player after registration
return registerSuccess && forceLogin(player);
}
}

View File

@@ -2,8 +2,11 @@ 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.tasks.ForceLoginTask;
import com.github.games647.fastlogin.bukkit.task.ForceLoginTask;
import com.github.games647.fastlogin.core.PremiumStatus;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.message.LoginActionMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage.Type;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
@@ -11,22 +14,24 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import static java.util.stream.Collectors.toSet;
/**
* Responsible for receiving messages from a BungeeCord instance.
*
* This class also receives the plugin message from the bungeecord version of this plugin in order to get notified if
* the connection is in online mode.
*/
public class BungeeCordListener implements PluginMessageListener {
public class BungeeListener implements PluginMessageListener {
private static final String FILE_NAME = "proxy-whitelist.txt";
@@ -34,51 +39,54 @@ public class BungeeCordListener implements PluginMessageListener {
//null if whitelist is empty so bungeecord support is disabled
private final Set<UUID> proxyIds;
public BungeeCordListener(FastLoginBukkit plugin) {
public BungeeListener(FastLoginBukkit plugin) {
this.plugin = plugin;
this.proxyIds = loadBungeeCordIds();
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
if (!channel.equals(plugin.getName())) {
ByteArrayDataInput dataInput = ByteStreams.newDataInput(message);
LoginActionMessage loginMessage = new LoginActionMessage();
loginMessage.readFrom(dataInput);
plugin.getLog().debug("Received plugin message {}", loginMessage);
//check if the player is still online or disconnected
Player checkedPlayer = Bukkit.getPlayerExact(loginMessage.getPlayerName());
if (checkedPlayer == null) {
return;
}
ByteArrayDataInput dataInput = ByteStreams.newDataInput(message);
String subChannel = dataInput.readUTF();
plugin.getLogger().log(Level.FINEST, "Received plugin message for sub channel {0} from {1}"
, new Object[]{subChannel, player});
String playerName = dataInput.readUTF();
//check if the player is still online or disconnected
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())) {
//bungeecord UUID
long mostSignificantBits = dataInput.readLong();
long leastSignificantBits = dataInput.readLong();
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
plugin.getLogger().log(Level.FINEST, "Received proxy id {0} from {1}", new Object[]{sourceId, player});
if (checkedPlayer.hasMetadata(plugin.getName())) {
plugin.getLog().warn("Received message {} from a blacklisted player {}", loginMessage, checkedPlayer);
} else {
//fail if BungeeCord support is disabled (id = null)
UUID sourceId = loginMessage.getProxyId();
if (proxyIds.contains(sourceId)) {
readMessage(checkedPlayer, subChannel, playerName, player);
readMessage(checkedPlayer, loginMessage);
} else {
plugin.getLog().warn("Received proxy id: {} that doesn't exist in the proxy whitelist file", sourceId);
}
}
}
private void readMessage(Player checkedPlayer, String subchannel, String playerName, Player player) {
InetSocketAddress address = checkedPlayer.getAddress();
private void readMessage(Player player, LoginActionMessage message) {
String playerName = message.getPlayerName();
Type type = message.getType();
InetSocketAddress address = player.getAddress();
String id = '/' + address.getAddress().getHostAddress() + ':' + address.getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
if (type == Type.LOGIN) {
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, () -> {
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin.getCore(), player), 10L);
} else if (type == Type.REGISTER) {
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
try {
//we need to check if the player is registered on Bukkit too
@@ -89,29 +97,33 @@ public class BungeeCordListener implements PluginMessageListener {
new ForceLoginTask(plugin.getCore(), player).run();
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
plugin.getLog().error("Failed to query isRegistered for player: {}", player, ex);
}
});
}, 10L);
} else if (type == Type.CRACKED) {
//we don't start a force login task here so update it manually
plugin.getPremiumPlayers().put(player.getUniqueId(), PremiumStatus.CRACKED);
}
}
public Set<UUID> loadBungeeCordIds() {
Path whitelistFile = plugin.getDataFolder().toPath().resolve(FILE_NAME);
Path whitelistFile = plugin.getPluginFolder().resolve(FILE_NAME);
try {
if (Files.notExists(whitelistFile)) {
Files.createFile(whitelistFile);
}
return Files.lines(whitelistFile)
.map(String::trim)
.map(UUID::fromString)
.collect(Collectors.toSet());
try (Stream<String> lines = Files.lines(whitelistFile)) {
return lines.map(String::trim)
.map(UUID::fromString)
.collect(toSet());
}
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
plugin.getLog().error("Failed to create file for Proxy whitelist", ex);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
plugin.getLog().error("Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
}
return null;
return Collections.emptySet();
}
}

View File

@@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import com.github.games647.fastlogin.bukkit.task.ForceLoginTask;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -17,13 +17,13 @@ import org.bukkit.event.player.PlayerQuitEvent;
* This listener tells authentication plugins if the player has a premium account and we checked it successfully. So the
* plugin can skip authentication.
*/
public class BukkitJoinListener implements Listener {
public class ConnectionListener implements Listener {
private static final long DELAY_LOGIN = 20L / 2;
private final FastLoginBukkit plugin;
public BukkitJoinListener(FastLoginBukkit plugin) {
public ConnectionListener(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@@ -38,7 +38,8 @@ public class BukkitJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
if (!plugin.isBungeeCord()) {
removeBlacklistStatus(player);
if (!plugin.isBungeeEnabled()) {
//Wait before auth plugin and we received a message from BungeeCord initializes the player
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player);
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, forceLoginTask, DELAY_LOGIN);
@@ -48,7 +49,12 @@ public class BukkitJoinListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
Player player = quitEvent.getPlayer();
removeBlacklistStatus(player);
plugin.getPremiumPlayers().remove(player.getUniqueId());
}
private void removeBlacklistStatus(Player player) {
player.removeMetadata(plugin.getName(), plugin);
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
}
}

View File

@@ -4,11 +4,13 @@ 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.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.security.PublicKey;
import java.util.Random;
import java.util.logging.Level;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -18,17 +20,20 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final PublicKey publicKey;
private final Random random;
private final Player player;
private final String username;
public NameCheckTask(FastLoginBukkit plugin, PacketEvent packetEvent, Random random, Player player, String username) {
public NameCheckTask(FastLoginBukkit plugin, PacketEvent packetEvent, Random random,
Player player, String username, PublicKey publicKey) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
this.packetEvent = packetEvent;
this.publicKey = publicKey;
this.random = random;
this.player = player;
this.username = username;
@@ -37,20 +42,28 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
@Override
public void run() {
try {
super.onLogin(username, new ProtocolLibLoginSource(plugin, packetEvent, player, random));
super.onLogin(username, new ProtocolLibLoginSource(packetEvent, player, random, publicKey));
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
//minecraft server implementation
@Override
public FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, ProtocolLibLoginSource source, StoredProfile profile) {
BukkitFastLoginPreLoginEvent event = new BukkitFastLoginPreLoginEvent(username, source, profile);
plugin.getServer().getPluginManager().callEvent(event);
return event;
}
//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) {
public void requestPremiumLogin(ProtocolLibLoginSource source, StoredProfile 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);
plugin.getLog().error("Cannot send encryption packet. Falling back to cracked login for: {}", profile, ex);
return;
}
@@ -69,7 +82,27 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
}
@Override
public void startCrackedSession(ProtocolLibLoginSource source, PlayerProfile profile, String username) {
public void requestConfirmationLogin(ProtocolLibLoginSource source, StoredProfile profile, String username) {
try {
source.setOnlineMode();
} catch (Exception ex) {
plugin.getLog().error("Cannot send encryption packet. Falling back to cracked login for: {}", profile, ex);
return;
}
String serverId = source.getServerId();
byte[] verify = source.getVerifyToken();
BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId, verify, 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, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(player.getAddress().toString(), loginSession);
}

View File

@@ -1,37 +1,43 @@
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.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.util.Random;
import java.util.logging.Level;
import java.security.KeyPair;
import java.security.SecureRandom;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
import static com.comphenix.protocol.PacketType.Login.Client.START;
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();
private final SecureRandom random = new SecureRandom();
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
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)
super(params()
.plugin(plugin)
.types(START, ENCRYPTION_BEGIN)
.optionAsync());
this.plugin = plugin;
}
public static void register(FastLoginBukkit plugin) {
//they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin))
@@ -48,7 +54,7 @@ public class ProtocolLibListener extends PacketAdapter {
Player sender = packetEvent.getPlayer();
PacketType packetType = packetEvent.getPacketType();
if (packetType == Client.START) {
if (packetType == START) {
onLogin(packetEvent, sender);
} else {
onEncryptionBegin(packetEvent, sender);
@@ -59,8 +65,8 @@ public class ProtocolLibListener extends PacketAdapter {
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret);
Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask);
Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
plugin.getScheduler().runAsync(verifyTask);
}
private void onLogin(PacketEvent packetEvent, Player player) {
@@ -74,10 +80,10 @@ public class ProtocolLibListener extends PacketAdapter {
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});
plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username);
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
Runnable nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username, keyPair.getPublic());
plugin.getScheduler().runAsync(nameCheckTask);
}
}

View File

@@ -1,59 +1,67 @@
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.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Random;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Server.DISCONNECT;
import static com.comphenix.protocol.PacketType.Login.Server.ENCRYPTION_BEGIN;
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 final PublicKey publicKey;
private String serverId;
private final byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
private final String serverId = "";
private byte[] verifyToken;
public ProtocolLibLoginSource(FastLoginBukkit plugin, PacketEvent packetEvent, Player player, Random random) {
this.plugin = plugin;
public ProtocolLibLoginSource(PacketEvent packetEvent, Player player, Random random, PublicKey publicKey) {
this.packetEvent = packetEvent;
this.player = player;
this.random = random;
this.publicKey = publicKey;
}
@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);
verifyToken = EncryptionUtil.generateVerifyToken(random);
//generate a random token which should be the same when we receive it from the client
random.nextBytes(verifyToken);
sentEncryptionRequest();
/*
* Packet Information: https://wiki.vg/Protocol#Encryption_Request
*
* ServerID="" (String) key=public server key verifyToken=random 4 byte array
*/
PacketContainer newPacket = new PacketContainer(ENCRYPTION_BEGIN);
newPacket.getStrings().write(0, serverId);
newPacket.getSpecificModifier(PublicKey.class).write(0, publicKey);
newPacket.getByteArrays().write(0, verifyToken);
//serverId is a empty string
ProtocolLibrary.getProtocolManager().sendServerPacket(player, newPacket);
}
@Override
public void kick(String message) throws Exception {
public void kick(String message) throws InvocationTargetException {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
PacketContainer kickPacket = new PacketContainer(DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(message));
try {
@@ -71,29 +79,22 @@ public class ProtocolLibLoginSource implements LoginSource {
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;
return verifyToken.clone();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"packetEvent=" + packetEvent +
", player=" + player +
", random=" + random +
", serverId='" + serverId + '\'' +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
}

View File

@@ -6,11 +6,11 @@ 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.craftapi.model.skin.Textures;
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;
@@ -19,20 +19,19 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
public class LoginSkinApplyListener implements Listener {
public class SkinApplyListener 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) {
public SkinApplyListener(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
//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;
@@ -45,10 +44,7 @@ public class LoginSkinApplyListener implements Listener {
//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);
session.getSkin().ifPresent(skin -> applySkin(player, skin.getValue(), skin.getSignature()));
break;
}
}
@@ -57,17 +53,17 @@ public class LoginSkinApplyListener implements Listener {
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);
WrappedSignedProperty skin = WrappedSignedProperty.fromValues(Textures.KEY, skinData, signature);
try {
gameProfile.getProperties().put(Textures.KEY, skin);
} catch (ClassCastException castException) {
//Cauldron, MCPC, Thermos, ...
Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
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);
}
MethodUtils.invokeMethod(map, "put", new Object[]{Textures.KEY, skin.getHandle()});
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLog().error("Error setting premium skin of: {}", player, ex);
}
}
}

View File

@@ -1,8 +1,6 @@
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;
@@ -10,46 +8,59 @@ 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.craftapi.model.auth.Verification;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.Key;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.START;
import static com.comphenix.protocol.PacketType.Login.Server.DISCONNECT;
public class VerifyResponseTask implements Runnable {
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final KeyPair serverKey;
private final Player fromPlayer;
private final Player player;
private final byte[] sharedSecret;
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player fromPlayer, byte[] sharedSecret) {
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player player,
byte[] sharedSecret, KeyPair keyPair) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.fromPlayer = fromPlayer;
this.sharedSecret = sharedSecret;
this.player = player;
this.sharedSecret = Arrays.copyOf(sharedSecret, sharedSecret.length);
this.serverKey = keyPair;
}
@Override
public void run() {
try {
BukkitLoginSession session = plugin.getLoginSessions().get(fromPlayer.getAddress().toString());
BukkitLoginSession session = plugin.getLoginSessions().get(player.getAddress().toString());
if (session == null) {
disconnect(plugin.getCore().getMessage("invalid-request"), true
, "Player {0} tried to send encryption response at invalid state", fromPlayer.getAddress());
disconnect("invalid-request", true
, "GameProfile {0} tried to send encryption response at invalid state", player.getAddress());
} else {
verifyResponse(session);
}
@@ -64,34 +75,58 @@ public class VerifyResponseTask implements Runnable {
}
private void verifyResponse(BukkitLoginSession session) {
PrivateKey privateKey = plugin.getServerKey().getPrivate();
PrivateKey privateKey = serverKey.getPrivate();
SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret);
if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) {
Cipher cipher;
SecretKey loginKey;
try {
cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
loginKey = EncryptionUtil.decryptSharedKey(cipher, sharedSecret);
} catch (GeneralSecurityException securityEx) {
disconnect("error-kick", false, "Cannot decrypt received contents", securityEx);
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();
try {
if (!checkVerifyToken(session, cipher, privateKey) || !encryptConnection(loginKey)) {
return;
}
} catch (Exception ex) {
disconnect("error-kick", false, "Cannot decrypt received contents", ex);
return;
}
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
//generate the server id based on client and server data
byte[] serverIdHash = EncryptionUtil.getServerIdHash(generatedId, plugin.getServerKey().getPublic(), loginKey);
String serverId = (new BigInteger(serverIdHash)).toString(16);
String serverId = EncryptionUtil.getServerIdHashString("", loginKey, serverKey.getPublic());
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);
InetSocketAddress socketAddress = player.getAddress();
try {
MojangResolver resolver = plugin.getCore().getResolver();
InetAddress address = socketAddress.getAddress();
Optional<Verification> response = resolver.hasJoined(username, serverId, address);
if (response.isPresent()) {
plugin.getLog().info("GameProfile {} 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);
SkinProperty[] properties = response.get().getProperties();
if (properties.length > 0) {
session.setSkinProperty(properties[0]);
}
session.setUuid(response.get().getId());
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect("invalid-session", true
, "GameProfile {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), socketAddress, serverId);
}
} catch (IOException ioEx) {
disconnect("error-kick", false, "Failed to connect to session server", ioEx);
}
}
@@ -102,21 +137,22 @@ public class VerifyResponseTask implements Runnable {
//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);
plugin.getLog().error("Error setting premium uuid of {}", player, exc);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session, Key privateKey) {
private boolean checkVerifyToken(BukkitLoginSession session, Cipher cipher, PrivateKey privateKey)
throws GeneralSecurityException {
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))) {
if (!Arrays.equals(requestVerify, EncryptionUtil.decrypt(cipher, 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}"
disconnect("invalid-verify-token", true
, "GameProfile {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}"
, session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
}
@@ -125,8 +161,8 @@ public class VerifyResponseTask implements Runnable {
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
Object injectorContainer = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer);
private Object getNetworkManager() throws IllegalAccessException, ClassNotFoundException {
Object injectorContainer = TemporaryPlayerFactory.getInjectorFromPlayer(player);
//ChannelInjector
Class<?> injectorClass = Class.forName("com.comphenix.protocol.injector.netty.Injector");
@@ -147,57 +183,52 @@ public class VerifyResponseTask implements Runnable {
//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");
disconnect("error-kick", false, "Couldn't enable encryption", ex);
return false;
}
return true;
}
private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) {
private void disconnect(String reasonKey, boolean debug, String logMessage, Object... arguments) {
if (debug) {
plugin.getLogger().log(Level.FINE, logMessage, arguments);
plugin.getLog().debug(logMessage, arguments);
} else {
plugin.getLogger().log(Level.SEVERE, logMessage, arguments);
plugin.getLog().error(logMessage, arguments);
}
kickPlayer(packetEvent.getPlayer(), kickReason);
kickPlayer(plugin.getCore().getMessage(reasonKey));
}
private void kickPlayer(Player player, String reason) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
private void kickPlayer(String reason) {
PacketContainer kickPacket = new PacketContainer(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);
ProtocolLibrary.getProtocolManager().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);
plugin.getLog().error("Error sending kick packet for: {}", player, 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);
PacketContainer startPacket = new PacketContainer(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);
ProtocolLibrary.getProtocolManager().recieveClientPacket(player, startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
plugin.getLog().warn("Failed to fake a new start packet for: {}", username, ex);
//cancel the event in order to prevent the server receiving an invalid packet
kickPlayer(fromPlayer, plugin.getCore().getMessage("error-kick"));
kickPlayer(plugin.getCore().getMessage("error-kick"));
}
}
}

View File

@@ -29,7 +29,10 @@ public class ProtocolLoginSource implements LoginSource {
return loginStartEvent.getAddress();
}
public PlayerLoginStartEvent getLoginStartEvent() {
return loginStartEvent;
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"loginStartEvent=" + loginStartEvent +
'}';
}
}

View File

@@ -1,18 +1,23 @@
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
import com.github.games647.craftapi.UUIDAdapter;
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.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.net.InetSocketAddress;
import java.util.Optional;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import protocolsupport.api.events.ConnectionCloseEvent;
import protocolsupport.api.events.PlayerLoginStartEvent;
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
import protocolsupport.api.events.PlayerProfileCompleteEvent;
public class ProtocolSupportListener extends JoinManagement<Player, CommandSender, ProtocolLoginSource>
implements Listener {
@@ -31,7 +36,7 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
return;
}
String username = loginStartEvent.getName();
String username = loginStartEvent.getConnection().getProfile().getName();
InetSocketAddress address = loginStartEvent.getAddress();
//remove old data every time on a new login in order to keep the session only for one person
@@ -41,32 +46,56 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
}
@EventHandler
public void onPropertiesResolve(PlayerPropertiesResolveEvent propertiesResolveEvent) {
InetSocketAddress address = propertiesResolveEvent.getAddress();
public void onConnectionClosed(ConnectionCloseEvent closeEvent) {
InetSocketAddress address = closeEvent.getConnection().getAddress();
plugin.getLoginSessions().remove(address.toString());
}
@EventHandler
public void onPropertiesResolve(PlayerProfileCompleteEvent profileCompleteEvent) {
InetSocketAddress address = profileCompleteEvent.getAddress();
BukkitLoginSession session = plugin.getLoginSessions().get(address.toString());
//skin was resolved -> premium player
if (propertiesResolveEvent.hasProperty("textures") && session != null) {
if (session != null && profileCompleteEvent.getConnection().getProfile().isOnlineMode()) {
session.setVerified(true);
if (!plugin.getConfig().getBoolean("premiumUuid")) {
String username = Optional.ofNullable(profileCompleteEvent.getForcedName())
.orElse(profileCompleteEvent.getConnection().getProfile().getName());
profileCompleteEvent.setForcedUUID(UUIDAdapter.generateOfflineId(username));
}
}
}
@Override
public void requestPremiumLogin(ProtocolLoginSource source, PlayerProfile profile, String username, boolean registered) {
public FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, ProtocolLoginSource source, StoredProfile profile) {
BukkitFastLoginPreLoginEvent event = new BukkitFastLoginPreLoginEvent(username, source, profile);
plugin.getServer().getPluginManager().callEvent(event);
return event;
}
@Override
public void requestPremiumLogin(ProtocolLoginSource source, StoredProfile 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);
BukkitLoginSession playerSession = new BukkitLoginSession(username, 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) {
public void requestConfirmationLogin(ProtocolLoginSource source, StoredProfile profile, String username) {
source.setOnlineMode();
BukkitLoginSession playerSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), playerSession);
}
@Override
public void startCrackedSession(ProtocolLoginSource source, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), loginSession);
}

View File

@@ -0,0 +1,100 @@
package com.github.games647.fastlogin.bukkit.task;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.hook.AuthMeHook;
import com.github.games647.fastlogin.bukkit.hook.CrazyLoginHook;
import com.github.games647.fastlogin.bukkit.hook.LogItHook;
import com.github.games647.fastlogin.bukkit.hook.LoginSecurityHook;
import com.github.games647.fastlogin.bukkit.hook.UltraAuthHook;
import com.github.games647.fastlogin.bukkit.hook.xAuthHook;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
public class DelayedAuthHook implements Runnable {
private final FastLoginBukkit plugin;
public DelayedAuthHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@Override
public void run() {
boolean hookFound = isHookFound();
if (plugin.isBungeeEnabled()) {
plugin.getLog().info("BungeeCord setting detected. No auth plugin is required");
} else if (!hookFound) {
plugin.getLog().warn("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 isHookFound() {
return plugin.getCore().getAuthPluginHook() != null || registerHooks();
}
private boolean registerHooks() {
AuthPlugin<Player> authPluginHook = getAuthHook();
if (authPluginHook == null) {
//run this check for exceptions (errors) and not found plugins
plugin.getLog().warn("No support offline Auth plugin found. ");
return false;
}
if (authPluginHook instanceof Listener) {
Bukkit.getPluginManager().registerEvents((Listener) authPluginHook, plugin);
}
if (plugin.getCore().getAuthPluginHook() == null) {
plugin.getLog().info("Hooking into auth plugin: {}", authPluginHook.getClass().getSimpleName());
plugin.getCore().setAuthPluginHook(authPluginHook);
}
return true;
}
private AuthPlugin<Player> getAuthHook() {
try {
@SuppressWarnings("unchecked")
List<Class<? extends AuthPlugin<Player>>> hooks = Arrays.asList(AuthMeHook.class,
CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class,
xAuthHook.class);
for (Class<? extends AuthPlugin<Player>> clazz : hooks) {
String pluginName = clazz.getSimpleName().replace("Hook", "");
//uses only member classes which uses AuthPlugin interface (skip interfaces)
if (Bukkit.getPluginManager().isPluginEnabled(pluginName)) {
//check only for enabled plugins. A single plugin could be disabled by plugin managers
return newInstance(clazz);
}
}
} catch (ReflectiveOperationException ex) {
plugin.getLog().error("Couldn't load the auth hook class", ex);
}
return null;
}
private AuthPlugin<Player> newInstance(Class<? extends AuthPlugin<Player>> clazz)
throws ReflectiveOperationException {
try {
Constructor<? extends AuthPlugin<Player>> cons = clazz.getDeclaredConstructor(FastLoginBukkit.class);
return cons.newInstance(plugin);
} catch (NoSuchMethodException noMethodEx) {
return clazz.getDeclaredConstructor().newInstance();
}
}
}

View File

@@ -1,16 +1,18 @@
package com.github.games647.fastlogin.bukkit.tasks;
package com.github.games647.fastlogin.bukkit.task;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginAutoLoginEvent;
import com.github.games647.fastlogin.core.PremiumStatus;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.message.SuccessMessage;
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 com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -19,29 +21,42 @@ 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);
super(core, player, getSession(core.getPlugin(), player));
}
private static BukkitLoginSession getSession(FastLoginBukkit plugin, Player player) {
//remove the bungeecord identifier if there is ones
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
return plugin.getLoginSessions().remove(id);
}
@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
//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();
PremiumStatus status = PremiumStatus.CRACKED;
if (isOnlineMode()) {
status = PremiumStatus.PREMIUM;
}
plugin.getPremiumPlayers().put(player.getUniqueId(), status);
}
@Override
public FastLoginAutoLoginEvent callFastLoginAutoLoginEvent(LoginSession session, StoredProfile profile) {
BukkitFastLoginAutoLoginEvent event = new BukkitFastLoginAutoLoginEvent(session, profile);
core.getPlugin().getServer().getPluginManager().callEvent(event);
return event;
}
@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());
if (core.getPlugin().isBungeeEnabled()) {
core.getPlugin().sendPluginMessage(player, new SuccessMessage());
}
}
@@ -53,16 +68,20 @@ public class ForceLoginTask extends ForceLoginManagement<Player, CommandSender,
@Override
public boolean isOnline(Player player) {
try {
//the playerlist isn't thread-safe
//the player-list 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);
core.getPlugin().getLog().error("Failed to perform thread-safe online check for {}", player, ex);
return false;
}
}
@Override
public boolean isOnlineMode() {
if (session == null) {
return false;
}
return session.isVerified() && player.getName().equals(session.getUsername());
}
}

View File

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

@@ -1,7 +1,7 @@
# 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}-git${git.commit.id}
version: ${project.version}-${git.commit.id.abbrev}
main: ${project.groupId}.${project.artifactId}.${project.name}
# meta data for plugin managers
@@ -11,14 +11,21 @@ description: |
website: ${project.url}
dev-url: ${project.url}
# Load the plugin as early as possible to inject it for all players
load: STARTUP
# This plugin don't have to be transformed for compatibility with Minecraft >= 1.13
api-version: '1.13'
softdepend:
# We depend either ProtocolLib or ProtocolSupport
- ProtocolSupport
- ProtocolLib
- PlaceholderAPI
# Auth plugins
- AuthMe
- LoginSecurity
- xAuth
- LogIt
- UltraAuth
- CrazyLogin
commands:
${project.parent.name}:

View File

@@ -0,0 +1,42 @@
package com.github.games647.fastlogin.bukkit;
import java.security.SecureRandom;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
public class EncryptionUtilTest {
@Test
public void testVerifyToken() throws Exception {
SecureRandom random = new SecureRandom();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertThat(token, notNullValue());
assertThat(token.length, is(4));
}
// @Test
// public void testDecryptSharedSecret() throws Exception {
//
// }
//
// @Test
// public void testDecryptData() throws Exception {
//
// }
// private static SecretKey createNewSharedKey() {
// try {
// KeyGenerator keygenerator = KeyGenerator.getInstance("AES");
// keygenerator.init(128);
// return keygenerator.generateKey();
// } catch (NoSuchAlgorithmException nosuchalgorithmexception) {
// throw new Error(nosuchalgorithmexception);
// }
// }
}

Binary file not shown.

View File

@@ -1,11 +1,11 @@
<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">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>
<version>1.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -16,44 +16,75 @@
<!--Represents the main plugin-->
<name>FastLoginBungee</name>
<repositories>
<!--BungeeCord with also the part outside the API-->
<repository>
<id>luck-repo</id>
<url>https://ci.lucko.me/plugin/repository/everything</url>
</repository>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
<artifactSet>
<excludes>
<!--Those classes are already present in BungeeCord version-->
<exclude>net.md-5:bungeecord-config</exclude>
<exclude>com.google.code.gson:gson</exclude>
</excludes>
</artifactSet>
<relocations>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>fastlogin.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.slf4j</pattern>
<shadedPattern>fastlogin.slf4j</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>vik1395-repo</id>
<url>http://repo.vik1395.me/repositories</url>
<id>codemc-repo</id>
<url>https://repo.codemc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<!--Common plugin component-->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<!--BungeeCord with also the part outside the API-->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.12-SNAPSHOT</version>
<version>1.14-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!--Login plugin-->
<dependency>
<groupId>me.vik1395</groupId>
<artifactId>BungeeAuth</artifactId>
<version>1.4</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
<scope>system</scope>
<systemPath>${project.basedir}/lib/BungeeAuth-1.4.jar</systemPath>
</dependency>
</dependencies>
</project>

View File

@@ -1,6 +1,6 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
public class BungeeLoginSession extends LoginSession {
@@ -8,27 +8,41 @@ public class BungeeLoginSession extends LoginSession {
private boolean alreadySaved;
private boolean alreadyLogged;
public BungeeLoginSession(String username, boolean registered, PlayerProfile profile) {
public BungeeLoginSession(String username, boolean registered, StoredProfile profile) {
super(username, registered, profile);
}
public void setRegistered(boolean registered) {
public BungeeLoginSession(String username, StoredProfile profile) {
super(username, profile);
}
public synchronized void setRegistered(boolean registered) {
this.registered = registered;
}
public boolean isAlreadySaved() {
public synchronized boolean isAlreadySaved() {
return alreadySaved;
}
public void setAlreadySaved(boolean alreadySaved) {
public synchronized void setAlreadySaved(boolean alreadySaved) {
this.alreadySaved = alreadySaved;
this.confirmationLogin = false;
}
public boolean isAlreadyLogged() {
public synchronized boolean isAlreadyLogged() {
return alreadyLogged;
}
public void setAlreadyLogged(boolean alreadyLogged) {
public synchronized void setAlreadyLogged(boolean alreadyLogged) {
this.alreadyLogged = alreadyLogged;
}
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"alreadySaved=" + alreadySaved +
", alreadyLogged=" + alreadyLogged +
", registered=" + registered +
"} " + super.toString();
}
}

View File

@@ -4,15 +4,20 @@ import com.github.games647.fastlogin.core.shared.LoginSource;
import java.net.InetSocketAddress;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PreLoginEvent;
public class BungeeLoginSource implements LoginSource {
private final PendingConnection connection;
private final PreLoginEvent preLoginEvent;
public BungeeLoginSource(PendingConnection connection) {
public BungeeLoginSource(PendingConnection connection, PreLoginEvent preLoginEvent) {
this.connection = connection;
this.preLoginEvent = preLoginEvent;
}
@Override
@@ -22,7 +27,12 @@ public class BungeeLoginSource implements LoginSource {
@Override
public void kick(String message) {
connection.disconnect(TextComponent.fromLegacyText(message));
preLoginEvent.setCancelled(true);
if (message != null)
preLoginEvent.setCancelReason(TextComponent.fromLegacyText(message));
else
preLoginEvent.setCancelReason(new ComponentBuilder("Kicked").color(ChatColor.WHITE).create());
}
@Override
@@ -33,4 +43,11 @@ public class BungeeLoginSource implements LoginSource {
public PendingConnection getConnection() {
return connection;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"connection=" + connection +
'}';
}
}

View File

@@ -1,44 +1,51 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthHook;
import com.github.games647.fastlogin.bungee.listener.PlayerConnectionListener;
import com.github.games647.fastlogin.bungee.hook.BungeeAuthHook;
import com.github.games647.fastlogin.bungee.listener.ConnectListener;
import com.github.games647.fastlogin.bungee.listener.PluginMessageListener;
import com.github.games647.fastlogin.core.AsyncScheduler;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.message.ChangePremiumMessage;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
import com.github.games647.fastlogin.core.message.SuccessMessage;
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 com.google.common.collect.MapMaker;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentMap;
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.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.connection.Server;
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;
import org.slf4j.Logger;
/**
* BungeeCord version of FastLogin. This plugin keeps track on online mode connections.
*/
public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSender> {
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = Maps.newConcurrentMap();
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = new MapMaker().weakKeys().makeMap();
private FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core;
private AsyncScheduler scheduler;
private Logger logger;
@Override
public void onEnable() {
logger = CommonUtil.createLoggerFromJDK(getLogger());
scheduler = new AsyncScheduler(logger, getThreadFactory());
core = new FastLoginCore<>(this);
core.load();
if (!core.setupDatabase()) {
@@ -46,11 +53,12 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
}
//events
getProxy().getPluginManager().registerListener(this, new PlayerConnectionListener(this));
getProxy().getPluginManager().registerListener(this, new ConnectListener(this));
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
//this is required to listen to messages from the server
getProxy().registerChannel(getDescription().getName());
//this is required to listen to incoming messages from the server
getProxy().registerChannel(NamespaceKey.getCombined(getName(), ChangePremiumMessage.CHANGE_CHANNEL));
getProxy().registerChannel(NamespaceKey.getCombined(getName(), SuccessMessage.SUCCESS_CHANNEL));
registerHook();
}
@@ -74,7 +82,17 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
Plugin plugin = getProxy().getPluginManager().getPlugin("BungeeAuth");
if (plugin != null) {
core.setAuthPluginHook(new BungeeAuthHook());
getLogger().info("Hooked into BungeeAuth");
logger.info("Hooked into BungeeAuth");
}
}
public void sendPluginMessage(Server server, ChannelMessage message) {
if (server != null) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
message.writeTo(dataOutput);
NamespaceKey channel = new NamespaceKey(getName(), message.getChannelName());
server.sendData(channel.getCombinedName(), dataOutput.toByteArray());
}
}
@@ -84,12 +102,13 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
}
@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));
public Path getPluginFolder() {
return getDataFolder().toPath();
}
@Override
public Logger getLog() {
return logger;
}
@Override
@@ -97,20 +116,19 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
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());
return new ThreadFactoryBuilder()
.setNameFormat(getName() + " Pool Thread #%1$d")
//Hikari create daemons by default
.setDaemon(true)
.setThreadFactory(new GroupedThreadFactory(this, getName()))
.build();
}
@Override
public MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests
, Map<String, Integer> proxies) {
return new MojangApiBungee(logger, addresses, requests, proxies);
public AsyncScheduler getScheduler() {
return scheduler;
}
}

View File

@@ -1,43 +0,0 @@
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,15 +0,0 @@
package com.github.games647.fastlogin.bungee;
public class MojangPlayer {
private String id;
private String name;
public String getId() {
return id;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,39 @@
package com.github.games647.fastlogin.bungee.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import net.md_5.bungee.api.plugin.Cancellable;
import net.md_5.bungee.api.plugin.Event;
public class BungeeFastLoginAutoLoginEvent extends Event implements FastLoginAutoLoginEvent, Cancellable {
private final LoginSession session;
private final StoredProfile profile;
private boolean cancelled;
public BungeeFastLoginAutoLoginEvent(LoginSession session, StoredProfile profile) {
this.session = session;
this.profile = profile;
}
@Override
public LoginSession getSession() {
return session;
}
@Override
public StoredProfile getProfile() {
return profile;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}

View File

@@ -0,0 +1,34 @@
package com.github.games647.fastlogin.bungee.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSource;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import net.md_5.bungee.api.plugin.Event;
public class BungeeFastLoginPreLoginEvent extends Event implements FastLoginPreLoginEvent {
private final String username;
private final LoginSource source;
private final StoredProfile profile;
public BungeeFastLoginPreLoginEvent(String username, LoginSource source, StoredProfile profile) {
this.username = username;
this.source = source;
this.profile = profile;
}
@Override
public String getUsername() {
return username;
}
@Override
public LoginSource getSource() {
return source;
}
@Override
public StoredProfile getProfile() {
return profile;
}
}

View File

@@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bungee.hooks;
package com.github.games647.fastlogin.bungee.hook;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
@@ -8,7 +8,7 @@ import me.vik1395.BungeeAuthAPI.RequestHandler;
import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* Github: https://github.com/vik1395/BungeeAuth-Minecraft
* GitHub: https://github.com/vik1395/BungeeAuth-Minecraft
*
* Project page:
*
@@ -21,15 +21,11 @@ public class BungeeAuthHook implements AuthPlugin<ProxiedPlayer> {
@Override
public boolean forceLogin(ProxiedPlayer player) {
String playerName = player.getName();
if (Main.plonline.contains(playerName)) {
return true;
}
return requestHandler.forceLogin(playerName);
return Main.plonline.contains(playerName) || requestHandler.forceLogin(playerName);
}
@Override
public boolean isRegistered(String playerName) throws Exception {
public boolean isRegistered(String playerName) {
return requestHandler.isRegistered(playerName);
}

View File

@@ -1,17 +1,16 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.craftapi.UUIDAdapter;
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.bungee.task.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
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;
@@ -27,15 +26,15 @@ 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
* 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 {
public class ConnectListener implements Listener {
private final FastLoginBungee plugin;
private final Property[] emptyProperties = {};
public PlayerConnectionListener(FastLoginBungee plugin) {
public ConnectListener(FastLoginBungee plugin) {
this.plugin = plugin;
}
@@ -46,10 +45,10 @@ public class PlayerConnectionListener implements Listener {
}
preLoginEvent.registerIntent(plugin);
PendingConnection connection = preLoginEvent.getConnection();
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
ProxyServer.getInstance().getScheduler().runAsync(plugin, asyncPremiumCheck);
plugin.getScheduler().runAsync(asyncPremiumCheck);
}
@EventHandler(priority = EventPriority.LOWEST)
@@ -58,7 +57,7 @@ public class PlayerConnectionListener implements Listener {
return;
}
//use the login event instead of the postlogin event in order to send the loginsuccess packet to the client
//use the login event instead of the post login event in order to send the login success 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;
@@ -68,13 +67,13 @@ public class PlayerConnectionListener implements Listener {
LoginSession session = plugin.getSession().get(connection);
session.setUuid(connection.getUniqueId());
PlayerProfile playerProfile = session.getProfile();
playerProfile.setUuid(connection.getUniqueId());
StoredProfile playerProfile = session.getProfile();
playerProfile.setId(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));
UUID offlineUUID = UUIDAdapter.generateOfflineId(username);
//bungeecord doesn't support overriding the premium uuid
//so we have to do it with reflection
@@ -82,7 +81,7 @@ public class PlayerConnectionListener implements Listener {
idField.setAccessible(true);
idField.set(connection, offlineUUID);
} catch (NoSuchFieldException | IllegalAccessException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to set offline uuid", ex);
plugin.getLog().error("Failed to set offline uuid of {}", username, ex);
}
}
@@ -90,7 +89,7 @@ public class PlayerConnectionListener implements Listener {
//this is null on offline mode
LoginResult loginProfile = initialHandler.getLoginProfile();
if (loginProfile != null) {
loginProfile.setProperties(new Property[]{});
loginProfile.setProperties(emptyProperties);
}
}
}
@@ -102,13 +101,13 @@ public class PlayerConnectionListener implements Listener {
Server server = serverConnectedEvent.getServer();
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
plugin.getScheduler().runAsync(loginTask);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
ProxiedPlayer player = disconnectEvent.getPlayer();
plugin.getSession().remove(player.getPendingConnection());
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
plugin.getCore().getPendingConfirms().remove(player.getName(), ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN);
}
}

View File

@@ -2,8 +2,12 @@ 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.bungee.task.AsyncToggleMessage;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.message.ChangePremiumMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
import com.github.games647.fastlogin.core.message.SuccessMessage;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
@@ -11,8 +15,6 @@ 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;
@@ -23,14 +25,20 @@ public class PluginMessageListener implements Listener {
private final FastLoginBungee plugin;
private final String successChannel;
private final String changeChannel;
public PluginMessageListener(FastLoginBungee plugin) {
this.plugin = plugin;
this.successChannel = new NamespaceKey(plugin.getName(), SuccessMessage.SUCCESS_CHANNEL).getCombinedName();
this.changeChannel = new NamespaceKey(plugin.getName(), ChangePremiumMessage.CHANGE_CHANNEL).getCombinedName();
}
@EventHandler
public void onPluginMessage(PluginMessageEvent pluginMessageEvent) {
String channel = pluginMessageEvent.getTag();
if (pluginMessageEvent.isCancelled() || !plugin.getDescription().getName().equals(channel)) {
if (pluginMessageEvent.isCancelled() || !channel.startsWith(plugin.getName().toLowerCase())) {
return;
}
@@ -38,45 +46,64 @@ public class PluginMessageListener implements Listener {
//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));
if (!(pluginMessageEvent.getSender() instanceof Server)) {
//check if the message is sent from the server
return;
}
//so that we can safely process this in the background
byte[] data = Arrays.copyOf(pluginMessageEvent.getData(), pluginMessageEvent.getData().length);
ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
plugin.getScheduler().runAsync(() -> readMessage(forPlayer, channel, data));
}
private void readMessage(ProxiedPlayer fromPlayer, String channel, byte[] data) {
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
if (successChannel.equals(channel)) {
onSuccessMessage(fromPlayer);
} else if (changeChannel.equals(channel)) {
ChangePremiumMessage changeMessage = new ChangePremiumMessage();
changeMessage.readFrom(dataInput);
String playerName = changeMessage.getPlayerName();
boolean isSourceInvoker = changeMessage.isSourceInvoker();
onChangeMessage(fromPlayer, changeMessage.shouldEnable(), playerName, isSourceInvoker);
}
}
private void readMessage(ProxiedPlayer forPlayer, byte[] data) {
private void onChangeMessage(ProxiedPlayer fromPlayer, boolean shouldEnable, String playerName, boolean isSourceInvoker) {
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());
if (shouldEnable) {
if (!isSourceInvoker) {
// fromPlayer is not the target player
activePremiumLogin(fromPlayer, playerName, false);
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();
if (plugin.getCore().getConfig().getBoolean("premium-confirm", true)) {
ConfirmationState state = plugin.getCore().getPendingConfirms().get(playerName);
if (state == null) {
// no pending confirmation
core.sendLocaleMessage("premium-confirm", fromPlayer);
core.getPendingConfirms().put(playerName, ConfirmationState.REQUIRE_RELOGIN);
} else if (state == ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN) {
// player logged in successful using premium authentication
activePremiumLogin(fromPlayer, playerName, true);
}
} else {
activePremiumLogin(fromPlayer, playerName, true);
}
} else {
Runnable task = new AsyncToggleMessage(core, fromPlayer, playerName, false, isSourceInvoker);
plugin.getScheduler().runAsync(task);
}
}
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, false, isPlayerSender);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
}
private void activePremiumLogin(ProxiedPlayer fromPlayer, String playerName, boolean isSourceInvoker) {
plugin.getCore().getPendingConfirms().remove(playerName);
Runnable task = new AsyncToggleMessage(plugin.getCore(), fromPlayer, playerName, true, isSourceInvoker);
plugin.getScheduler().runAsync(task);
}
private void onSuccessMessage(ProxiedPlayer forPlayer) {
@@ -84,13 +111,16 @@ public class PluginMessageListener implements Listener {
//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();
StoredProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
playerProfile.setId(loginSession.getUuid());
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
}
}
}

View File

@@ -1,26 +1,28 @@
package com.github.games647.fastlogin.bungee.tasks;
package com.github.games647.fastlogin.bungee.task;
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.bungee.event.BungeeFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
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.api.event.PreLoginEvent;
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 PreLoginEvent preLoginEvent;
private final PendingConnection connection;
public AsyncPremiumCheck(FastLoginBungee plugin, AsyncEvent<?> preLoginEvent, PendingConnection connection) {
public AsyncPremiumCheck(FastLoginBungee plugin, PreLoginEvent preLoginEvent, PendingConnection connection) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
@@ -35,14 +37,22 @@ public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, CommandSend
InitialHandler initialHandler = (InitialHandler) connection;
String username = initialHandler.getLoginRequest().getData();
try {
super.onLogin(username, new BungeeLoginSource(connection));
super.onLogin(username, new BungeeLoginSource(connection, preLoginEvent));
} finally {
preLoginEvent.completeIntent(plugin);
}
}
@Override
public void requestPremiumLogin(BungeeLoginSource source, PlayerProfile profile, String username, boolean registered) {
public FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, BungeeLoginSource source,
StoredProfile profile) {
return plugin.getProxy().getPluginManager()
.callEvent(new BungeeFastLoginPreLoginEvent(username, source, profile));
}
@Override
public void requestPremiumLogin(BungeeLoginSource source, StoredProfile profile,
String username, boolean registered) {
source.setOnlineMode();
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, registered, profile));
@@ -51,7 +61,13 @@ public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, CommandSend
}
@Override
public void startCrackedSession(BungeeLoginSource source, PlayerProfile profile, String username) {
public void requestConfirmationLogin(BungeeLoginSource source, StoredProfile profile, String username) {
source.setOnlineMode();
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, profile));
}
@Override
public void startCrackedSession(BungeeLoginSource source, StoredProfile profile, String username) {
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, false, profile));
}
}

View File

@@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bungee.tasks;
package com.github.games647.fastlogin.bungee.task;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import net.md_5.bungee.api.CommandSender;
@@ -36,21 +36,21 @@ public class AsyncToggleMessage implements Runnable {
}
private void turnOffPremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
StoredProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
//existing player is already cracked
if (playerProfile.getUserId() != -1 && !playerProfile.isPremium()) {
if (playerProfile.isSaved() && !playerProfile.isPremium()) {
sendMessage("not-premium");
return;
}
playerProfile.setPremium(false);
playerProfile.setUuid(null);
playerProfile.setId(null);
core.getStorage().save(playerProfile);
sendMessage("remove-premium");
}
private void activatePremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
StoredProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
if (playerProfile.isPremium()) {
sendMessage("already-exists");
return;

View File

@@ -1,36 +1,38 @@
package com.github.games647.fastlogin.bungee.tasks;
package com.github.games647.fastlogin.bungee.task;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.event.BungeeFastLoginAutoLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage.Type;
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 com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.ProxyServer;
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> {
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);
super(core, player, core.getPlugin().getSession().get(player.getPendingConnection()));
this.server = server;
}
@Override
public void run() {
PendingConnection pendingConnection = player.getPendingConnection();
session = core.getPlugin().getSession().get(pendingConnection);
if (session == null) {
return;
}
@@ -53,35 +55,28 @@ public class ForceLoginTask extends ForceLoginManagement<ProxiedPlayer, CommandS
}
@Override
public boolean forceRegister(ProxiedPlayer player) {
if (session.isAlreadyLogged()) {
return true;
}
public FastLoginAutoLoginEvent callFastLoginAutoLoginEvent(LoginSession session, StoredProfile profile) {
return core.getPlugin().getProxy().getPluginManager()
.callEvent(new BungeeFastLoginAutoLoginEvent(session, profile));
}
return super.forceRegister(player);
@Override
public boolean forceRegister(ProxiedPlayer player) {
return session.isAlreadyLogged() || super.forceRegister(player);
}
@Override
public void onForceActionSuccess(LoginSession session) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
//sub channel name
Type type = Type.LOGIN;
if (session.needsRegistration()) {
dataOutput.writeUTF("AUTO_REGISTER");
} else {
dataOutput.writeUTF("AUTO_LOGIN");
type = Type.REGISTER;
}
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
dataOutput.writeUTF(player.getName());
UUID proxyId = UUID.fromString(ProxyServer.getInstance().getConfig().getUuid());
ChannelMessage loginMessage = new LoginActionMessage(type, player.getName(), proxyId);
//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());
}
core.getPlugin().sendPluginMessage(server, loginMessage);
}
@Override

View File

@@ -5,8 +5,8 @@ name: ${project.parent.name}
# ${-} will be automatically replaced by Maven
main: ${project.groupId}.${project.artifactId}.${project.name}
version: ${project.version}-git${git.commit.id}
author: games647, http://github.com/games647/FastLogin/graphs/contributors
version: ${project.version}-${git.commit.id.abbrev}
author: games647, https://github.com/games647/FastLogin/graphs/contributors
softDepends:
# BungeeCord auth plugins

View File

@@ -1,11 +1,11 @@
<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">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>
<version>1.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -14,26 +14,69 @@
<name>FastLoginCore</name>
<repositories>
<repository>
<id>luck-repo</id>
<url>https://ci.lucko.me/plugin/repository/everything</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<!-- Libraries that we shade into the project -->
<!--Database pooling-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.6.3</version>
<version>3.4.2</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>
<version>1.7.30</version>
</dependency>
<!-- snakeyaml is present in Bungee, Spigot, Cauldron and so we could use this independent implementation -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<version>1.12-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Common component for contacting the Mojang API-->
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>craftapi</artifactId>
<version>0.4</version>
</dependency>
<!-- APIs we can use because they are available in all platforms (Spigot, Bungee) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0.1</version>
<version>17.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
package com.github.games647.fastlogin.core;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
/**
* This limits the number of threads that are used at maximum. Thread creation can be very heavy for the CPU and
* context switching between threads too. However we need many threads for blocking HTTP and database calls.
* Nevertheless this number can be further limited, because the number of actually working database threads
* is limited by the size of our database pool. The goal is to separate concerns into processing and blocking only
* threads.
*/
public class AsyncScheduler {
private static final int MAX_CAPACITY = 1024;
//todo: single thread for delaying and scheduling tasks
private final Logger logger;
// 30 threads are still too many - the optimal solution is to separate into processing and blocking threads
// where processing threads could only be max number of cores while blocking threads could be minimized using
// non-blocking I/O and a single event executor
private final ExecutorService processingPool;
/*
private final ExecutorService databaseExecutor = new ThreadPoolExecutor(1, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(MAX_CAPACITY));
*/
public AsyncScheduler(Logger logger, ThreadFactory threadFactory) {
this.logger = logger;
processingPool = new ThreadPoolExecutor(6, 32,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(MAX_CAPACITY), threadFactory);
}
/*
public <R> CompletableFuture<R> runDatabaseTask(Supplier<R> databaseTask) {
return CompletableFuture.supplyAsync(databaseTask, databaseExecutor)
.exceptionally(error -> {
logger.warn("Error occurred on thread pool", error);
return null;
})
// change context to the processing pool
.thenApplyAsync(r -> r, processingPool);
}
*/
public CompletableFuture<Void> runAsync(Runnable task) {
return CompletableFuture.runAsync(task, processingPool).exceptionally(error -> {
logger.warn("Error occurred on thread pool", error);
return null;
});
}
public void shutdown() {
MoreExecutors.shutdownAndAwaitTermination(processingPool, 1, TimeUnit.MINUTES);
//MoreExecutors.shutdownAndAwaitTermination(databaseExecutor, 1, TimeUnit.MINUTES);
}
}

View File

@@ -1,7 +1,7 @@
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.UUIDAdapter;
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;
@@ -10,256 +10,176 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import javax.sql.DataSource;
import static java.sql.Statement.RETURN_GENERATED_KEYS;
public class AuthStorage {
private static final String PREMIUM_TABLE = "premium";
private static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `Name`=? LIMIT 1";
private static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `UUID`=? LIMIT 1";
private static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE
+ "` (`UUID`, `Name`, `Premium`, `LastIp`) " + "VALUES (?, ?, ?, ?) ";
// limit not necessary here, because it's unique
private static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE
+ "` SET `UUID`=?, `Name`=?, `Premium`=?, `LastIp`=?, `LastLogin`=CURRENT_TIMESTAMP WHERE `UserID`=?";
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) {
public AuthStorage(FastLoginCore<?, ?, ?> core, String host, int port, String databasePath,
HikariConfig config, boolean useSSL) {
this.core = core;
HikariConfig config = new HikariConfig();
config.setUsername(user);
config.setPassword(pass);
config.setDriverClassName(driver);
config.setPoolName(core.getPlugin().getName());
//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());
config.setThreadFactory(platformThreadFactory);
}
databasePath = databasePath.replace("{pluginDir}", core.getPlugin().getDataFolder().getAbsolutePath());
String jdbcUrl = "jdbc:";
if (driver.contains("sqlite")) {
jdbcUrl += "sqlite" + "://" + databasePath;
if (config.getDriverClassName().contains("sqlite")) {
String pluginFolder = core.getPlugin().getPluginFolder().toAbsolutePath().toString();
databasePath = databasePath.replace("{pluginDir}", pluginFolder);
jdbcUrl += "sqlite://" + databasePath;
config.setConnectionTestQuery("SELECT 1");
config.setMaximumPoolSize(1);
} else {
jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath;
jdbcUrl += "mysql://" + host + ':' + port + '/' + databasePath;
// enable MySQL specific optimizations
// default prepStmtCacheSize 25 - amount of cached statements - enough for us
// default prepStmtCacheSqlLimit 256 - length of SQL - our queries are not longer
// disabled by default - will return the same prepared statement instance
config.addDataSourceProperty("cachePrepStmts", true);
// default false - available in newer versions caches the statements server-side
config.addDataSourceProperty("useServerPrepStmts", true);
}
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();
// choose surrogate PK(ID), because UUID can be null for offline players
// if UUID is always Premium UUID we would have to update offline player entries on insert
// name cannot be PK, because it can be changed for premium players
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`) "
+ ')';
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");
}
if (dataSource.getJdbcUrl().contains("sqlite")) {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
//todo: add unique uuid index usage
try (Connection con = dataSource.getConnection();
Statement createStmt = con.createStatement()) {
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");
public StoredProfile loadProfile(String name) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_NAME)
) {
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, "");
try (ResultSet resultSet = loadStmt.executeQuery()) {
return parseResult(resultSet).orElseGet(() -> new StoredProfile(null, name, false, ""));
}
} catch (SQLException sqlEx) {
core.getPlugin().getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
closeQuietly(loadStmt);
closeQuietly(resultSet);
core.getPlugin().getLog().error("Failed to query profile: {}", name, sqlEx);
}
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("-", ""));
public StoredProfile loadProfile(UUID uuid) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)) {
loadStmt.setString(1, UUIDAdapter.toMojangId(uuid));
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);
try (ResultSet resultSet = loadStmt.executeQuery()) {
return parseResult(resultSet).orElse(null);
}
} catch (SQLException sqlEx) {
core.getPlugin().getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx);
} finally {
closeQuietly(con);
closeQuietly(loadStmt);
closeQuietly(resultSet);
core.getPlugin().getLog().error("Failed to query profile: {}", uuid, sqlEx);
}
return null;
}
public boolean save(PlayerProfile playerProfile) {
Connection con = null;
PreparedStatement updateStmt = null;
PreparedStatement saveStmt = null;
private Optional<StoredProfile> parseResult(ResultSet resultSet) throws SQLException {
if (resultSet.next()) {
long userId = resultSet.getInt(1);
ResultSet generatedKeys = null;
try {
con = dataSource.getConnection();
UUID uuid = Optional.ofNullable(resultSet.getString(2)).map(UUIDAdapter::parseId).orElse(null);
UUID uuid = playerProfile.getUuid();
if (playerProfile.getUserId() == -1) {
saveStmt = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE
+ " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?) ", Statement.RETURN_GENERATED_KEYS);
String name = resultSet.getString(3);
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
Instant lastLogin = resultSet.getTimestamp(6).toInstant();
return Optional.of(new StoredProfile(userId, uuid, name, premium, lastIp, lastLogin));
}
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
}
return Optional.empty();
}
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
public void save(StoredProfile playerProfile) {
try (Connection con = dataSource.getConnection()) {
String uuid = playerProfile.getOptId().map(UUIDAdapter::toMojangId).orElse(null);
saveStmt.execute();
if (playerProfile.isSaved()) {
try (PreparedStatement saveStmt = con.prepareStatement(UPDATE_PROFILE)) {
saveStmt.setString(1, uuid);
saveStmt.setString(2, playerProfile.getName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
generatedKeys = saveStmt.getGeneratedKeys();
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setUserId(generatedKeys.getInt(1));
saveStmt.setLong(5, playerProfile.getRowId());
saveStmt.execute();
}
} else {
saveStmt = con.prepareStatement("UPDATE " + PREMIUM_TABLE
+ " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=CURRENT_TIMESTAMP WHERE UserID=?");
try (PreparedStatement saveStmt = con.prepareStatement(INSERT_PROFILE, RETURN_GENERATED_KEYS)) {
saveStmt.setString(1, uuid);
if (uuid == null) {
saveStmt.setString(1, null);
} else {
saveStmt.setString(1, uuid.toString().replace("-", ""));
saveStmt.setString(2, playerProfile.getName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
saveStmt.execute();
try (ResultSet generatedKeys = saveStmt.getGeneratedKeys()) {
if (generatedKeys != null && generatedKeys.next()) {
playerProfile.setRowId(generatedKeys.getInt(1));
}
}
}
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);
core.getPlugin().getLog().error("Failed to save playerProfile {}", playerProfile, ex);
}
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

@@ -1,69 +0,0 @@
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,66 @@
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.cache.SafeCacheBuilder;
import com.google.common.cache.CacheLoader;
import java.lang.reflect.Constructor;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.impl.JDK14LoggerAdapter;
public class CommonUtil {
private static final char COLOR_CHAR = '&';
private static final char TRANSLATED_CHAR = '§';
public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
SafeCacheBuilder<Object, Object> builder = SafeCacheBuilder.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 String translateColorCodes(String rawMessage) {
char[] chars = rawMessage.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
if (chars[i] == COLOR_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(chars[i + 1]) > -1) {
chars[i] = TRANSLATED_CHAR;
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
}
}
return new String(chars);
}
public static Logger createLoggerFromJDK(java.util.logging.Logger parent) {
try {
parent.setLevel(Level.ALL);
Class<JDK14LoggerAdapter> adapterClass = JDK14LoggerAdapter.class;
Constructor<JDK14LoggerAdapter> cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class);
cons.setAccessible(true);
return cons.newInstance(parent);
} catch (ReflectiveOperationException reflectEx) {
parent.log(Level.WARNING, "Cannot create slf4j logging adapter", reflectEx);
parent.log(Level.WARNING, "Creating logger instance manually...");
return LoggerFactory.getLogger(parent.getName());
}
}
private CommonUtil() {
//Utility class
}
}

View File

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

@@ -0,0 +1,15 @@
package com.github.games647.fastlogin.core;
public enum ConfirmationState {
/**
* Require server login where we request onlinemode authentication
*/
REQUIRE_RELOGIN,
/**
* The command have to be invoked again to confirm that the player who joined through onlinemode knows
* the password of the cracked account
*/
REQUIRE_AUTH_PLUGIN_LOGIN
}

View File

@@ -1,76 +0,0 @@
package com.github.games647.fastlogin.core;
import java.util.UUID;
public class PlayerProfile {
private 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(-1, uuid, playerName, premium, lastIp, System.currentTimeMillis());
}
public synchronized String getPlayerName() {
return playerName;
}
public synchronized void setPlayerName(String playerName) {
this.playerName = playerName;
}
public synchronized long getUserId() {
return userId;
}
public synchronized void setUserId(long generatedId) {
this.userId = generatedId;
}
public synchronized UUID getUuid() {
return uuid;
}
public synchronized void setUuid(UUID uuid) {
this.uuid = uuid;
}
public synchronized boolean isPremium() {
return premium;
}
public synchronized void setPremium(boolean premium) {
this.premium = premium;
}
public synchronized String getLastIp() {
return lastIp;
}
public synchronized void setLastIp(String lastIp) {
this.lastIp = lastIp;
}
public synchronized long getLastLogin() {
return lastLogin;
}
public synchronized void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
}

View File

@@ -0,0 +1,10 @@
package com.github.games647.fastlogin.core;
public enum PremiumStatus {
PREMIUM,
CRACKED,
UNKNOWN
}

View File

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

@@ -0,0 +1,92 @@
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.model.Profile;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
public class StoredProfile extends Profile {
private long rowId;
private boolean premium;
private String lastIp;
private Instant lastLogin;
public StoredProfile(long rowId, UUID uuid, String playerName, boolean premium, String lastIp, Instant lastLogin) {
super(uuid, playerName);
this.rowId = rowId;
this.premium = premium;
this.lastIp = lastIp;
this.lastLogin = lastLogin;
}
public StoredProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
this(-1, uuid, playerName, premium, lastIp, Instant.now());
}
public synchronized boolean isSaved() {
return rowId >= 0;
}
public synchronized void setPlayerName(String playerName) {
this.name = playerName;
}
public synchronized long getRowId() {
return rowId;
}
public synchronized void setRowId(long generatedId) {
this.rowId = generatedId;
}
// can be null
public synchronized UUID getId() {
return id;
}
public synchronized Optional<UUID> getOptId() {
return Optional.ofNullable(id);
}
public synchronized void setId(UUID uniqueId) {
this.id = uniqueId;
}
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 Instant getLastLogin() {
return lastLogin;
}
public synchronized void setLastLogin(Instant lastLogin) {
this.lastLogin = lastLogin;
}
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"rowId=" + rowId +
", premium=" + premium +
", lastIp='" + lastIp + '\'' +
", lastLogin=" + lastLogin +
"} " + super.toString();
}
}

View File

@@ -3,7 +3,8 @@ package com.github.games647.fastlogin.core.hooks;
/**
* Represents a supporting authentication plugin in BungeeCord and Bukkit/Spigot/... servers
*
* @param <P> either org.bukkit.entity.Player for Bukkit or net.md_5.bungee.api.connection.ProxiedPlayer for BungeeCord
* @param <P> either {@link org.bukkit.entity.Player} for Bukkit or {@link net.md_5.bungee.api.connection.ProxiedPlayer}
* for BungeeCord
*/
public interface AuthPlugin<P> {

View File

@@ -1,19 +1,24 @@
package com.github.games647.fastlogin.core.hooks;
import java.security.SecureRandom;
import java.util.Random;
import java.util.stream.IntStream;
public class DefaultPasswordGenerator<P> implements PasswordGenerator<P> {
private static final int PASSWORD_LENGTH = 8;
private static final char[] PASSWORD_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
private final Random random = new Random();
private final Random random = new SecureRandom();
@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)]);
}
IntStream.rangeClosed(1, PASSWORD_LENGTH)
.map(i -> random.nextInt(PASSWORD_CHARACTERS.length - 1))
.mapToObj(pos -> PASSWORD_CHARACTERS[pos])
.forEach(generatedPassword::append);
return generatedPassword.toString();
}

View File

@@ -0,0 +1,66 @@
package com.github.games647.fastlogin.core.message;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
public class ChangePremiumMessage implements ChannelMessage {
public static final String CHANGE_CHANNEL = "ch-st";
private String playerName;
private boolean willEnable;
private boolean isSourceInvoker;
public ChangePremiumMessage(String playerName, boolean willEnable, boolean isSourceInvoker) {
this.playerName = playerName;
this.willEnable = willEnable;
this.isSourceInvoker = isSourceInvoker;
}
public ChangePremiumMessage() {
//reading from
}
public String getPlayerName() {
return playerName;
}
public boolean shouldEnable() {
return willEnable;
}
/**
* @return true if the player invoker = target
*/
public boolean isSourceInvoker() {
return isSourceInvoker;
}
@Override
public String getChannelName() {
return CHANGE_CHANNEL;
}
@Override
public void readFrom(ByteArrayDataInput input) {
willEnable = input.readBoolean();
playerName = input.readUTF();
isSourceInvoker = input.readBoolean();
}
@Override
public void writeTo(ByteArrayDataOutput output) {
output.writeBoolean(willEnable);
output.writeUTF(playerName);
output.writeBoolean(isSourceInvoker);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"playerName='" + playerName + '\'' +
", shouldEnable=" + willEnable +
", isSourceInvoker=" + isSourceInvoker +
'}';
}
}

View File

@@ -0,0 +1,13 @@
package com.github.games647.fastlogin.core.message;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
public interface ChannelMessage {
String getChannelName();
void readFrom(ByteArrayDataInput input);
void writeTo(ByteArrayDataOutput output);
}

View File

@@ -0,0 +1,85 @@
package com.github.games647.fastlogin.core.message;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import java.util.UUID;
public class LoginActionMessage implements ChannelMessage {
public static final String FORCE_CHANNEL = "force";
private Type type;
private String playerName;
private UUID proxyId;
public LoginActionMessage(Type type, String playerName, UUID proxyId) {
this.type = type;
this.playerName = playerName;
this.proxyId = proxyId;
}
public LoginActionMessage() {
//reading mode
}
public Type getType() {
return type;
}
public String getPlayerName() {
return playerName;
}
public UUID getProxyId() {
return proxyId;
}
@Override
public void readFrom(ByteArrayDataInput input) {
this.type = Type.values()[input.readInt()];
this.playerName = input.readUTF();
//bungeecord UUID
long mostSignificantBits = input.readLong();
long leastSignificantBits = input.readLong();
this.proxyId = new UUID(mostSignificantBits, leastSignificantBits);
}
@Override
public void writeTo(ByteArrayDataOutput output) {
output.writeInt(type.ordinal());
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
output.writeUTF(playerName);
//proxy identifier to check if it's a acceptable proxy
output.writeLong(proxyId.getMostSignificantBits());
output.writeLong(proxyId.getLeastSignificantBits());
}
@Override
public String getChannelName() {
return FORCE_CHANNEL;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"type='" + type + '\'' +
", playerName='" + playerName + '\'' +
", proxyId=" + proxyId +
'}';
}
public enum Type {
LOGIN,
REGISTER,
CRACKED
}
}

View File

@@ -0,0 +1,26 @@
package com.github.games647.fastlogin.core.message;
public class NamespaceKey {
private static final char SEPARATOR_CHAR = ':';
private final String namespace;
private final String key;
private final String combined;
public NamespaceKey(String namespace, String key) {
this.namespace = namespace.toLowerCase();
this.key = key.toLowerCase();
this.combined = this.namespace + SEPARATOR_CHAR + this.key;
}
public String getCombinedName() {
return combined;
}
public static String getCombined(String namespace, String key) {
return new NamespaceKey(namespace, key).combined;
}
}

View File

@@ -0,0 +1,29 @@
package com.github.games647.fastlogin.core.message;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
public class SuccessMessage implements ChannelMessage {
public static final String SUCCESS_CHANNEL = "succ";
@Override
public String getChannelName() {
return SUCCESS_CHANNEL;
}
@Override
public void readFrom(ByteArrayDataInput input) {
//empty
}
@Override
public void writeTo(ByteArrayDataOutput output) {
//empty
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "{}";
}
}

View File

@@ -1,76 +1,58 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
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.CommonUtil;
import com.github.games647.fastlogin.core.ConfirmationState;
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 com.google.common.net.HostAndPort;
import com.zaxxer.hikari.HikariConfig;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.HashSet;
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;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import org.slf4j.Logger;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
/**
* @param <P> Player class
* @param <P> GameProfile 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 Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> pendingLogin = CommonUtil.buildCache(5, -1);
private final ConcurrentMap<String, ConfirmationState> pendingConfirms = CommonUtil.buildCache(1, 1024);
private final T plugin;
private SharedConfig sharedConfig;
private MojangApiConnector apiConnector;
private final MojangResolver resolver = new MojangResolver();
private Configuration config;
private AuthStorage storage;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
@@ -83,55 +65,70 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
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();
config = loadFile("config.yml");
Configuration messages = loadFile("messages.yml");
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();
messages.getKeys()
.stream()
.filter(key -> messages.get(key) != null)
.collect(toMap(identity(), messages::get))
.forEach((key, message) -> {
String colored = CommonUtil.translateColorCodes((String) message);
if (!colored.isEmpty()) {
localeMessages.put(key, colored.replace("/newline", "\n"));
}
});
} 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);
}
plugin.getLog().error("Failed to load yaml files", ioEx);
}
Set<Proxy> proxies = config.getStringList("proxies")
.stream()
.map(HostAndPort::fromString)
.map(proxy -> new InetSocketAddress(proxy.getHostText(), proxy.getPort()))
.map(sa -> new Proxy(Type.HTTP, sa))
.collect(toSet());
Collection<InetAddress> addresses = new HashSet<>();
for (String localAddress : config.getStringList("ip-addresses")) {
try {
addresses.add(InetAddress.getByName(localAddress.replace('-', '.')));
} catch (UnknownHostException ex) {
plugin.getLog().error("IP-Address is unknown to us", 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);
resolver.setMaxNameRequests(config.getInt("mojang-request-limit"));
resolver.setProxySelector(new RotatingProxySelector(proxies));
resolver.setOutgoingAddresses(addresses);
}
public MojangApiConnector getApiConnector() {
return apiConnector;
private Configuration loadFile(String fileName) throws IOException {
ConfigurationProvider configProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);
Configuration defaults;
try (InputStream defaultStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
defaults = configProvider.load(defaultStream);
}
Path file = plugin.getPluginFolder().resolve(fileName);
Configuration config;
try (Reader reader = Files.newBufferedReader(file)) {
config = configProvider.load(reader, defaults);
}
//explicitly add keys here, because Configuration.getKeys doesn't return the keys from the default configuration
for (String key : defaults.getKeys()) {
config.set(key, config.get(key));
}
return config;
}
public MojangResolver getResolver() {
return resolver;
}
public AuthStorage getStorage() {
@@ -145,7 +142,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
public void sendLocaleMessage(String key, C receiver) {
String message = localeMessages.get(key);
if (message != null) {
plugin.sendMessage(receiver, message);
plugin.sendMultiLineMessage(receiver, message);
}
}
@@ -154,28 +151,49 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
}
public boolean setupDatabase() {
String driver = sharedConfig.get("driver");
String host = sharedConfig.get("host", "");
int port = sharedConfig.get("port", 3306);
String database = sharedConfig.get("database");
HikariConfig databaseConfig = new HikariConfig();
databaseConfig.setDriverClassName(config.getString("driver"));
if (!checkDriver(databaseConfig.getDriverClassName())) {
return false;
}
String user = sharedConfig.get("username", "");
String password = sharedConfig.get("password", "");
String host = config.get("host", "");
int port = config.get("port", 3306);
String database = config.getString("database");
boolean useSSL = sharedConfig.get("useSSL", false);
boolean useSSL = config.get("useSSL", false);
storage = new AuthStorage(this, driver, host, port, database, user, password, useSSL);
databaseConfig.setUsername(config.get("username", ""));
databaseConfig.setPassword(config.getString("password"));
databaseConfig.setConnectionTimeout(config.getInt("timeout", 30) * 1_000L);
databaseConfig.setMaxLifetime(config.getInt("lifetime", 30) * 1_000L);
storage = new AuthStorage(this, host, port, database, databaseConfig, useSSL);
try {
storage.createTables();
return true;
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to setup database. Disabling plugin...", ex);
plugin.getLog().warn("Failed to setup database. Disabling plugin...", ex);
return false;
}
}
public SharedConfig getConfig() {
return sharedConfig;
private boolean checkDriver(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException notFoundEx) {
Logger log = plugin.getLog();
log.warn("This driver {} is not supported on this platform", className);
log.warn("Please choose MySQL (Spigot+BungeeCord), SQLite (Spigot+Sponge) or MariaDB (Sponge)", notFoundEx);
}
return false;
}
public Configuration getConfig() {
return config;
}
public PasswordGenerator<P> getPasswordGenerator() {
@@ -186,11 +204,18 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
this.passwordGenerator = passwordGenerator;
}
/**
* @return list of player names that are currently during the login process and might fail and so could be used
* for second attempt logins
*/
public ConcurrentMap<String, Object> getPendingLogin() {
return pendingLogin;
}
public Collection<UUID> getPendingConfirms() {
/**
* @return list of player names that request onlinemode authentication but are not yet approved
*/
public ConcurrentMap<String, ConfirmationState> getPendingConfirms() {
return pendingConfirms;
}
@@ -203,28 +228,28 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
}
public void saveDefaultFile(String fileName) {
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdir();
}
Path dataFolder = plugin.getPluginFolder();
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);
try {
if (Files.notExists(dataFolder)) {
Files.createDirectories(dataFolder);
}
Path configFile = dataFolder.resolve(fileName);
if (Files.notExists(configFile)) {
try (InputStream defaultStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
Files.copy(defaultStream, configFile);
}
}
} catch (IOException ioExc) {
plugin.getLog().error("Cannot create plugin folder {}", dataFolder, ioExc);
}
}
public void close() {
plugin.getLog().info("Safely shutting down scheduler. This could take up to one minute.");
plugin.getScheduler().shutdown();
if (storage != null) {
storage.close();
}

View File

@@ -1,22 +1,21 @@
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.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.logging.Level;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
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 final L session;
protected L session;
public ForceLoginManagement(FastLoginCore<P, C, T> core, P player) {
public ForceLoginManagement(FastLoginCore<P, C, T> core, P player, L session) {
this.core = core;
this.player = player;
this.session = session;
}
@Override
@@ -26,14 +25,19 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
}
AuthStorage storage = core.getStorage();
PlayerProfile playerProfile = session.getProfile();
StoredProfile playerProfile = session.getProfile();
try {
if (isOnlineMode()) {
if (session.isConfirmationPending()) {
// do not perform force actions, because this confirmation login we have to verify that it's the
// owner of the account
return;
}
//premium player
AuthPlugin<P> authPlugin = core.getAuthPluginHook();
if (authPlugin == null) {
//maybe only bungeecord plugin
//maybe only bungeecord plugin without any auth plugins on Bungee
onForceActionSuccess(session);
} else {
boolean success = true;
@@ -43,7 +47,7 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
|| (core.getConfig().get("auto-register-unknown", false)
&& !authPlugin.isRegistered(playerName))) {
success = forceRegister(player);
} else {
} else if (!callFastLoginAutoLoginEvent(session, playerProfile).isCancelled()) {
success = forceLogin(player);
}
}
@@ -51,7 +55,7 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
if (success) {
//update only on success to prevent corrupt data
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
playerProfile.setId(session.getUuid());
playerProfile.setPremium(true);
storage.save(playerProfile);
}
@@ -61,17 +65,17 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
}
} else if (playerProfile != null) {
//cracked player
playerProfile.setUuid(null);
playerProfile.setId(null);
playerProfile.setPremium(false);
storage.save(playerProfile);
}
} catch (Exception ex) {
core.getPlugin().getLogger().log(Level.WARNING, "ERROR ON FORCE LOGIN", ex);
core.getPlugin().getLog().warn("ERROR ON FORCE LOGIN of {}", getName(player), ex);
}
}
public boolean forceRegister(P player) {
core.getPlugin().getLogger().log(Level.INFO, "Register player {0}", getName(player));
core.getPlugin().getLog().info("Register player {}", getName(player));
String generatedPassword = core.getPasswordGenerator().getRandomPassword(player);
boolean success = core.getAuthPluginHook().forceRegister(player, generatedPassword);
@@ -86,9 +90,9 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
}
public boolean forceLogin(P player) {
core.getPlugin().getLogger().log(Level.INFO, "Logging player {0} in", getName(player));
boolean success = core.getAuthPluginHook().forceLogin(player);
core.getPlugin().getLog().info("Logging player {} in", getName(player));
boolean success = core.getAuthPluginHook().forceLogin(player);
if (success) {
core.sendLocaleMessage("auto-login", player);
}
@@ -96,6 +100,8 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
return success;
}
public abstract FastLoginAutoLoginEvent callFastLoginAutoLoginEvent(LoginSession session, StoredProfile profile);
public abstract void onForceActionSuccess(LoginSession session);
public abstract String getName(P player);

View File

@@ -1,11 +1,15 @@
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.craftapi.model.Profile;
import com.github.games647.craftapi.resolver.RateLimitException;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.util.UUID;
import java.util.logging.Level;
import java.util.Optional;
import net.md_5.bungee.config.Configuration;
public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
@@ -18,32 +22,47 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
}
public void onLogin(String username, S source) {
PlayerProfile profile = core.getStorage().loadProfile(username);
StoredProfile profile = core.getStorage().loadProfile(username);
if (profile == null) {
return;
}
SharedConfig config = core.getConfig();
callFastLoginPreLoginEvent(username, source, profile);
Configuration config = core.getConfig();
String ip = source.getAddress().getAddress().getHostAddress();
profile.setLastIp(ip);
try {
if (profile.getUserId() == -1) {
if (profile.isSaved()) {
if (profile.isPremium()) {
requestPremiumLogin(source, profile, username, true);
} else {
ConfirmationState confirmationState = core.getPendingConfirms().get(username);
if (confirmationState == ConfirmationState.REQUIRE_RELOGIN) {
core.getPendingConfirms().put(username, ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN);
requestPremiumLogin(source, profile, username, true);
} else {
// cracked player, but wants to change to premium
startCrackedSession(source, profile, username);
}
}
} else {
if (core.getPendingLogin().remove(ip + username) != null && config.get("secondAttemptCracked", false)) {
core.getPlugin().getLogger().log(Level.INFO, "Second attempt login -> cracked {0}", username);
core.getPlugin().getLog().info("Second attempt login -> cracked {}", username);
//first login request failed so make a cracked session
startCrackedSession(source, profile, username);
return;
}
UUID premiumUUID = null;
Optional<Profile> premiumUUID = Optional.empty();
if (config.get("nameChangeCheck", false) || config.get("autoRegister", false)) {
premiumUUID = core.getApiConnector().getPremiumUUID(username);
premiumUUID = core.getResolver().findProfile(username);
}
if (premiumUUID == null
|| (!checkNameChange(source, username, premiumUUID)
if (!premiumUUID.isPresent()
|| (!checkNameChange(source, username, premiumUUID.get())
&& !checkPremiumName(source, username, profile))) {
//nothing detected the player as premium -> start a cracked session
if (core.getConfig().get("switchMode", false)) {
@@ -53,18 +72,19 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
startCrackedSession(source, profile, username);
}
} else if (profile.isPremium()) {
requestPremiumLogin(source, profile, username, true);
} else {
startCrackedSession(source, profile, username);
}
} catch (RateLimitException rateLimitEx) {
core.getPlugin().getLog().error("Mojang's rate limit reached for {}. The public IPv4 address of this" +
" server issued more than 600 Name -> UUID requests within 10 minutes. After those 10" +
" minutes we can make requests again.", username);
} catch (Exception ex) {
core.getPlugin().getLogger().log(Level.SEVERE, "Failed to check premium state", ex);
core.getPlugin().getLog().error("Failed to check premium state for {}", username, ex);
core.getPlugin().getLog().error("Failed to check premium state of {}", username, 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);
private boolean checkPremiumName(S source, String username, StoredProfile profile) throws Exception {
core.getPlugin().getLog().info("GameProfile {} uses a premium username", username);
if (core.getConfig().get("autoRegister", false) && (authHook == null || !authHook.isRegistered(username))) {
requestPremiumLogin(source, profile, username, false);
return true;
@@ -73,18 +93,18 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
return false;
}
private boolean checkNameChange(S source, String username, UUID premiumUUID) {
private boolean checkNameChange(S source, String username, Profile profile) {
//user not exists in the db
if (core.getConfig().get("nameChangeCheck", false)) {
PlayerProfile profile = core.getStorage().loadProfile(premiumUUID);
if (profile != null) {
StoredProfile storedProfile = core.getStorage().loadProfile(profile.getId());
if (storedProfile != null) {
//uuid exists in the database
core.getPlugin().getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID);
core.getPlugin().getLog().info("GameProfile {} changed it's username", profile);
//update the username to the new one in the database
profile.setPlayerName(username);
storedProfile.setPlayerName(username);
requestPremiumLogin(source, profile, username, false);
requestPremiumLogin(source, storedProfile, username, false);
return true;
}
}
@@ -92,7 +112,11 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
return false;
}
public abstract void requestPremiumLogin(S source, PlayerProfile profile, String username, boolean registered);
public abstract FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, S source, StoredProfile profile);
public abstract void startCrackedSession(S source, PlayerProfile profile, String username);
public abstract void requestPremiumLogin(S source, StoredProfile profile, String username, boolean registered);
public abstract void requestConfirmationLogin(S source, StoredProfile profile, String username);
public abstract void startCrackedSession(S source, StoredProfile profile, String username);
}

View File

@@ -1,24 +1,31 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import java.util.UUID;
public abstract class LoginSession {
private final String username;
private final PlayerProfile profile;
private final StoredProfile profile;
private UUID uuid;
protected boolean registered;
public LoginSession(String username, boolean registered, PlayerProfile profile) {
protected boolean confirmationLogin;
public LoginSession(String username, boolean registered, StoredProfile profile) {
this.username = username;
this.registered = registered;
this.profile = profile;
}
public LoginSession(String username, StoredProfile profile) {
this(username, true, profile);
this.confirmationLogin = true;
}
public String getUsername() {
return username;
}
@@ -32,7 +39,7 @@ public abstract class LoginSession {
return !registered;
}
public PlayerProfile getProfile() {
public StoredProfile getProfile() {
return profile;
}
@@ -53,4 +60,18 @@ public abstract class LoginSession {
public synchronized void setUuid(UUID uuid) {
this.uuid = uuid;
}
public synchronized boolean isConfirmationPending() {
return confirmationLogin;
}
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"username='" + username + '\'' +
", profile=" + profile +
", uuid=" + uuid +
", registered=" + registered +
'}';
}
}

View File

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

@@ -1,28 +1,37 @@
package com.github.games647.fastlogin.core.shared;
import java.io.File;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import com.github.games647.fastlogin.core.AsyncScheduler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.nio.file.Path;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;
import org.slf4j.Logger;
public interface PlatformPlugin<C> {
String getName();
File getDataFolder();
Path getPluginFolder();
Logger getLogger();
Map<String, Object> loadYamlFile(Reader reader);
Logger getLog();
void sendMessage(C receiver, String message);
ThreadFactory getThreadFactory();
AsyncScheduler getScheduler();
String translateColorCodes(char colorChar, String rawMessage);
default void sendMultiLineMessage(C receiver, String message) {
for (String line : message.split("%nl%")) {
sendMessage(receiver, line);
}
}
MojangApiConnector makeApiConnector(Logger logger, List<String> addresses, int requests
, Map<String, Integer> proxies);
default ThreadFactory getThreadFactory() {
return new ThreadFactoryBuilder()
.setNameFormat(getName() + " Pool Thread #%1$d")
// Hikari create daemons by default and we could daemon threads for our own scheduler too
// because we safely shutdown
.setDaemon(true)
.build();
}
}

View File

@@ -0,0 +1,9 @@
package com.github.games647.fastlogin.core.shared.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
public interface FastLoginAutoLoginEvent extends FastLoginCancellableEvent {
LoginSession getSession();
StoredProfile getProfile();
}

View File

@@ -0,0 +1,7 @@
package com.github.games647.fastlogin.core.shared.event;
public interface FastLoginCancellableEvent {
boolean isCancelled();
void setCancelled(boolean cancelled);
}

View File

@@ -0,0 +1,11 @@
package com.github.games647.fastlogin.core.shared.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSource;
public interface FastLoginPreLoginEvent {
String getUsername();
LoginSource getSource();
StoredProfile getProfile();
}

View File

@@ -62,8 +62,8 @@ premiumUuid: false
# #### 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.
# GameProfile logins as cracked until the player invoked the command /premium. Then we could override the existing
# database record.
#
# #### Case 2
#
@@ -82,7 +82,7 @@ premiumUuid: false
#
# 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
# 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.
#
@@ -95,7 +95,7 @@ premiumUuid: false
# 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
# **Limitation from autoRegister**: New offline players who uses the username of an existing Minecraft cannot join the
# server.
nameChangeCheck: false
@@ -115,11 +115,11 @@ nameChangeCheck: false
# 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
# Players have to rejoin to verify that they can join through onlinemode authentication.
# After that they have to confirm again using the /premium command that they are also the owner of the auth plugin
# account.
# If the onlinemode authentication fails or the player waits to long, it will fallback to offlinemode authentication.
premium-confirm: 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
@@ -128,8 +128,8 @@ premium-warning: true
# 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
# 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:
@@ -138,7 +138,7 @@ premium-warning: true
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
# 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.
@@ -154,24 +154,29 @@ auto-register-unknown: false
# 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
# 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
driver: 'org.sqlite.JDBC'
# File location
database: '{pluginDir}/FastLogin.db'
# MySQL/MariaDB
#driver: com.mysql.jdbc.Driver
#host: localhost
# If you want to enable it uncomment only the lines below this not this line.
#driver: 'com.mysql.jdbc.Driver'
#host: '127.0.0.1'
#port: 3306
#database: fastlogin
#username: myUser
#password: myPassword
#database: 'fastlogin'
#username: 'myUser'
#password: 'myPassword'
# Advanced Connection Pool settings in seconds
#timeout: 30
#lifetime: 30
# It's strongly recommended to enable SSL and setup a SSL certificate if the MySQL server isn't running on the same
# machine

View File

@@ -5,7 +5,7 @@
# 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:
# 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 '
@@ -22,27 +22,27 @@
# ========= 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'
switch-kick-message: '&4Only paid Minecraft whitelisted accounts are allowed to join this server'
# Player activated premium login in order to skip offline authentication
# GameProfile 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
# GameProfile 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
# GameProfile is already set be a paid account
already-exists: '&4You are already on the premium list'
# Player is already set be a paid account
# GameProfile is already set be a paid account
already-exists-other: '&4Player is already on the premium list'
# Player was changed to be cracked
# GameProfile was changed to be cracked
remove-premium: '&2Removed from the list of premium players'
# Player is already set to be cracked
# GameProfile is already set to be cracked
not-premium: '&4You are not in the premium list'
# Player is already set to be cracked
# GameProfile 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
@@ -58,7 +58,7 @@ auto-login: '&2Auto logged in'
auto-register: '&2Auto registered with password: %password
You may want change it?'
# Player is not able to toggle the premium state of other players
# GameProfile 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.
@@ -67,7 +67,7 @@ no-console: '&4You are not a player. You cannot toggle the premium state for YOU
# 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...'
wait-on-proxy: '&6Sending request... (Do not forget to follow the BungeeCord setup guide)'
# 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.
@@ -86,12 +86,12 @@ 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
# 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'
# Premium confirmation message if the player tries to activate the command
premium-confirm: '&6Please relogin and type the command again to apply the changes.
If the request fails or you wait to long, it will fallback to offlinemode.'
# ========= Bungee/Waterfall only ================================

42
pom.xml
View File

@@ -1,5 +1,5 @@
<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">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.games647</groupId>
@@ -8,47 +8,51 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.10</version>
<inceptionYear>2015</inceptionYear>
<version>1.11-SNAPSHOT</version>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>
Automatically logins premium (paid accounts) player on a offline mode server
Automatically login premium (paid accounts) player on a offline mode server
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--Possibility to deploy directly to the plugins folder-->
<outputDir>${basedir}/target</outputDir>
<!-- Set default for non-git clones -->
<git.commit.id>Unknown</git.commit.id>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<modules>
<module>core</module>
<module>bukkit</module>
<module>bungee</module>
<module>universal</module>
</modules>
<!--Deployment configuration for the Maven repository-->
<distributionManagement>
<snapshotRepository>
<id>codemc-snapshots</id>
<url>https://repo.codemc.io/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-releases/</url>
</repository>
</distributionManagement>
<build>
<defaultGoal>install</defaultGoal>
<!--Just use the project name to replace an old version of the plugin if the user does only copy-paste-->
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version>
<version>4.0.0</version>
<configuration>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
</configuration>

View File

@@ -1,68 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.10</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>fastlogin-universal</artifactId>
<packaging>jar</packaging>
<name>FastLoginUniversal</name>
<build>
<defaultGoal>package</defaultGoal>
<finalName>${project.parent.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
<artifactSet>
<includes>
<include>${project.groupId}:*</include>
<include>com.zaxxer:HikariCP</include>
<include>org.slf4j:*</include>
</includes>
</artifactSet>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.bukkit</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>fastlogin.bungee</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>