├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── HEADER.txt
├── LICENSE
├── README.md
├── VERSION
├── build.gradle
├── config
├── checkstyle
│ ├── checkstyle.xml
│ └── suppressions.xml
└── spotbugs
│ └── suppressions.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
└── main
├── java
└── net
│ └── elytrium
│ └── limboauth
│ ├── LimboAuth.java
│ ├── Settings.java
│ ├── backend
│ ├── Endpoint.java
│ └── type
│ │ ├── LongDatabaseEndpoint.java
│ │ ├── LongEndpoint.java
│ │ ├── StringDatabaseEndpoint.java
│ │ ├── StringEndpoint.java
│ │ └── UnknownEndpoint.java
│ ├── command
│ ├── ChangePasswordCommand.java
│ ├── CommandPermissionState.java
│ ├── DestroySessionCommand.java
│ ├── ForceChangePasswordCommand.java
│ ├── ForceLoginCommand.java
│ ├── ForceRegisterCommand.java
│ ├── ForceUnregisterCommand.java
│ ├── LimboAuthCommand.java
│ ├── PremiumCommand.java
│ ├── RatelimitedCommand.java
│ ├── TotpCommand.java
│ └── UnregisterCommand.java
│ ├── dependencies
│ ├── BaseLibrary.java
│ ├── DatabaseLibrary.java
│ ├── IsolatedClassLoader.java
│ └── IsolatedDriver.java
│ ├── event
│ ├── AuthPluginReloadEvent.java
│ ├── AuthUnregisterEvent.java
│ ├── ChangePasswordEvent.java
│ ├── PostAuthorizationEvent.java
│ ├── PostEvent.java
│ ├── PostRegisterEvent.java
│ ├── PreAuthorizationEvent.java
│ ├── PreEvent.java
│ ├── PreRegisterEvent.java
│ └── TaskEvent.java
│ ├── floodgate
│ └── FloodgateApiHolder.java
│ ├── handler
│ └── AuthSessionHandler.java
│ ├── listener
│ ├── AuthListener.java
│ └── BackendEndpointsListener.java
│ ├── migration
│ ├── MigrationHash.java
│ └── MigrationHashVerifier.java
│ └── model
│ ├── RegisteredPlayer.java
│ └── SQLRuntimeException.java
├── resources
└── unsafe_passwords.txt
└── templates
└── net
└── elytrium
└── limboauth
└── BuildConstants.java
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report the problem that has occurred in our project.
4 | title: "[BUG] "
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of the bug.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 | 1. Set '...' in config to '...'
15 | 2. Do in game '....'
16 | 3. See error
17 |
18 | **Expected behavior**
19 | A clear and concise description of what you expected to happen.
20 |
21 | **Screenshots**
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | **Server Info (please complete the following information):**
25 | - All Limbo plugins versions:
26 | - [e.g. LimboAPI 1.0.4-SNAPSHOT, downloaded from https://github.com/Elytrium/LimboAPI/actions/runs/xxxxxx]
27 | - [e.g. LimboAuth 1.0.3-rc3, downloaded from https://github.com/Elytrium/LimboAPI/actions/runs/xxxxxx]
28 |
29 | - /velocity dump link [e.g. https://dump.velocitypowered.com/abcdef.json]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea to improve our project.
4 | title: "[ENHANCEMENT] "
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | **Describe the feature you'd like to have implemented**
10 | A clear and concise description of what you want to be added.
11 |
12 | **Is your feature request related to an existing problem? Please describe.**
13 | A clear and concise description of what the problem is.
14 |
15 | **Additional context**
16 | Add any other context or screenshots about the feature request here.
17 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Java CI with Gradle
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4.2.2
17 | - name: Set up JDK
18 | uses: actions/setup-java@v4.7.0
19 | with:
20 | distribution: adopt
21 | java-version: 17
22 | - name: Build LimboAuth
23 | run: ./gradlew build
24 | - name: Upload LimboAuth
25 | uses: actions/upload-artifact@v4.6.2
26 | with:
27 | name: LimboAuth
28 | path: "build/libs/limboauth*.jar"
29 | - uses: dev-drprasad/delete-tag-and-release@v0.2.1
30 | if: ${{ github.event_name == 'push' }}
31 | with:
32 | delete_release: true
33 | tag_name: dev-build
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | - name: Find git version
37 | id: git-version
38 | run: echo "id=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
39 | - name: Find correct JAR
40 | if: ${{ github.event_name == 'push' }}
41 | id: find-jar
42 | run: |
43 | output="$(find build/libs/ ! -name "*-javadoc.jar" ! -name "*-sources.jar" -type f -printf "%f\n")"
44 | echo "::set-output name=jarname::$output"
45 | - name: Release the build
46 | if: ${{ github.event_name == 'push' }}
47 | uses: ncipollo/release-action@v1
48 | with:
49 | artifacts: build/libs/${{ steps.find-jar.outputs.jarname }}
50 | body: ${{ join(github.event.commits.*.message, '\n') }}
51 | prerelease: true
52 | name: Dev-build ${{ steps.git-version.outputs.id }}
53 | tag: dev-build
54 | - name: Upload to Modrinth
55 | if: ${{ github.event_name == 'push' }}
56 | uses: RubixDev/modrinth-upload@v1.0.0
57 | with:
58 | token: ${{ secrets.MODRINTH_TOKEN }}
59 | file_path: build/libs/${{ steps.find-jar.outputs.jarname }}
60 | name: Dev-build ${{ steps.git-version.outputs.id }}
61 | version: ${{ steps.git-version.outputs.id }}
62 | changelog: ${{ join(github.event.commits.*.message, '\n') }}
63 | relations: TZOteSf2:required
64 | game_versions: 1.7.2
65 | release_type: beta
66 | loaders: velocity
67 | featured: false
68 | project_id: 4iChqdl8
69 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Java CI with Gradle
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4.2.2
13 | - name: Set up JDK
14 | uses: actions/setup-java@v4.7.0
15 | with:
16 | distribution: adopt
17 | java-version: 17
18 | - name: Build LimboAuth
19 | run: ./gradlew build
20 | - name: Upload LimboAuth
21 | uses: actions/upload-artifact@v4.6.2
22 | with:
23 | name: LimboAuth
24 | path: "build/libs/limboauth*.jar"
25 | - name: Find correct JAR
26 | id: find-jar
27 | run: |
28 | output="$(find build/libs/ ! -name "*-javadoc.jar" ! -name "*-sources.jar" -type f -printf "%f\n")"
29 | echo "::set-output name=jarname::$output"
30 | - name: Upload to the GitHub release
31 | uses: actions/upload-release-asset@v1
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | with:
35 | upload_url: ${{ github.event.release.upload_url }}
36 | asset_path: build/libs/${{ steps.find-jar.outputs.jarname }}
37 | asset_name: ${{ steps.find-jar.outputs.jarname }}
38 | asset_content_type: application/java-archive
39 | - name: Upload to Modrinth
40 | uses: RubixDev/modrinth-upload@v1.0.0
41 | with:
42 | token: ${{ secrets.MODRINTH_TOKEN }}
43 | file_path: build/libs/${{ steps.find-jar.outputs.jarname }}
44 | name: Release ${{ github.event.release.tag_name }}
45 | version: ${{ github.event.release.tag_name }}
46 | changelog: ${{ github.event.release.body }}
47 | relations: TZOteSf2:required
48 | game_versions: 1.7.2
49 | release_type: release
50 | loaders: velocity
51 | featured: true
52 | project_id: 4iChqdl8
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IntelliJ user-specific stuff
2 | .idea/
3 | *.iml
4 |
5 | # Compiled class file
6 | *.class
7 |
8 | # Log file
9 | *.log
10 |
11 | # Package Files
12 | *.jar
13 | *.zip
14 | *.tar.gz
15 | *.rar
16 |
17 | # Virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
18 | hs_err_pid*
19 |
20 | *~
21 |
22 | # Temporary files which can be created if a process still has a handle open of a deleted file
23 | .fuse_hidden*
24 |
25 | # KDE directory preferences
26 | .directory
27 |
28 | # Linux trash folder which might appear on any partition or disk
29 | .Trash-*
30 |
31 | # .nfs files are created when an open file is removed but is still being accessed
32 | .nfs*
33 |
34 | # General
35 | .DS_Store
36 | .AppleDouble
37 | .LSOverride
38 |
39 | # Icon must end with two \r
40 | Icon
41 |
42 | # Thumbnails
43 | ._*
44 |
45 | # Files that might appear in the root of a volume
46 | .DocumentRevisions-V100
47 | .fseventsd
48 | .Spotlight-V100
49 | .TemporaryItems
50 | .Trashes
51 | .VolumeIcon.icns
52 | .com.apple.timemachine.donotpresent
53 |
54 | # Directories potentially created on remote AFP share
55 | .AppleDB
56 | .AppleDesktop
57 | Network Trash Folder
58 | Temporary Items
59 | .apdisk
60 |
61 | # Windows thumbnail cache files
62 | Thumbs.db
63 | Thumbs.db:encryptable
64 | ehthumbs.db
65 | ehthumbs_vista.db
66 |
67 | # Dump file
68 | *.stackdump
69 |
70 | # Folder config file
71 | [Dd]esktop.ini
72 |
73 | # Recycle Bin used on file shares
74 | $RECYCLE.BIN/
75 |
76 | # Windows Installer files
77 | *.cab
78 | *.msi
79 | *.msix
80 | *.msm
81 | *.msp
82 |
83 | # Windows shortcuts
84 | *.lnk
85 |
86 | # Gradle
87 | .gradle
88 | build/
89 |
90 | # Gradle Patch
91 | **/build/
92 |
93 | # Common working directory
94 | run/
95 |
96 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
97 | !gradle-wrapper.jar
98 |
--------------------------------------------------------------------------------
/HEADER.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2021 - 2025 Elytrium
2 |
3 | This program is free software: you can redistribute it and/or modify
4 | it under the terms of the GNU Affero General Public License as published by
5 | the Free Software Foundation, either version 3 of the License, or
6 | (at your option) any later version.
7 |
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU Affero General Public License for more details.
12 |
13 | You should have received a copy of the GNU Affero General Public License
14 | along with this program. If not, see .
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # LimboAuth
4 |
5 | [](https://ely.su/discord)
6 | 
7 | 
8 | [](https://bstats.org/plugin/velocity/LimboAuth/13700)
9 | [](https://bstats.org/plugin/velocity/LimboAuth/13700)
10 |
11 | Auth System built in virtual server (Limbo). \
12 | [Описание и обсуждение на русском языке (spigotmc.ru)](https://spigotmc.ru/resources/limboapi-limboauth-limbofilter-virtualnye-servera-dlja-velocity.715/) \
13 | [Описание и обсуждение на русском языке (rubukkit.org)](http://rubukkit.org/threads/limboapi-limboauth-limbofilter-virtualnye-servera-dlja-velocity.177904/)
14 |
15 | Test server: [``ely.su``](https://hotmc.ru/minecraft-server-203216)
16 |
17 | ## See also
18 |
19 | - [LimboFilter](https://github.com/Elytrium/LimboFilter) - Most powerful bot filtering solution for Minecraft proxies. Built with [LimboAPI](https://github.com/Elytrium/LimboAPI).
20 | - [LimboAPI](https://github.com/Elytrium/LimboAPI) - Library for sending players to virtual servers (called limbo)
21 |
22 | ## Features of LimboAuth
23 |
24 | - Supports [H2](https://www.h2database.com/html/main.html), [MySQL](https://www.mysql.com/about/), [PostgreSQL](https://www.postgresql.org/about/) [databases](https://en.wikipedia.org/wiki/Database);
25 | - [Geyser](https://wiki.geysermc.org) [Floodgate](https://wiki.geysermc.org/floodgate/) support;
26 | - Hybrid ([Floodgate](https://wiki.geysermc.org/floodgate/)/Online/Offline) mode support;
27 | - Uses [BCrypt](https://en.wikipedia.org/wiki/Bcrypt) - the best [hashing algorithm](https://en.wikipedia.org/wiki/Cryptographic_hash_function) for password;
28 | - Ability to migrate from [AuthMe](https://www.spigotmc.org/resources/authmereloaded.6269/)-alike plugins;
29 | - Ability to block weak passwords;
30 | - [TOTP](https://en.wikipedia.org/wiki/Time-based_one-time_password) [2FA](https://en.wikipedia.org/wiki/Help:Two-factor_authentication) support;
31 | - Ability to set [UUID](https://minecraft.wiki/w/Universally_unique_identifier) from [database](https://en.wikipedia.org/wiki/Database);
32 | - Highly customisable config - you can change all the messages the plugin sends, or just disable them;
33 | - [MCEdit](https://www.mcedit.net/about.html) schematic world loading;
34 | - And more...
35 |
36 | ## Commands and permissions
37 |
38 | ### Player
39 |
40 | - ***limboauth.commands.destroysession* | /destroysession** - Destroy Account Auth Session Command
41 | - ***limboauth.commands.premium* | /license or /premium** - Command Makes Account Premium
42 | - ***limboauth.commands.unregister* | /unregister** - Unregister Account Command
43 | - ***limboauth.commands.changepassword* | /changepassword** - Change Account Password Command
44 | - ***limboauth.commands.totp* | /totp** - 2FA Management Command
45 | - ***limboauth.commands.***\* - Gives All Player Permissions
46 |
47 | ### Admin
48 |
49 | - ***limboauth.admin.forceunregister* | /forceunregister** - Force Unregister Account Command
50 | - ***limboauth.admin.forcechangepassword* | /forcechangepassword** - Force Change Account Password Command
51 | - ***limboauth.admin.forceregister* | /forceregister** - Force Registration Account Command
52 | - ***limboauth.admin.forcelogin* | /forcelogin** - Force Login Account Command
53 | - ***limboauth.admin.reload* | /lauth reload** - Reload Plugin Command
54 | - ***limboauth.admin.***\* - Gives All Admin Permissions
55 |
56 | ## Donation
57 |
58 | Your donations are really appreciated. Donations wallets/links/cards:
59 |
60 | - MasterCard Debit Card (Tinkoff Bank): ``5536 9140 0599 1975``
61 | - Qiwi Wallet: ``PFORG`` or [this link](https://my.qiwi.com/form/Petr-YSpyiLt9c6)
62 | - YooMoney Wallet: ``4100 1721 8467 044`` or [this link](https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=Donation&targets-hint=&default-sum=&button-text=11&payment-type-choice=on&mobile-payment-type-choice=on&hint=&successURL=&quickpay=shop&account=410017218467044)
63 | - Monero (XMR): 86VQyCz68ApebfFgrzRFAuYLdvd3qG8iT9RHcru9moQkJR9W2Q89Gt3ecFQcFu6wncGwGJsMS9E8Bfr9brztBNbX7Q2rfYS
64 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 1.1.14
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | //file:noinspection GroovyAssignabilityCheck
2 |
3 | plugins {
4 | id("java")
5 | id("checkstyle")
6 | id("maven-publish")
7 | id("com.github.spotbugs").version("6.1.7")
8 | id("org.cadixdev.licenser").version("0.6.1")
9 | id("com.gradleup.shadow").version("8.3.6")
10 | }
11 |
12 | setGroup("net.elytrium")
13 | setVersion("1.1.14")
14 |
15 | java {
16 | setSourceCompatibility(JavaVersion.VERSION_17)
17 | setTargetCompatibility(JavaVersion.VERSION_17)
18 | }
19 |
20 | compileJava {
21 | getOptions().setEncoding("UTF-8")
22 | }
23 |
24 | repositories {
25 | mavenCentral()
26 |
27 | maven {
28 | setName("elytrium-repo")
29 | setUrl("https://maven.elytrium.net/repo/")
30 | }
31 | maven {
32 | setName("papermc-repo")
33 | setUrl("https://repo.papermc.io/repository/maven-public/")
34 | }
35 | maven {
36 | setName("opencollab-snapshot")
37 | setUrl("https://repo.opencollab.dev/maven-snapshots")
38 | }
39 | }
40 |
41 | dependencies {
42 | compileOnly("net.elytrium.limboapi:api:$limboapiVersion")
43 | compileOnly("net.elytrium.commons:config:$elytriumCommonsVersion")
44 | compileOnly("net.elytrium.commons:utils:$elytriumCommonsVersion")
45 | compileOnly("net.elytrium.commons:velocity:$elytriumCommonsVersion")
46 | compileOnly("net.elytrium.commons:kyori:$elytriumCommonsVersion")
47 |
48 | compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
49 | annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
50 | compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion") // From Elytrium Repo.
51 |
52 | // Needs for some velocity methods.
53 | compileOnly("io.netty:netty-codec:$nettyVersion")
54 | compileOnly("io.netty:netty-handler:$nettyVersion")
55 |
56 | compileOnly("org.geysermc.floodgate:api:$floodgateVersion")
57 |
58 | implementation("at.favre.lib:bcrypt:0.9.0")
59 | implementation("dev.samstevens.totp:totp:1.7.1")
60 |
61 | implementation("com.j256.ormlite:ormlite-jdbc:6.1")
62 | implementation("de.mkammerer:argon2-jvm-nolibs:2.11")
63 |
64 | implementation("io.whitfin:siphash:2.0.0")
65 |
66 | implementation("org.bstats:bstats-velocity:$bstatsVersion")
67 |
68 | compileOnly("com.github.spotbugs:spotbugs-annotations:$spotbugsVersion")
69 | }
70 |
71 | shadowJar {
72 | getArchiveClassifier().set("")
73 |
74 | exclude("META-INF/licenses/**")
75 | exclude("META-INF/maven/**")
76 | exclude("META-INF/versions/**")
77 | exclude("META-INF/AL2.0")
78 | exclude("META-INF/INFO_BIN")
79 | exclude("META-INF/INFO_SRC")
80 | exclude("META-INF/LGPL2.1")
81 | exclude("META-INF/LICENSE")
82 | exclude("META-INF/NOTICE")
83 | exclude("META-INF/README")
84 | exclude("META-INF/*.txt")
85 | exclude("google/protobuf/**")
86 | exclude("com/google/protobuf/**")
87 | exclude("com/j256/ormlite/**/*.txt")
88 | exclude("com/mysql/cj/x/**")
89 | exclude("com/mysql/cj/xdevapi/**")
90 | exclude("com/sun/jna/aix-ppc*/**")
91 | exclude("com/sun/jna/darwin-aarch64/**")
92 | exclude("com/sun/jna/freebsd-*/**")
93 | exclude("com/sun/jna/linux-arm*/**")
94 | exclude("com/sun/jna/linux-mips64el/**")
95 | exclude("com/sun/jna/linux-ppc*/**")
96 | exclude("com/sun/jna/linux-riscv64/**")
97 | exclude("com/sun/jna/linux-s390x/**")
98 | exclude("com/sun/jna/linux-x86/**")
99 | exclude("com/sun/jna/openbsd-*/**")
100 | exclude("com/sun/jna/sunos-*/**")
101 | exclude("com/sun/jna/win32-x86/**")
102 | exclude("org/apache/commons/codec/language/**")
103 | exclude("org/checkerframework/**")
104 | exclude("**/package-info.class")
105 |
106 | minimize()
107 |
108 | relocate("at.favre.lib", "net.elytrium.limboauth.thirdparty.at.favre.lib")
109 | relocate("com.j256.ormlite", "net.elytrium.limboauth.thirdparty.com.j256.ormlite")
110 | relocate("com.sun.jna", "net.elytrium.limboauth.thirdparty.com.sun.jna") {
111 | exclude("com.sun.jna.Native") // For compatibility with native methods.
112 | }
113 | relocate("de.mkammerer.argon2", "net.elytrium.limboauth.thirdparty.de.mkammerer.argon2")
114 | relocate("dev.samstevens.totp", "net.elytrium.limboauth.thirdparty.dev.samstevens.totp")
115 | relocate("org.apache.commons.codec", "net.elytrium.limboauth.thirdparty.org.apache.commons.codec")
116 | relocate("org.bstats", "net.elytrium.limboauth.thirdparty.org.bstats")
117 | relocate("net.elytrium.commons.velocity", "net.elytrium.limboapi.thirdparty.commons.velocity")
118 | relocate("net.elytrium.commons.kyori", "net.elytrium.limboapi.thirdparty.commons.kyori")
119 | relocate("net.elytrium.commons.config", "net.elytrium.limboapi.thirdparty.commons.config")
120 | }
121 |
122 | license {
123 | setHeader(file("HEADER.txt"))
124 | }
125 |
126 | checkstyle {
127 | setToolVersion("10.1")
128 | setConfigFile(file("${this.getRootDir()}/config/checkstyle/checkstyle.xml"))
129 | setConfigProperties("configDirectory": "${this.getRootDir()}/config/checkstyle")
130 |
131 | // The build should immediately fail if we have errors.
132 | setMaxErrors(0)
133 | setMaxWarnings(0)
134 | }
135 |
136 | spotbugsMain {
137 | setExcludeFilter(file("${this.getRootDir()}/config/spotbugs/suppressions.xml"))
138 |
139 | reports {
140 | html {
141 | getRequired().set(true)
142 | getOutputLocation().set(file("${this.getBuildDir()}/reports/spotbugs/main/spotbugs.html"))
143 | setStylesheet("fancy-hist.xsl")
144 | }
145 | }
146 | }
147 |
148 | task javadocJar(type: Jar) {
149 | getArchiveClassifier().set("javadoc")
150 | from(javadoc)
151 | }
152 |
153 | task sourcesJar(type: Jar) {
154 | getArchiveClassifier().set("sources")
155 | from(sourceSets.main.getAllSource())
156 | }
157 |
158 | publishing {
159 | repositories {
160 | maven {
161 | credentials {
162 | setUsername(System.getenv("PUBLISH_USERNAME"))
163 | setPassword(System.getenv("PUBLISH_PASSWORD"))
164 | }
165 |
166 | setName("elytrium-repo")
167 | setUrl("https://maven.elytrium.net/repo/")
168 | }
169 | }
170 |
171 | publications {
172 | maven(MavenPublication) {
173 | from(getComponents().java)
174 |
175 | artifact(javadocJar)
176 | artifact(sourcesJar)
177 | }
178 | }
179 | }
180 |
181 | javadoc {
182 | getOptions().setEncoding("UTF-8")
183 | getOptions().setCharSet("UTF-8")
184 | getOptions().setSource("17")
185 | getOptions().links("https://docs.oracle.com/en/java/javase/11/docs/api/")
186 |
187 | // Remove "undefined" from search paths when generating javadoc for a non-modular project. (JDK-8215291)
188 | if (JavaVersion.current() == JavaVersion.VERSION_11) {
189 | getOptions().addBooleanOption("-no-module-directories", true)
190 | }
191 | }
192 |
193 | artifacts {
194 | archives(javadocJar)
195 | archives(sourcesJar)
196 | }
197 |
198 | sourceSets.main.getJava().srcDir(
199 | getTasks().register("generateTemplates", Copy) {
200 | task -> {
201 | String version = getVersion().contains("-") ? "${getVersion()} (git-${getCurrentShortRevision()})" : getVersion()
202 | task.getInputs().properties("version": version)
203 | task.from(file("src/main/templates")).into(getLayout().getBuildDirectory().dir("generated/sources/templates"))
204 | task.expand("version": version)
205 | }
206 | }.map {
207 | it.getOutputs()
208 | }
209 | )
210 |
211 | assemble.dependsOn(shadowJar)
212 |
213 | String getCurrentShortRevision() {
214 | OutputStream outputStream = new ByteArrayOutputStream()
215 | exec {
216 | if (System.getProperty("os.name").toLowerCase().contains("win")) {
217 | commandLine("cmd", "/c", "git rev-parse --short HEAD")
218 | } else {
219 | commandLine("bash", "-c", "git rev-parse --short HEAD")
220 | }
221 |
222 | setStandardOutput(outputStream)
223 | }
224 |
225 | return outputStream.toString().trim()
226 | }
--------------------------------------------------------------------------------
/config/checkstyle/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
76 |
77 |
78 |
80 |
81 |
82 |
88 |
89 |
90 |
91 |
94 |
95 |
96 |
97 |
98 |
102 |
103 |
104 |
105 |
106 |
108 |
109 |
110 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
129 |
131 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
179 |
180 |
181 |
183 |
185 |
186 |
187 |
188 |
190 |
191 |
192 |
193 |
195 |
196 |
197 |
198 |
200 |
201 |
202 |
203 |
205 |
206 |
207 |
208 |
210 |
211 |
212 |
213 |
215 |
216 |
217 |
218 |
220 |
221 |
222 |
223 |
225 |
226 |
227 |
228 |
230 |
231 |
232 |
233 |
235 |
236 |
237 |
238 |
240 |
241 |
242 |
243 |
245 |
247 |
249 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
273 |
274 |
275 |
278 |
279 |
280 |
281 |
287 |
288 |
289 |
290 |
294 |
295 |
296 |
297 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
312 |
313 |
314 |
315 |
316 |
317 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
333 |
334 |
335 |
336 |
339 |
340 |
341 |
342 |
343 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
357 |
358 |
359 |
360 |
361 |
--------------------------------------------------------------------------------
/config/checkstyle/suppressions.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/config/spotbugs/suppressions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | limboapiVersion=1.1.26
2 | velocityVersion=3.4.0-SNAPSHOT
3 | nettyVersion=4.1.105.Final
4 | floodgateVersion=2.2.0-SNAPSHOT
5 | fastutilVersion=8.5.11
6 | bstatsVersion=3.0.1
7 | spotbugsVersion=4.9.3
8 | elytriumCommonsVersion=1.2.5-1
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elytrium/LimboAuth/bc8ebd92e541d755d76172f247574b10dd185971/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | getRootProject().setName("limboauth")
2 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/Endpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend;
19 |
20 | import com.google.common.io.ByteArrayDataInput;
21 | import com.google.common.io.ByteArrayDataOutput;
22 | import net.elytrium.limboauth.LimboAuth;
23 | import net.elytrium.limboauth.Settings;
24 |
25 | public abstract class Endpoint {
26 |
27 | protected final LimboAuth plugin;
28 | protected String type;
29 | protected String username;
30 |
31 | public Endpoint(LimboAuth plugin) {
32 | this.plugin = plugin;
33 | }
34 |
35 | public Endpoint(LimboAuth plugin, String type, String username) {
36 | this.plugin = plugin;
37 | this.type = type;
38 | this.username = username;
39 | }
40 |
41 | public void write(ByteArrayDataOutput output) {
42 | output.writeUTF(this.type);
43 | if (!this.type.equals("available_endpoints") && !Settings.IMP.MAIN.BACKEND_API.ENABLED_ENDPOINTS.contains(this.type)) {
44 | output.writeInt(-1);
45 | output.writeUTF(this.username);
46 | return;
47 | }
48 |
49 | output.writeInt(1);
50 | output.writeUTF(Settings.IMP.MAIN.BACKEND_API.TOKEN);
51 | output.writeUTF(this.username);
52 | this.writeContents(output);
53 | }
54 |
55 | public void read(ByteArrayDataInput input) {
56 | int version = input.readInt();
57 | if (version != 0) {
58 | throw new IllegalStateException("unsupported '" + this.type + "' endpoint version: " + version);
59 | }
60 |
61 | this.username = input.readUTF();
62 | this.readContents(input);
63 | }
64 |
65 | public abstract void writeContents(ByteArrayDataOutput output);
66 |
67 | public abstract void readContents(ByteArrayDataInput input);
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/type/LongDatabaseEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend.type;
19 |
20 | import java.util.function.Function;
21 | import net.elytrium.limboauth.LimboAuth;
22 | import net.elytrium.limboauth.handler.AuthSessionHandler;
23 | import net.elytrium.limboauth.model.RegisteredPlayer;
24 |
25 | public class LongDatabaseEndpoint extends LongEndpoint {
26 |
27 | public LongDatabaseEndpoint(LimboAuth plugin, String type, String username, long value) {
28 | super(plugin, type, username, value);
29 | }
30 |
31 | public LongDatabaseEndpoint(LimboAuth plugin, String type, Function function) {
32 | super(plugin, type, username -> {
33 | RegisteredPlayer player = AuthSessionHandler.fetchInfo(plugin.getPlayerDao(), username);
34 | if (player == null) {
35 | return Long.MIN_VALUE;
36 | } else {
37 | return function.apply(player);
38 | }
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/type/LongEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend.type;
19 |
20 | import com.google.common.io.ByteArrayDataInput;
21 | import com.google.common.io.ByteArrayDataOutput;
22 | import java.util.function.Function;
23 | import net.elytrium.limboauth.LimboAuth;
24 | import net.elytrium.limboauth.backend.Endpoint;
25 |
26 | public class LongEndpoint extends Endpoint {
27 |
28 | private Function function;
29 | private long value;
30 |
31 | public LongEndpoint(LimboAuth plugin, String type, Function function) {
32 | super(plugin, type, null);
33 | this.function = function;
34 | }
35 |
36 | public LongEndpoint(LimboAuth plugin, String type, String username, long value) {
37 | super(plugin, type, username);
38 | this.value = value;
39 | }
40 |
41 | @Override
42 | public void writeContents(ByteArrayDataOutput output) {
43 | output.writeLong(this.value);
44 | }
45 |
46 | @Override
47 | public void readContents(ByteArrayDataInput input) {
48 | this.value = this.function.apply(this.username);
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return "LongEndpoint{"
54 | + "username='" + this.username + '\''
55 | + ", value=" + this.value
56 | + '}';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/type/StringDatabaseEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend.type;
19 |
20 | import java.util.function.Function;
21 | import net.elytrium.limboauth.LimboAuth;
22 | import net.elytrium.limboauth.handler.AuthSessionHandler;
23 | import net.elytrium.limboauth.model.RegisteredPlayer;
24 |
25 | public class StringDatabaseEndpoint extends StringEndpoint {
26 |
27 | public StringDatabaseEndpoint(LimboAuth plugin, String type, String username, String value) {
28 | super(plugin, type, username, value);
29 | }
30 |
31 | public StringDatabaseEndpoint(LimboAuth plugin, String type, Function function) {
32 | super(plugin, type, username -> {
33 | RegisteredPlayer player = AuthSessionHandler.fetchInfo(plugin.getPlayerDao(), username);
34 | if (player == null) {
35 | return "";
36 | } else {
37 | return function.apply(player);
38 | }
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/type/StringEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend.type;
19 |
20 | import com.google.common.io.ByteArrayDataInput;
21 | import com.google.common.io.ByteArrayDataOutput;
22 | import java.util.function.Function;
23 | import net.elytrium.limboauth.LimboAuth;
24 | import net.elytrium.limboauth.backend.Endpoint;
25 |
26 | public class StringEndpoint extends Endpoint {
27 |
28 | private String value;
29 | private Function function;
30 |
31 | public StringEndpoint(LimboAuth plugin, String type, Function function) {
32 | super(plugin, type, null);
33 | this.function = function;
34 | }
35 |
36 | public StringEndpoint(LimboAuth plugin, String type, String username, String value) {
37 | super(plugin, type, username);
38 | this.value = value;
39 | }
40 |
41 | @Override
42 | public void writeContents(ByteArrayDataOutput output) {
43 | output.writeUTF(this.value);
44 | }
45 |
46 | @Override
47 | public void readContents(ByteArrayDataInput input) {
48 | this.value = this.function.apply(this.username);
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return "StringEndpoint{"
54 | + "username='" + this.username + '\''
55 | + ", value=" + this.value
56 | + '}';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/backend/type/UnknownEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.backend.type;
19 |
20 | import com.google.common.io.ByteArrayDataInput;
21 | import com.google.common.io.ByteArrayDataOutput;
22 | import net.elytrium.limboauth.LimboAuth;
23 | import net.elytrium.limboauth.backend.Endpoint;
24 |
25 | public class UnknownEndpoint extends Endpoint {
26 |
27 | private String type;
28 |
29 | public UnknownEndpoint(LimboAuth plugin) {
30 | super(plugin);
31 | }
32 |
33 | public UnknownEndpoint(LimboAuth plugin, String type) {
34 | super(plugin);
35 | this.type = type;
36 | }
37 |
38 | @Override
39 | public void write(ByteArrayDataOutput output) {
40 | output.writeUTF(this.type);
41 | output.writeInt(-2);
42 | }
43 |
44 | @Override
45 | public void read(ByteArrayDataInput input) {
46 | throw new UnsupportedOperationException();
47 | }
48 |
49 | @Override
50 | public void writeContents(ByteArrayDataOutput output) {
51 | throw new UnsupportedOperationException();
52 | }
53 |
54 | @Override
55 | public void readContents(ByteArrayDataInput input) {
56 | throw new UnsupportedOperationException();
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return "UnknownEndpoint{"
62 | + "type='" + this.type + '\''
63 | + '}';
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/ChangePasswordCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.j256.ormlite.stmt.UpdateBuilder;
22 | import com.velocitypowered.api.command.CommandSource;
23 | import com.velocitypowered.api.command.SimpleCommand;
24 | import com.velocitypowered.api.proxy.Player;
25 | import java.sql.SQLException;
26 | import java.util.Locale;
27 | import net.elytrium.commons.kyori.serialization.Serializer;
28 | import net.elytrium.limboauth.LimboAuth;
29 | import net.elytrium.limboauth.Settings;
30 | import net.elytrium.limboauth.event.ChangePasswordEvent;
31 | import net.elytrium.limboauth.handler.AuthSessionHandler;
32 | import net.elytrium.limboauth.model.RegisteredPlayer;
33 | import net.elytrium.limboauth.model.SQLRuntimeException;
34 | import net.kyori.adventure.text.Component;
35 |
36 | public class ChangePasswordCommand extends RatelimitedCommand {
37 |
38 | private final LimboAuth plugin;
39 | private final Dao playerDao;
40 |
41 | private final boolean needOldPass;
42 | private final Component notRegistered;
43 | private final Component wrongPassword;
44 | private final Component successful;
45 | private final Component errorOccurred;
46 | private final Component usage;
47 | private final Component notPlayer;
48 |
49 | public ChangePasswordCommand(LimboAuth plugin, Dao playerDao) {
50 | this.plugin = plugin;
51 | this.playerDao = playerDao;
52 |
53 | Serializer serializer = LimboAuth.getSerializer();
54 | this.needOldPass = Settings.IMP.MAIN.CHANGE_PASSWORD_NEED_OLD_PASSWORD;
55 | this.notRegistered = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_REGISTERED);
56 | this.wrongPassword = serializer.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_PASSWORD);
57 | this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.CHANGE_PASSWORD_SUCCESSFUL);
58 | this.errorOccurred = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED);
59 | this.usage = serializer.deserialize(Settings.IMP.MAIN.STRINGS.CHANGE_PASSWORD_USAGE);
60 | this.notPlayer = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PLAYER);
61 | }
62 |
63 | @Override
64 | public void execute(CommandSource source, String[] args) {
65 | if (source instanceof Player) {
66 | String usernameLowercase = ((Player) source).getUsername().toLowerCase(Locale.ROOT);
67 | RegisteredPlayer player = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase);
68 |
69 | if (player == null) {
70 | source.sendMessage(this.notRegistered);
71 | return;
72 | }
73 |
74 | boolean onlineMode = player.getHash().isEmpty();
75 | boolean needOldPass = this.needOldPass && !onlineMode;
76 | if (needOldPass) {
77 | if (args.length < 2) {
78 | source.sendMessage(this.usage);
79 | return;
80 | }
81 |
82 | if (!AuthSessionHandler.checkPassword(args[0], player, this.playerDao)) {
83 | source.sendMessage(this.wrongPassword);
84 | return;
85 | }
86 | } else if (args.length < 1) {
87 | source.sendMessage(this.usage);
88 | return;
89 | }
90 |
91 | try {
92 | final String oldHash = player.getHash();
93 | final String newPassword = needOldPass ? args[1] : args[0];
94 | final String newHash = RegisteredPlayer.genHash(newPassword);
95 |
96 | UpdateBuilder updateBuilder = this.playerDao.updateBuilder();
97 | updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, usernameLowercase);
98 | updateBuilder.updateColumnValue(RegisteredPlayer.HASH_FIELD, newHash);
99 | updateBuilder.update();
100 |
101 | this.plugin.removePlayerFromCacheLowercased(usernameLowercase);
102 |
103 | this.plugin.getServer().getEventManager().fireAndForget(
104 | new ChangePasswordEvent(player, needOldPass ? args[0] : null, oldHash, newPassword, newHash));
105 |
106 | source.sendMessage(this.successful);
107 | } catch (SQLException e) {
108 | source.sendMessage(this.errorOccurred);
109 | throw new SQLRuntimeException(e);
110 | }
111 | } else {
112 | source.sendMessage(this.notPlayer);
113 | }
114 | }
115 |
116 | @Override
117 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
118 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.CHANGE_PASSWORD
119 | .hasPermission(invocation.source(), "limboauth.commands.changepassword");
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/CommandPermissionState.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.velocitypowered.api.permission.PermissionSubject;
21 | import com.velocitypowered.api.permission.Tristate;
22 | import java.util.function.BiFunction;
23 |
24 | public enum CommandPermissionState {
25 | FALSE((source, permission) -> false),
26 | TRUE((source, permission) -> source.getPermissionValue(permission) != Tristate.FALSE),
27 | PERMISSION(PermissionSubject::hasPermission);
28 |
29 | private final BiFunction hasPermissionFunction;
30 |
31 | CommandPermissionState(BiFunction hasPermissionFunction) {
32 | this.hasPermissionFunction = hasPermissionFunction;
33 | }
34 |
35 | public boolean hasPermission(PermissionSubject permissionSubject, String permission) {
36 | return this.hasPermissionFunction.apply(permissionSubject, permission);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/DestroySessionCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.velocitypowered.api.command.CommandSource;
21 | import com.velocitypowered.api.command.SimpleCommand;
22 | import com.velocitypowered.api.proxy.Player;
23 | import net.elytrium.commons.kyori.serialization.Serializer;
24 | import net.elytrium.limboauth.LimboAuth;
25 | import net.elytrium.limboauth.Settings;
26 | import net.kyori.adventure.text.Component;
27 |
28 | public class DestroySessionCommand extends RatelimitedCommand {
29 |
30 | private final LimboAuth plugin;
31 |
32 | private final Component successful;
33 | private final Component notPlayer;
34 |
35 | public DestroySessionCommand(LimboAuth plugin) {
36 | this.plugin = plugin;
37 |
38 | Serializer serializer = LimboAuth.getSerializer();
39 | this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.DESTROY_SESSION_SUCCESSFUL);
40 | this.notPlayer = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PLAYER);
41 | }
42 |
43 | @Override
44 | public void execute(CommandSource source, String[] args) {
45 | if (source instanceof Player) {
46 | this.plugin.removePlayerFromCache(((Player) source).getUsername());
47 | source.sendMessage(this.successful);
48 | } else {
49 | source.sendMessage(this.notPlayer);
50 | }
51 | }
52 |
53 | @Override
54 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
55 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.DESTROY_SESSION
56 | .hasPermission(invocation.source(), "limboauth.commands.destroysession");
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/ForceChangePasswordCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.j256.ormlite.stmt.UpdateBuilder;
22 | import com.velocitypowered.api.command.CommandSource;
23 | import com.velocitypowered.api.command.SimpleCommand;
24 | import com.velocitypowered.api.proxy.ProxyServer;
25 | import java.sql.SQLException;
26 | import java.text.MessageFormat;
27 | import java.util.List;
28 | import java.util.Locale;
29 | import net.elytrium.commons.kyori.serialization.Serializer;
30 | import net.elytrium.commons.velocity.commands.SuggestUtils;
31 | import net.elytrium.limboauth.LimboAuth;
32 | import net.elytrium.limboauth.Settings;
33 | import net.elytrium.limboauth.event.ChangePasswordEvent;
34 | import net.elytrium.limboauth.handler.AuthSessionHandler;
35 | import net.elytrium.limboauth.model.RegisteredPlayer;
36 | import net.elytrium.limboauth.model.SQLRuntimeException;
37 | import net.kyori.adventure.text.Component;
38 |
39 | public class ForceChangePasswordCommand extends RatelimitedCommand {
40 |
41 | private final LimboAuth plugin;
42 | private final ProxyServer server;
43 | private final Dao playerDao;
44 |
45 | private final String message;
46 | private final String successful;
47 | private final String notSuccessful;
48 | private final String notRegistered;
49 | private final Component usage;
50 |
51 | public ForceChangePasswordCommand(LimboAuth plugin, ProxyServer server, Dao playerDao) {
52 | this.plugin = plugin;
53 | this.server = server;
54 | this.playerDao = playerDao;
55 |
56 | this.message = Settings.IMP.MAIN.STRINGS.FORCE_CHANGE_PASSWORD_MESSAGE;
57 | this.successful = Settings.IMP.MAIN.STRINGS.FORCE_CHANGE_PASSWORD_SUCCESSFUL;
58 | this.notSuccessful = Settings.IMP.MAIN.STRINGS.FORCE_CHANGE_PASSWORD_NOT_SUCCESSFUL;
59 | this.notRegistered = Settings.IMP.MAIN.STRINGS.FORCE_CHANGE_PASSWORD_NOT_REGISTERED;
60 | this.usage = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.FORCE_CHANGE_PASSWORD_USAGE);
61 | }
62 |
63 | @Override
64 | public List suggest(SimpleCommand.Invocation invocation) {
65 | return SuggestUtils.suggestPlayers(this.server, invocation.arguments(), 0);
66 | }
67 |
68 | @Override
69 | public void execute(CommandSource source, String[] args) {
70 | if (args.length == 2) {
71 | String nickname = args[0];
72 | String nicknameLowercased = args[0].toLowerCase(Locale.ROOT);
73 | String newPassword = args[1];
74 |
75 | Serializer serializer = LimboAuth.getSerializer();
76 | try {
77 | RegisteredPlayer registeredPlayer = AuthSessionHandler.fetchInfoLowercased(this.playerDao, nicknameLowercased);
78 |
79 | if (registeredPlayer == null) {
80 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.notRegistered, nickname)));
81 | return;
82 | }
83 |
84 | final String oldHash = registeredPlayer.getHash();
85 | final String newHash = RegisteredPlayer.genHash(newPassword);
86 |
87 | UpdateBuilder updateBuilder = this.playerDao.updateBuilder();
88 | updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, nicknameLowercased);
89 | updateBuilder.updateColumnValue(RegisteredPlayer.HASH_FIELD, newHash);
90 | updateBuilder.update();
91 |
92 | this.plugin.removePlayerFromCacheLowercased(nicknameLowercased);
93 | this.server.getPlayer(nickname)
94 | .ifPresent(player -> player.sendMessage(serializer.deserialize(MessageFormat.format(this.message, newPassword))));
95 |
96 | this.plugin.getServer().getEventManager().fireAndForget(new ChangePasswordEvent(registeredPlayer, null, oldHash, newPassword, newHash));
97 |
98 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.successful, nickname)));
99 | } catch (SQLException e) {
100 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.notSuccessful, nickname)));
101 | throw new SQLRuntimeException(e);
102 | }
103 | } else {
104 | source.sendMessage(this.usage);
105 | }
106 | }
107 |
108 | @Override
109 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
110 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.FORCE_CHANGE_PASSWORD
111 | .hasPermission(invocation.source(), "limboauth.admin.forcechangepassword");
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/ForceLoginCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.velocitypowered.api.command.CommandSource;
21 | import java.text.MessageFormat;
22 | import java.util.ArrayList;
23 | import java.util.List;
24 | import net.elytrium.commons.kyori.serialization.Serializer;
25 | import net.elytrium.limboauth.LimboAuth;
26 | import net.elytrium.limboauth.Settings;
27 | import net.elytrium.limboauth.handler.AuthSessionHandler;
28 | import net.kyori.adventure.text.Component;
29 |
30 | public class ForceLoginCommand extends RatelimitedCommand {
31 |
32 | private final LimboAuth plugin;
33 |
34 | private final String successful;
35 | private final String unknownPlayer;
36 | private final Component usage;
37 |
38 | public ForceLoginCommand(LimboAuth plugin) {
39 | this.plugin = plugin;
40 |
41 | this.successful = Settings.IMP.MAIN.STRINGS.FORCE_LOGIN_SUCCESSFUL;
42 | this.unknownPlayer = Settings.IMP.MAIN.STRINGS.FORCE_LOGIN_UNKNOWN_PLAYER;
43 | this.usage = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.FORCE_LOGIN_USAGE);
44 | }
45 |
46 | @Override
47 | public void execute(CommandSource source, String[] args) {
48 | if (args.length == 1) {
49 | String nickname = args[0];
50 |
51 | Serializer serializer = LimboAuth.getSerializer();
52 | AuthSessionHandler handler = this.plugin.getAuthenticatingPlayer(nickname);
53 | if (handler == null) {
54 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.unknownPlayer, nickname)));
55 | return;
56 | }
57 |
58 | handler.finishLogin();
59 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.successful, nickname)));
60 | } else {
61 | source.sendMessage(this.usage);
62 | }
63 | }
64 |
65 | @Override
66 | public boolean hasPermission(Invocation invocation) {
67 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.FORCE_LOGIN
68 | .hasPermission(invocation.source(), "limboauth.admin.forcelogin");
69 | }
70 |
71 | @Override
72 | public List suggest(Invocation invocation) {
73 | if (invocation.arguments().length > 1) {
74 | return super.suggest(invocation);
75 | }
76 |
77 | String nickname = invocation.arguments().length == 0 ? "" : invocation.arguments()[0];
78 | List suggest = new ArrayList<>();
79 | for (String username : this.plugin.getAuthenticatingPlayers().keySet()) {
80 | if (username.startsWith(nickname)) {
81 | suggest.add(username);
82 | }
83 | }
84 |
85 | return suggest;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/ForceRegisterCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.velocitypowered.api.command.CommandSource;
22 | import com.velocitypowered.api.command.SimpleCommand;
23 | import java.sql.SQLException;
24 | import java.text.MessageFormat;
25 | import java.util.Locale;
26 | import net.elytrium.commons.kyori.serialization.Serializer;
27 | import net.elytrium.limboauth.LimboAuth;
28 | import net.elytrium.limboauth.Settings;
29 | import net.elytrium.limboauth.model.RegisteredPlayer;
30 | import net.elytrium.limboauth.model.SQLRuntimeException;
31 | import net.kyori.adventure.text.Component;
32 |
33 | public class ForceRegisterCommand extends RatelimitedCommand {
34 |
35 | private final LimboAuth plugin;
36 | private final Dao playerDao;
37 |
38 | private final String successful;
39 | private final String notSuccessful;
40 | private final Component usage;
41 | private final Component takenNickname;
42 | private final Component incorrectNickname;
43 |
44 | public ForceRegisterCommand(LimboAuth plugin, Dao playerDao) {
45 | this.plugin = plugin;
46 | this.playerDao = playerDao;
47 |
48 | this.successful = Settings.IMP.MAIN.STRINGS.FORCE_REGISTER_SUCCESSFUL;
49 | this.notSuccessful = Settings.IMP.MAIN.STRINGS.FORCE_REGISTER_NOT_SUCCESSFUL;
50 | this.usage = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.FORCE_REGISTER_USAGE);
51 | this.takenNickname = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.FORCE_REGISTER_TAKEN_NICKNAME);
52 | this.incorrectNickname = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.FORCE_REGISTER_INCORRECT_NICKNAME);
53 | }
54 |
55 | @Override
56 | public void execute(CommandSource source, String[] args) {
57 | if (args.length == 2) {
58 | String nickname = args[0];
59 | String password = args[1];
60 |
61 | Serializer serializer = LimboAuth.getSerializer();
62 | try {
63 | if (!this.plugin.getNicknameValidationPattern().matcher(nickname).matches()) {
64 | source.sendMessage(this.incorrectNickname);
65 | return;
66 | }
67 |
68 | String lowercaseNickname = nickname.toLowerCase(Locale.ROOT);
69 | if (this.playerDao.idExists(lowercaseNickname)) {
70 | source.sendMessage(this.takenNickname);
71 | return;
72 | }
73 |
74 | RegisteredPlayer player = new RegisteredPlayer(nickname, "", "").setPassword(password);
75 | this.playerDao.create(player);
76 |
77 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.successful, nickname)));
78 | } catch (SQLException e) {
79 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.notSuccessful, nickname)));
80 | throw new SQLRuntimeException(e);
81 | }
82 | } else {
83 | source.sendMessage(this.usage);
84 | }
85 | }
86 |
87 | @Override
88 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
89 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.FORCE_REGISTER
90 | .hasPermission(invocation.source(), "limboauth.admin.forceregister");
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/ForceUnregisterCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.velocitypowered.api.command.CommandSource;
22 | import com.velocitypowered.api.command.SimpleCommand;
23 | import com.velocitypowered.api.proxy.ProxyServer;
24 | import java.sql.SQLException;
25 | import java.text.MessageFormat;
26 | import java.util.List;
27 | import java.util.Locale;
28 | import net.elytrium.commons.kyori.serialization.Serializer;
29 | import net.elytrium.commons.velocity.commands.SuggestUtils;
30 | import net.elytrium.limboauth.LimboAuth;
31 | import net.elytrium.limboauth.Settings;
32 | import net.elytrium.limboauth.event.AuthUnregisterEvent;
33 | import net.elytrium.limboauth.model.RegisteredPlayer;
34 | import net.elytrium.limboauth.model.SQLRuntimeException;
35 | import net.kyori.adventure.text.Component;
36 |
37 | public class ForceUnregisterCommand extends RatelimitedCommand {
38 |
39 | private final LimboAuth plugin;
40 | private final ProxyServer server;
41 | private final Dao playerDao;
42 |
43 | private final Component kick;
44 | private final String successful;
45 | private final String notSuccessful;
46 | private final Component usage;
47 |
48 | public ForceUnregisterCommand(LimboAuth plugin, ProxyServer server, Dao playerDao) {
49 | this.plugin = plugin;
50 | this.server = server;
51 | this.playerDao = playerDao;
52 |
53 | Serializer serializer = LimboAuth.getSerializer();
54 | this.kick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.FORCE_UNREGISTER_KICK);
55 | this.successful = Settings.IMP.MAIN.STRINGS.FORCE_UNREGISTER_SUCCESSFUL;
56 | this.notSuccessful = Settings.IMP.MAIN.STRINGS.FORCE_UNREGISTER_NOT_SUCCESSFUL;
57 | this.usage = serializer.deserialize(Settings.IMP.MAIN.STRINGS.FORCE_UNREGISTER_USAGE);
58 | }
59 |
60 | @Override
61 | public List suggest(SimpleCommand.Invocation invocation) {
62 | return SuggestUtils.suggestPlayers(this.server, invocation.arguments(), 0);
63 | }
64 |
65 | @Override
66 | public void execute(CommandSource source, String[] args) {
67 | if (args.length == 1) {
68 | String playerNick = args[0];
69 | String usernameLowercased = playerNick.toLowerCase(Locale.ROOT);
70 |
71 | Serializer serializer = LimboAuth.getSerializer();
72 | try {
73 | this.plugin.getServer().getEventManager().fireAndForget(new AuthUnregisterEvent(playerNick));
74 | this.playerDao.deleteById(usernameLowercased);
75 | this.plugin.removePlayerFromCacheLowercased(usernameLowercased);
76 | this.server.getPlayer(playerNick).ifPresent(player -> player.disconnect(this.kick));
77 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.successful, playerNick)));
78 | } catch (SQLException e) {
79 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.notSuccessful, playerNick)));
80 | throw new SQLRuntimeException(e);
81 | }
82 | } else {
83 | source.sendMessage(this.usage);
84 | }
85 | }
86 |
87 | @Override
88 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
89 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.FORCE_UNREGISTER
90 | .hasPermission(invocation.source(), "limboauth.admin.forceunregister");
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/LimboAuthCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.google.common.collect.ImmutableList;
21 | import com.velocitypowered.api.command.CommandSource;
22 | import com.velocitypowered.api.command.SimpleCommand;
23 | import java.util.Arrays;
24 | import java.util.List;
25 | import java.util.Locale;
26 | import java.util.stream.Collectors;
27 | import net.elytrium.limboauth.LimboAuth;
28 | import net.elytrium.limboauth.Settings;
29 | import net.kyori.adventure.text.Component;
30 | import net.kyori.adventure.text.format.NamedTextColor;
31 |
32 | public class LimboAuthCommand extends RatelimitedCommand {
33 |
34 | private static final List HELP_MESSAGE = List.of(
35 | Component.text("This server is using LimboAuth and LimboAPI.", NamedTextColor.YELLOW),
36 | Component.text("(C) 2021 - 2024 Elytrium", NamedTextColor.YELLOW),
37 | Component.text("https://elytrium.net/github/", NamedTextColor.GREEN),
38 | Component.empty()
39 | );
40 |
41 | private static final Component AVAILABLE_SUBCOMMANDS_MESSAGE = Component.text("Available subcommands:", NamedTextColor.WHITE);
42 | private static final Component NO_AVAILABLE_SUBCOMMANDS_MESSAGE = Component.text("There is no available subcommands for you.", NamedTextColor.WHITE);
43 |
44 | private final LimboAuth plugin;
45 |
46 | public LimboAuthCommand(LimboAuth plugin) {
47 | this.plugin = plugin;
48 | }
49 |
50 | @Override
51 | public List suggest(SimpleCommand.Invocation invocation) {
52 | CommandSource source = invocation.source();
53 | String[] args = invocation.arguments();
54 |
55 | if (args.length == 0) {
56 | return Arrays.stream(Subcommand.values())
57 | .filter(command -> command.hasPermission(source))
58 | .map(Subcommand::getCommand)
59 | .collect(Collectors.toList());
60 | } else if (args.length == 1) {
61 | String argument = args[0];
62 | return Arrays.stream(Subcommand.values())
63 | .filter(command -> command.hasPermission(source))
64 | .map(Subcommand::getCommand)
65 | .filter(str -> str.regionMatches(true, 0, argument, 0, argument.length()))
66 | .collect(Collectors.toList());
67 | } else {
68 | return ImmutableList.of();
69 | }
70 | }
71 |
72 | @Override
73 | public void execute(CommandSource source, String[] args) {
74 | int argsAmount = args.length;
75 | if (argsAmount > 0) {
76 | try {
77 | Subcommand subcommand = Subcommand.valueOf(args[0].toUpperCase(Locale.ROOT));
78 | if (!subcommand.hasPermission(source)) {
79 | this.showHelp(source);
80 | return;
81 | }
82 |
83 | subcommand.executor.execute(this, source, args);
84 | } catch (IllegalArgumentException e) {
85 | this.showHelp(source);
86 | }
87 | } else {
88 | this.showHelp(source);
89 | }
90 | }
91 |
92 | @Override
93 | public boolean hasPermission(Invocation invocation) {
94 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.HELP
95 | .hasPermission(invocation.source(), "limboauth.commands.help");
96 | }
97 |
98 | private void showHelp(CommandSource source) {
99 | HELP_MESSAGE.forEach(source::sendMessage);
100 |
101 | List availableSubcommands = Arrays.stream(Subcommand.values())
102 | .filter(command -> command.hasPermission(source))
103 | .collect(Collectors.toList());
104 |
105 | if (availableSubcommands.size() > 0) {
106 | source.sendMessage(AVAILABLE_SUBCOMMANDS_MESSAGE);
107 | availableSubcommands.forEach(command -> source.sendMessage(command.getMessageLine()));
108 | } else {
109 | source.sendMessage(NO_AVAILABLE_SUBCOMMANDS_MESSAGE);
110 | }
111 | }
112 |
113 | private enum Subcommand {
114 | RELOAD("Reload config.", Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.RELOAD,
115 | (LimboAuthCommand parent, CommandSource source, String[] args) -> {
116 | parent.plugin.reload();
117 | source.sendMessage(LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.RELOAD));
118 | });
119 |
120 | private final String command;
121 | private final String description;
122 | private final CommandPermissionState permissionState;
123 | private final SubcommandExecutor executor;
124 |
125 | Subcommand(String description, CommandPermissionState permissionState, SubcommandExecutor executor) {
126 | this.permissionState = permissionState;
127 | this.command = this.name().toLowerCase(Locale.ROOT);
128 | this.description = description;
129 | this.executor = executor;
130 | }
131 |
132 | public boolean hasPermission(CommandSource source) {
133 | return this.permissionState.hasPermission(source, "limboauth.admin." + this.command);
134 | }
135 |
136 | public Component getMessageLine() {
137 | return Component.textOfChildren(
138 | Component.text(" /limboauth " + this.command, NamedTextColor.GREEN),
139 | Component.text(" - ", NamedTextColor.DARK_GRAY),
140 | Component.text(this.description, NamedTextColor.YELLOW)
141 | );
142 | }
143 |
144 | public String getCommand() {
145 | return this.command;
146 | }
147 | }
148 |
149 | private interface SubcommandExecutor {
150 | void execute(LimboAuthCommand parent, CommandSource source, String[] args);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/PremiumCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.velocitypowered.api.command.CommandSource;
22 | import com.velocitypowered.api.command.SimpleCommand;
23 | import com.velocitypowered.api.proxy.Player;
24 | import java.sql.SQLException;
25 | import java.util.Locale;
26 | import net.elytrium.commons.kyori.serialization.Serializer;
27 | import net.elytrium.limboauth.LimboAuth;
28 | import net.elytrium.limboauth.Settings;
29 | import net.elytrium.limboauth.handler.AuthSessionHandler;
30 | import net.elytrium.limboauth.model.RegisteredPlayer;
31 | import net.elytrium.limboauth.model.SQLRuntimeException;
32 | import net.kyori.adventure.text.Component;
33 |
34 | public class PremiumCommand extends RatelimitedCommand {
35 |
36 | private final LimboAuth plugin;
37 | private final Dao playerDao;
38 |
39 | private final String confirmKeyword;
40 | private final Component notRegistered;
41 | private final Component alreadyPremium;
42 | private final Component successful;
43 | private final Component errorOccurred;
44 | private final Component notPremium;
45 | private final Component wrongPassword;
46 | private final Component usage;
47 | private final Component notPlayer;
48 |
49 | public PremiumCommand(LimboAuth plugin, Dao playerDao) {
50 | this.plugin = plugin;
51 | this.playerDao = playerDao;
52 |
53 | Serializer serializer = LimboAuth.getSerializer();
54 | this.confirmKeyword = Settings.IMP.MAIN.CONFIRM_KEYWORD;
55 | this.notRegistered = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_REGISTERED);
56 | this.alreadyPremium = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ALREADY_PREMIUM);
57 | this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.PREMIUM_SUCCESSFUL);
58 | this.errorOccurred = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED);
59 | this.notPremium = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PREMIUM);
60 | this.wrongPassword = serializer.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_PASSWORD);
61 | this.usage = serializer.deserialize(Settings.IMP.MAIN.STRINGS.PREMIUM_USAGE);
62 | this.notPlayer = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PLAYER);
63 | }
64 |
65 | @Override
66 | public void execute(CommandSource source, String[] args) {
67 | if (source instanceof Player) {
68 | if (args.length == 2) {
69 | if (this.confirmKeyword.equalsIgnoreCase(args[1])) {
70 | String usernameLowercase = ((Player) source).getUsername().toLowerCase(Locale.ROOT);
71 | RegisteredPlayer player = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase);
72 | if (player == null) {
73 | source.sendMessage(this.notRegistered);
74 | } else if (player.getHash().isEmpty()) {
75 | source.sendMessage(this.alreadyPremium);
76 | } else if (AuthSessionHandler.checkPassword(args[0], player, this.playerDao)) {
77 | if (this.plugin.isPremiumExternal(usernameLowercase).getState() == LimboAuth.PremiumState.PREMIUM_USERNAME) {
78 | try {
79 | player.setHash("");
80 | this.playerDao.update(player);
81 | this.plugin.removePlayerFromCacheLowercased(usernameLowercase);
82 | ((Player) source).disconnect(this.successful);
83 | } catch (SQLException e) {
84 | source.sendMessage(this.errorOccurred);
85 | throw new SQLRuntimeException(e);
86 | }
87 | } else {
88 | source.sendMessage(this.notPremium);
89 | }
90 | } else {
91 | source.sendMessage(this.wrongPassword);
92 | }
93 |
94 | return;
95 | }
96 | }
97 |
98 | source.sendMessage(this.usage);
99 | } else {
100 | source.sendMessage(this.notPlayer);
101 | }
102 | }
103 |
104 | @Override
105 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
106 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.PREMIUM
107 | .hasPermission(invocation.source(), "limboauth.commands.premium");
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/RatelimitedCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.velocitypowered.api.command.CommandSource;
21 | import com.velocitypowered.api.command.SimpleCommand;
22 | import com.velocitypowered.api.proxy.Player;
23 | import net.elytrium.limboauth.LimboAuth;
24 | import net.elytrium.limboauth.Settings;
25 | import net.kyori.adventure.text.Component;
26 |
27 | public abstract class RatelimitedCommand implements SimpleCommand {
28 |
29 | private final Component ratelimited;
30 |
31 | public RatelimitedCommand() {
32 | this.ratelimited = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.RATELIMITED);
33 | }
34 |
35 | @Override
36 | public final void execute(SimpleCommand.Invocation invocation) {
37 | CommandSource source = invocation.source();
38 | if (source instanceof Player) {
39 | if (!LimboAuth.RATELIMITER.attempt(((Player) source).getRemoteAddress().getAddress())) {
40 | source.sendMessage(this.ratelimited);
41 | return;
42 | }
43 | }
44 |
45 | this.execute(source, invocation.arguments());
46 | }
47 |
48 | protected abstract void execute(CommandSource source, String[] args);
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/TotpCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.j256.ormlite.stmt.UpdateBuilder;
22 | import com.velocitypowered.api.command.CommandSource;
23 | import com.velocitypowered.api.command.SimpleCommand;
24 | import com.velocitypowered.api.proxy.Player;
25 | import dev.samstevens.totp.qr.QrData;
26 | import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
27 | import dev.samstevens.totp.secret.DefaultSecretGenerator;
28 | import dev.samstevens.totp.secret.SecretGenerator;
29 | import java.net.URLEncoder;
30 | import java.nio.charset.StandardCharsets;
31 | import java.sql.SQLException;
32 | import java.text.MessageFormat;
33 | import java.util.Locale;
34 | import net.elytrium.commons.kyori.serialization.Serializer;
35 | import net.elytrium.limboauth.LimboAuth;
36 | import net.elytrium.limboauth.Settings;
37 | import net.elytrium.limboauth.handler.AuthSessionHandler;
38 | import net.elytrium.limboauth.model.RegisteredPlayer;
39 | import net.elytrium.limboauth.model.SQLRuntimeException;
40 | import net.kyori.adventure.text.Component;
41 | import net.kyori.adventure.text.event.ClickEvent;
42 |
43 | public class TotpCommand extends RatelimitedCommand {
44 |
45 | private final SecretGenerator secretGenerator = new DefaultSecretGenerator();
46 | private final RecoveryCodeGenerator codesGenerator = new RecoveryCodeGenerator();
47 | private final Dao playerDao;
48 |
49 | private final Component notPlayer;
50 | private final Component usage;
51 | private final boolean needPassword;
52 | private final Component notRegistered;
53 | private final Component wrongPassword;
54 | private final Component alreadyEnabled;
55 | private final Component errorOccurred;
56 | private final Component successful;
57 | private final String issuer;
58 | private final String qrGeneratorUrl;
59 | private final Component qr;
60 | private final String token;
61 | private final int recoveryCodesAmount;
62 | private final String recovery;
63 | private final Component disabled;
64 | private final Component wrong;
65 | private final Component crackedCommand;
66 |
67 | public TotpCommand(Dao playerDao) {
68 | this.playerDao = playerDao;
69 |
70 | Serializer serializer = LimboAuth.getSerializer();
71 | this.notPlayer = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PLAYER);
72 | this.usage = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_USAGE);
73 | this.needPassword = Settings.IMP.MAIN.TOTP_NEED_PASSWORD;
74 | this.notRegistered = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_REGISTERED);
75 | this.wrongPassword = serializer.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_PASSWORD);
76 | this.alreadyEnabled = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_ALREADY_ENABLED);
77 | this.errorOccurred = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED);
78 | this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_SUCCESSFUL);
79 | this.issuer = Settings.IMP.MAIN.TOTP_ISSUER;
80 | this.qrGeneratorUrl = Settings.IMP.MAIN.QR_GENERATOR_URL;
81 | this.qr = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_QR);
82 | this.token = Settings.IMP.MAIN.STRINGS.TOTP_TOKEN;
83 | this.recoveryCodesAmount = Settings.IMP.MAIN.TOTP_RECOVERY_CODES_AMOUNT;
84 | this.recovery = Settings.IMP.MAIN.STRINGS.TOTP_RECOVERY;
85 | this.disabled = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_DISABLED);
86 | this.wrong = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_WRONG);
87 | this.crackedCommand = serializer.deserialize(Settings.IMP.MAIN.STRINGS.CRACKED_COMMAND);
88 | }
89 |
90 | // TODO: Rewrite.
91 | @Override
92 | public void execute(CommandSource source, String[] args) {
93 | if (source instanceof Player) {
94 | if (args.length == 0) {
95 | source.sendMessage(this.usage);
96 | } else {
97 | String username = ((Player) source).getUsername();
98 | String usernameLowercase = username.toLowerCase(Locale.ROOT);
99 |
100 | RegisteredPlayer playerInfo;
101 | UpdateBuilder updateBuilder;
102 | if (args[0].equalsIgnoreCase("enable")) {
103 | if (this.needPassword ? args.length == 2 : args.length == 1) {
104 | playerInfo = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase);
105 | if (playerInfo == null) {
106 | source.sendMessage(this.notRegistered);
107 | return;
108 | } else if (playerInfo.getHash().isEmpty()) {
109 | source.sendMessage(this.crackedCommand);
110 | return;
111 | } else if (this.needPassword && !AuthSessionHandler.checkPassword(args[1], playerInfo, this.playerDao)) {
112 | source.sendMessage(this.wrongPassword);
113 | return;
114 | }
115 |
116 | if (!playerInfo.getTotpToken().isEmpty()) {
117 | source.sendMessage(this.alreadyEnabled);
118 | return;
119 | }
120 |
121 | String secret = this.secretGenerator.generate();
122 | try {
123 | updateBuilder = this.playerDao.updateBuilder();
124 | updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, usernameLowercase);
125 | updateBuilder.updateColumnValue(RegisteredPlayer.TOTP_TOKEN_FIELD, secret);
126 | updateBuilder.update();
127 | } catch (SQLException e) {
128 | source.sendMessage(this.errorOccurred);
129 | throw new SQLRuntimeException(e);
130 | }
131 | source.sendMessage(this.successful);
132 |
133 | QrData data = new QrData.Builder()
134 | .label(username)
135 | .secret(secret)
136 | .issuer(this.issuer)
137 | .build();
138 | String qrUrl = this.qrGeneratorUrl.replace("{data}", URLEncoder.encode(data.getUri(), StandardCharsets.UTF_8));
139 | source.sendMessage(this.qr.clickEvent(ClickEvent.openUrl(qrUrl)));
140 |
141 | Serializer serializer = LimboAuth.getSerializer();
142 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.token, secret))
143 | .clickEvent(ClickEvent.copyToClipboard(secret)));
144 | String codes = String.join(", ", this.codesGenerator.generateCodes(this.recoveryCodesAmount));
145 | source.sendMessage(serializer.deserialize(MessageFormat.format(this.recovery, codes))
146 | .clickEvent(ClickEvent.copyToClipboard(codes)));
147 | } else {
148 | source.sendMessage(this.usage);
149 | }
150 | } else if (args[0].equalsIgnoreCase("disable")) {
151 | if (args.length == 2) {
152 | playerInfo = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase);
153 |
154 | if (playerInfo == null) {
155 | source.sendMessage(this.notRegistered);
156 | return;
157 | }
158 |
159 | if (AuthSessionHandler.TOTP_CODE_VERIFIER.isValidCode(playerInfo.getTotpToken(), args[1])) {
160 | try {
161 | updateBuilder = this.playerDao.updateBuilder();
162 | updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, usernameLowercase);
163 | updateBuilder.updateColumnValue(RegisteredPlayer.TOTP_TOKEN_FIELD, "");
164 | updateBuilder.update();
165 |
166 | source.sendMessage(this.disabled);
167 | } catch (SQLException e) {
168 | source.sendMessage(this.errorOccurred);
169 | throw new SQLRuntimeException(e);
170 | }
171 | } else {
172 | source.sendMessage(this.wrong);
173 | }
174 | } else {
175 | source.sendMessage(this.usage);
176 | }
177 | } else {
178 | source.sendMessage(this.usage);
179 | }
180 | }
181 | } else {
182 | source.sendMessage(this.notPlayer);
183 | }
184 | }
185 |
186 | @Override
187 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
188 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.TOTP
189 | .hasPermission(invocation.source(), "limboauth.commands.totp");
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/command/UnregisterCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.command;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.velocitypowered.api.command.CommandSource;
22 | import com.velocitypowered.api.command.SimpleCommand;
23 | import com.velocitypowered.api.proxy.Player;
24 | import java.sql.SQLException;
25 | import java.util.Locale;
26 | import net.elytrium.commons.kyori.serialization.Serializer;
27 | import net.elytrium.limboauth.LimboAuth;
28 | import net.elytrium.limboauth.Settings;
29 | import net.elytrium.limboauth.event.AuthUnregisterEvent;
30 | import net.elytrium.limboauth.handler.AuthSessionHandler;
31 | import net.elytrium.limboauth.model.RegisteredPlayer;
32 | import net.elytrium.limboauth.model.SQLRuntimeException;
33 | import net.kyori.adventure.text.Component;
34 |
35 | public class UnregisterCommand extends RatelimitedCommand {
36 |
37 | private final LimboAuth plugin;
38 | private final Dao playerDao;
39 |
40 | private final String confirmKeyword;
41 | private final Component notPlayer;
42 | private final Component notRegistered;
43 | private final Component successful;
44 | private final Component errorOccurred;
45 | private final Component wrongPassword;
46 | private final Component usage;
47 | private final Component crackedCommand;
48 |
49 | public UnregisterCommand(LimboAuth plugin, Dao playerDao) {
50 | this.plugin = plugin;
51 | this.playerDao = playerDao;
52 |
53 | Serializer serializer = LimboAuth.getSerializer();
54 | this.confirmKeyword = Settings.IMP.MAIN.CONFIRM_KEYWORD;
55 | this.notPlayer = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_PLAYER);
56 | this.notRegistered = serializer.deserialize(Settings.IMP.MAIN.STRINGS.NOT_REGISTERED);
57 | this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.UNREGISTER_SUCCESSFUL);
58 | this.errorOccurred = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED);
59 | this.wrongPassword = serializer.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_PASSWORD);
60 | this.usage = serializer.deserialize(Settings.IMP.MAIN.STRINGS.UNREGISTER_USAGE);
61 | this.crackedCommand = serializer.deserialize(Settings.IMP.MAIN.STRINGS.CRACKED_COMMAND);
62 | }
63 |
64 | @Override
65 | public void execute(CommandSource source, String[] args) {
66 | if (source instanceof Player) {
67 | if (args.length == 2) {
68 | if (this.confirmKeyword.equalsIgnoreCase(args[1])) {
69 | String username = ((Player) source).getUsername();
70 | String usernameLowercase = username.toLowerCase(Locale.ROOT);
71 | RegisteredPlayer player = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase);
72 | if (player == null) {
73 | source.sendMessage(this.notRegistered);
74 | } else if (player.getHash().isEmpty()) {
75 | source.sendMessage(this.crackedCommand);
76 | } else if (AuthSessionHandler.checkPassword(args[0], player, this.playerDao)) {
77 | try {
78 | this.plugin.getServer().getEventManager().fireAndForget(new AuthUnregisterEvent(username));
79 | this.playerDao.deleteById(usernameLowercase);
80 | this.plugin.removePlayerFromCacheLowercased(usernameLowercase);
81 | ((Player) source).disconnect(this.successful);
82 | } catch (SQLException e) {
83 | source.sendMessage(this.errorOccurred);
84 | throw new SQLRuntimeException(e);
85 | }
86 | } else {
87 | source.sendMessage(this.wrongPassword);
88 | }
89 |
90 | return;
91 | }
92 | }
93 |
94 | source.sendMessage(this.usage);
95 | } else {
96 | source.sendMessage(this.notPlayer);
97 | }
98 | }
99 |
100 | @Override
101 | public boolean hasPermission(SimpleCommand.Invocation invocation) {
102 | return Settings.IMP.MAIN.COMMAND_PERMISSION_STATE.UNREGISTER
103 | .hasPermission(invocation.source(), "limboauth.commands.unregister");
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.dependencies;
19 |
20 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.net.MalformedURLException;
24 | import java.net.URL;
25 | import java.nio.file.Files;
26 | import java.nio.file.Path;
27 | import java.nio.file.StandardCopyOption;
28 |
29 | public enum BaseLibrary {
30 | H2_V1(
31 | "com.h2database",
32 | "h2",
33 | "1.4.200"
34 | ),
35 | H2_V2(
36 | "com.h2database",
37 | "h2",
38 | "2.1.214"
39 | ),
40 | MYSQL(
41 | "com.mysql",
42 | "mysql-connector-j",
43 | "8.0.33"
44 | ),
45 | MARIADB(
46 | "org.mariadb.jdbc",
47 | "mariadb-java-client",
48 | "3.1.4"
49 | ),
50 | POSTGRESQL(
51 | "org.postgresql",
52 | "postgresql",
53 | "42.5.1"
54 | ),
55 | SQLITE(
56 | "org.xerial",
57 | "sqlite-jdbc",
58 | "3.40.0.0"
59 | );
60 |
61 | private final Path filenamePath;
62 | private final URL mavenRepoURL;
63 |
64 | BaseLibrary(String groupId, String artifactId, String version) {
65 | String mavenPath = String.format("%s/%s/%s/%s-%s.jar",
66 | groupId.replace(".", "/"),
67 | artifactId,
68 | version,
69 | artifactId,
70 | version
71 | );
72 |
73 | this.filenamePath = Path.of("libraries/" + mavenPath);
74 |
75 | try {
76 | this.mavenRepoURL = new URL("https://repo1.maven.org/maven2/" + mavenPath);
77 | } catch (MalformedURLException e) {
78 | throw new IllegalArgumentException(e);
79 | }
80 | }
81 |
82 | @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
83 | public URL getClassLoaderURL() throws MalformedURLException {
84 | if (!Files.exists(this.filenamePath)) {
85 | try {
86 | try (InputStream in = this.mavenRepoURL.openStream()) {
87 | Files.createDirectories(this.filenamePath.getParent());
88 | Files.copy(in, Files.createFile(this.filenamePath), StandardCopyOption.REPLACE_EXISTING);
89 | }
90 | } catch (IOException e) {
91 | throw new IllegalArgumentException(e);
92 | }
93 | }
94 |
95 | return this.filenamePath.toUri().toURL();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.dependencies;
19 |
20 | import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
21 | import com.j256.ormlite.jdbc.db.DatabaseTypeUtils;
22 | import com.j256.ormlite.support.ConnectionSource;
23 | import java.io.IOException;
24 | import java.lang.reflect.Constructor;
25 | import java.lang.reflect.Method;
26 | import java.net.URISyntaxException;
27 | import java.net.URL;
28 | import java.nio.file.Files;
29 | import java.nio.file.Path;
30 | import java.sql.Connection;
31 | import java.sql.Driver;
32 | import java.sql.DriverManager;
33 | import java.sql.PreparedStatement;
34 | import java.sql.SQLException;
35 | import java.util.Locale;
36 | import java.util.Properties;
37 |
38 | public enum DatabaseLibrary {
39 | H2_LEGACY_V1(
40 | BaseLibrary.H2_V1,
41 | (classLoader, dir, jdbc, user, password) -> fromDriver(classLoader.loadClass("org.h2.Driver"), jdbc, null, null, false),
42 | (dir, hostname, database) -> "jdbc:h2:" + dir + "/limboauth"
43 | ),
44 | H2(
45 | BaseLibrary.H2_V2,
46 | (classLoader, dir, jdbc, user, password) -> {
47 | Connection modernConnection = fromDriver(classLoader.loadClass("org.h2.Driver"), jdbc, null, null, true);
48 |
49 | Path legacyDatabase = dir.resolve("limboauth.mv.db");
50 | if (Files.exists(legacyDatabase)) {
51 | Path dumpFile = dir.resolve("limboauth.dump.sql");
52 | try (Connection legacyConnection = H2_LEGACY_V1.connect(dir, null, null, user, password)) {
53 | try (PreparedStatement migrateStatement = legacyConnection.prepareStatement("SCRIPT TO '?'")) {
54 | migrateStatement.setString(1, dumpFile.toString());
55 | migrateStatement.execute();
56 | }
57 | }
58 |
59 | try (PreparedStatement migrateStatement = modernConnection.prepareStatement("RUNSCRIPT FROM '?'")) {
60 | migrateStatement.setString(1, dumpFile.toString());
61 | migrateStatement.execute();
62 | }
63 |
64 | Files.delete(dumpFile);
65 | Files.move(legacyDatabase, dir.resolve("limboauth-v1-backup.mv.db"));
66 | }
67 |
68 | return modernConnection;
69 | },
70 | (dir, hostname, database) -> "jdbc:h2:" + dir + "/limboauth-v2"
71 | ),
72 | MYSQL(
73 | BaseLibrary.MYSQL,
74 | (classLoader, dir, jdbc, user, password)
75 | -> fromDriver(classLoader.loadClass("com.mysql.cj.jdbc.NonRegisteringDriver"), jdbc, user, password, true),
76 | (dir, hostname, database) ->
77 | "jdbc:mysql://" + hostname + "/" + database
78 | ),
79 | MARIADB(
80 | BaseLibrary.MARIADB,
81 | (classLoader, dir, jdbc, user, password)
82 | -> fromDriver(classLoader.loadClass("org.mariadb.jdbc.Driver"), jdbc, user, password, true),
83 | (dir, hostname, database) ->
84 | "jdbc:mariadb://" + hostname + "/" + database
85 | ),
86 | POSTGRESQL(
87 | BaseLibrary.POSTGRESQL,
88 | (classLoader, dir, jdbc, user, password) -> fromDriver(classLoader.loadClass("org.postgresql.Driver"), jdbc, user, password, true),
89 | (dir, hostname, database) -> "jdbc:postgresql://" + hostname + "/" + database
90 | ),
91 | SQLITE(
92 | BaseLibrary.SQLITE,
93 | (classLoader, dir, jdbc, user, password) -> fromDriver(classLoader.loadClass("org.sqlite.JDBC"), jdbc, user, password, true),
94 | (dir, hostname, database) -> "jdbc:sqlite:" + dir + "/limboauth.db"
95 | );
96 |
97 | private final BaseLibrary baseLibrary;
98 | private final DatabaseConnector connector;
99 | private final DatabaseStringGetter stringGetter;
100 | private final IsolatedDriver driver = new IsolatedDriver("jdbc:limboauth_" + this.name().toLowerCase(Locale.ROOT) + ":");
101 |
102 | DatabaseLibrary(BaseLibrary baseLibrary, DatabaseConnector connector, DatabaseStringGetter stringGetter) {
103 | this.baseLibrary = baseLibrary;
104 | this.connector = connector;
105 | this.stringGetter = stringGetter;
106 | }
107 |
108 | public Connection connect(ClassLoader classLoader, Path dir, String hostname, String database, String user, String password)
109 | throws ReflectiveOperationException, SQLException, IOException {
110 | return this.connect(classLoader, dir, this.stringGetter.getJdbcString(dir, hostname, database), user, password);
111 | }
112 |
113 | public Connection connect(Path dir, String hostname, String database, String user, String password)
114 | throws ReflectiveOperationException, SQLException, IOException {
115 | return this.connect(dir, this.stringGetter.getJdbcString(dir, hostname, database), user, password);
116 | }
117 |
118 | public Connection connect(ClassLoader classLoader, Path dir, String jdbc, String user, String password)
119 | throws ReflectiveOperationException, SQLException, IOException {
120 | return this.connector.connect(classLoader, dir, jdbc, user, password);
121 | }
122 |
123 | public Connection connect(Path dir, String jdbc, String user, String password) throws IOException, ReflectiveOperationException, SQLException {
124 | return this.connector.connect(new IsolatedClassLoader(new URL[]{this.baseLibrary.getClassLoaderURL()}), dir, jdbc, user, password);
125 | }
126 |
127 | public ConnectionSource connectToORM(Path dir, String hostname, String database, String user, String password)
128 | throws ReflectiveOperationException, IOException, SQLException, URISyntaxException {
129 | if (this.driver.getOriginal() == null) {
130 | IsolatedClassLoader classLoader = new IsolatedClassLoader(new URL[] {this.baseLibrary.getClassLoaderURL()});
131 | Class> driverClass = classLoader.loadClass(
132 | switch (this) {
133 | case H2_LEGACY_V1, H2 -> "org.h2.Driver";
134 | case MYSQL -> "com.mysql.cj.jdbc.NonRegisteringDriver";
135 | case MARIADB -> "org.mariadb.jdbc.Driver";
136 | case POSTGRESQL -> "org.postgresql.Driver";
137 | case SQLITE -> "org.sqlite.JDBC";
138 | }
139 | );
140 |
141 | this.driver.setOriginal((Driver) driverClass.getConstructor().newInstance());
142 | DriverManager.registerDriver(this.driver);
143 | }
144 |
145 | String jdbc = this.stringGetter.getJdbcString(dir, hostname, database);
146 | boolean h2 = this.baseLibrary == BaseLibrary.H2_V1 || this.baseLibrary == BaseLibrary.H2_V2;
147 | return new JdbcPooledConnectionSource(this.driver.getInitializer() + jdbc,
148 | h2 ? null : user, h2 ? null : password, DatabaseTypeUtils.createDatabaseType(jdbc));
149 | }
150 |
151 | private static Connection fromDriver(Class> connectionClass, String jdbc, String user, String password, boolean register)
152 | throws ReflectiveOperationException, SQLException {
153 | Constructor> legacyConstructor = connectionClass.getConstructor();
154 |
155 | Properties info = new Properties();
156 | if (user != null) {
157 | info.put("user", user);
158 | }
159 |
160 | if (password != null) {
161 | info.put("password", password);
162 | }
163 |
164 | Object driver = legacyConstructor.newInstance();
165 |
166 | DriverManager.deregisterDriver((Driver) driver);
167 | if (register) {
168 | DriverManager.registerDriver((Driver) driver);
169 | }
170 |
171 | Method connect = connectionClass.getDeclaredMethod("connect", String.class, Properties.class);
172 | connect.setAccessible(true);
173 | return (Connection) connect.invoke(driver, jdbc, info);
174 | }
175 |
176 | public interface DatabaseConnector {
177 | Connection connect(ClassLoader classLoader, Path dir, String jdbc, String user, String password)
178 | throws ReflectiveOperationException, SQLException, IOException;
179 | }
180 |
181 | public interface DatabaseStringGetter {
182 | String getJdbcString(Path dir, String hostname, String database);
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/dependencies/IsolatedClassLoader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.dependencies;
19 |
20 | import java.net.URL;
21 | import java.net.URLClassLoader;
22 |
23 | public class IsolatedClassLoader extends URLClassLoader {
24 |
25 | public IsolatedClassLoader(URL[] urls) {
26 | super(urls, ClassLoader.getSystemClassLoader().getParent());
27 | }
28 |
29 | static {
30 | ClassLoader.registerAsParallelCapable();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/dependencies/IsolatedDriver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.dependencies;
19 |
20 | import java.sql.Connection;
21 | import java.sql.Driver;
22 | import java.sql.DriverPropertyInfo;
23 | import java.sql.SQLException;
24 | import java.sql.SQLFeatureNotSupportedException;
25 | import java.util.Properties;
26 | import java.util.logging.Logger;
27 |
28 | public class IsolatedDriver implements Driver {
29 |
30 | private final String initializer;
31 | private Driver original;
32 |
33 | public IsolatedDriver(String initializer) {
34 | this.initializer = initializer;
35 | }
36 |
37 | public String getInitializer() {
38 | return this.initializer;
39 | }
40 |
41 | public Driver getOriginal() {
42 | return this.original;
43 | }
44 |
45 | public void setOriginal(Driver driver) {
46 | this.original = driver;
47 | }
48 |
49 | @Override
50 | public Connection connect(String url, Properties info) throws SQLException {
51 | if (url.startsWith(this.initializer)) {
52 | return this.original.connect(url.substring(this.initializer.length()), info);
53 | }
54 |
55 | return null;
56 | }
57 |
58 | @Override
59 | public boolean acceptsURL(String url) throws SQLException {
60 | if (url.startsWith(this.initializer)) {
61 | if (this.original == null) {
62 | return false;
63 | }
64 |
65 | return this.original.acceptsURL(url.substring(this.initializer.length()));
66 | }
67 |
68 | return false;
69 | }
70 |
71 | @Override
72 | public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
73 | if (url.startsWith(this.initializer)) {
74 | return this.original.getPropertyInfo(url.substring(this.initializer.length()), info);
75 | }
76 |
77 | return new DriverPropertyInfo[0];
78 | }
79 |
80 | @Override
81 | public int getMajorVersion() {
82 | return this.original.getMajorVersion();
83 | }
84 |
85 | @Override
86 | public int getMinorVersion() {
87 | return this.original.getMinorVersion();
88 | }
89 |
90 | @Override
91 | public boolean jdbcCompliant() {
92 | return this.original.jdbcCompliant();
93 | }
94 |
95 | @Override
96 | public Logger getParentLogger() throws SQLFeatureNotSupportedException {
97 | return this.original.getParentLogger();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/AuthPluginReloadEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | public class AuthPluginReloadEvent {
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/AuthUnregisterEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | public class AuthUnregisterEvent {
21 |
22 | private final String nickname;
23 |
24 | public AuthUnregisterEvent(String nickname) {
25 | this.nickname = nickname;
26 | }
27 |
28 | public String getNickname() {
29 | return this.nickname;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/ChangePasswordEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import net.elytrium.limboauth.model.RegisteredPlayer;
21 | import org.checkerframework.checker.nullness.qual.Nullable;
22 |
23 | public class ChangePasswordEvent {
24 |
25 | private final RegisteredPlayer playerInfo;
26 | @Nullable
27 | private final String oldPassword;
28 | private final String oldHash;
29 | private final String newPassword;
30 | private final String newHash;
31 |
32 | public ChangePasswordEvent(RegisteredPlayer playerInfo, @Nullable String oldPassword,
33 | String oldHash, String newPassword, String newHash) {
34 | this.playerInfo = playerInfo;
35 | this.oldPassword = oldPassword;
36 | this.oldHash = oldHash;
37 | this.newPassword = newPassword;
38 | this.newHash = newHash;
39 | }
40 |
41 | public RegisteredPlayer getPlayerInfo() {
42 | return this.playerInfo;
43 | }
44 |
45 | @Nullable
46 | public String getOldPassword() {
47 | return this.oldPassword;
48 | }
49 |
50 | public String getOldHash() {
51 | return this.oldHash;
52 | }
53 |
54 | public String getNewPassword() {
55 | return this.newPassword;
56 | }
57 |
58 | public String getNewHash() {
59 | return this.newHash;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PostAuthorizationEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import java.util.function.Consumer;
21 | import net.elytrium.limboapi.api.player.LimboPlayer;
22 | import net.elytrium.limboauth.model.RegisteredPlayer;
23 |
24 | public class PostAuthorizationEvent extends PostEvent {
25 |
26 | public PostAuthorizationEvent(Consumer onComplete, LimboPlayer player, RegisteredPlayer playerInfo, String password) {
27 | super(onComplete, player, playerInfo, password);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PostEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import java.util.function.Consumer;
21 | import net.elytrium.limboapi.api.player.LimboPlayer;
22 | import net.elytrium.limboauth.model.RegisteredPlayer;
23 |
24 | public abstract class PostEvent extends TaskEvent {
25 |
26 | private final LimboPlayer player;
27 | private final RegisteredPlayer playerInfo;
28 | private final String password;
29 |
30 | protected PostEvent(Consumer onComplete, LimboPlayer player, RegisteredPlayer playerInfo, String password) {
31 | super(onComplete);
32 |
33 | this.player = player;
34 | this.playerInfo = playerInfo;
35 | this.password = password;
36 | }
37 |
38 | protected PostEvent(Consumer onComplete, Result result, LimboPlayer player, RegisteredPlayer playerInfo, String password) {
39 | super(onComplete, result);
40 |
41 | this.player = player;
42 | this.playerInfo = playerInfo;
43 | this.password = password;
44 | }
45 |
46 | public LimboPlayer getPlayer() {
47 | return this.player;
48 | }
49 |
50 | public RegisteredPlayer getPlayerInfo() {
51 | return this.playerInfo;
52 | }
53 |
54 | public String getPassword() {
55 | return this.password;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PostRegisterEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import java.util.function.Consumer;
21 | import net.elytrium.limboapi.api.player.LimboPlayer;
22 | import net.elytrium.limboauth.model.RegisteredPlayer;
23 |
24 | public class PostRegisterEvent extends PostEvent {
25 |
26 | public PostRegisterEvent(Consumer onComplete, LimboPlayer player, RegisteredPlayer playerInfo, String password) {
27 | super(onComplete, player, playerInfo, password);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PreAuthorizationEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import com.velocitypowered.api.proxy.Player;
21 | import java.util.function.Consumer;
22 | import net.elytrium.limboauth.model.RegisteredPlayer;
23 |
24 | public class PreAuthorizationEvent extends PreEvent {
25 |
26 | private final RegisteredPlayer playerInfo;
27 |
28 | public PreAuthorizationEvent(Consumer onComplete, Result result, Player player, RegisteredPlayer playerInfo) {
29 | super(onComplete, result, player);
30 |
31 | this.playerInfo = playerInfo;
32 | }
33 |
34 | public RegisteredPlayer getPlayerInfo() {
35 | return this.playerInfo;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PreEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import com.velocitypowered.api.proxy.Player;
21 | import java.util.function.Consumer;
22 |
23 | public abstract class PreEvent extends TaskEvent {
24 |
25 | private final Player player;
26 |
27 | protected PreEvent(Consumer onComplete, Result result, Player player) {
28 | super(onComplete, result);
29 |
30 | this.player = player;
31 | }
32 |
33 | public Player getPlayer() {
34 | return this.player;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/PreRegisterEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import com.velocitypowered.api.proxy.Player;
21 | import java.util.function.Consumer;
22 |
23 | public class PreRegisterEvent extends PreEvent {
24 |
25 | public PreRegisterEvent(Consumer onComplete, Result result, Player player) {
26 | super(onComplete, result, player);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/event/TaskEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.event;
19 |
20 | import java.util.function.Consumer;
21 | import net.elytrium.limboauth.LimboAuth;
22 | import net.elytrium.limboauth.Settings;
23 | import net.kyori.adventure.text.Component;
24 | import org.jetbrains.annotations.NotNull;
25 |
26 | public abstract class TaskEvent {
27 |
28 | private static Component DEFAULT_REASON;
29 |
30 | private final Consumer onComplete;
31 |
32 | private Result result = Result.NORMAL;
33 | private Component reason = DEFAULT_REASON;
34 |
35 | public TaskEvent(Consumer onComplete) {
36 | this.onComplete = onComplete;
37 | }
38 |
39 | public TaskEvent(Consumer onComplete, Result result) {
40 | this.onComplete = onComplete;
41 | this.result = result;
42 | }
43 |
44 | public void complete(@NotNull Result result) {
45 | if (this.result != Result.WAIT) {
46 | return;
47 | }
48 |
49 | this.result = result;
50 | this.onComplete.accept(this);
51 | }
52 |
53 | public void completeAndCancel(@NotNull Component reason) {
54 | if (this.result != Result.WAIT) {
55 | return;
56 | }
57 |
58 | this.cancel(reason);
59 | this.onComplete.accept(this);
60 | }
61 |
62 | public void cancel(@NotNull Component reason) {
63 | this.result = Result.CANCEL;
64 | this.reason = reason;
65 | }
66 |
67 | public void setResult(@NotNull Result result) {
68 | this.result = result;
69 | }
70 |
71 | public Result getResult() {
72 | return this.result;
73 | }
74 |
75 | public Component getReason() {
76 | return this.reason;
77 | }
78 |
79 | public static void reload() {
80 | DEFAULT_REASON = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.EVENT_CANCELLED);
81 | }
82 |
83 | public enum Result {
84 |
85 | CANCEL,
86 | BYPASS,
87 | NORMAL,
88 | WAIT
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/floodgate/FloodgateApiHolder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.floodgate;
19 |
20 | import java.util.UUID;
21 | import org.geysermc.floodgate.api.FloodgateApi;
22 |
23 | /**
24 | * Holder class for optional floodgate feature, we can't inject of optional plugins without holders due to Velocity structure.
25 | */
26 | public class FloodgateApiHolder {
27 |
28 | private final FloodgateApi floodgateApi;
29 |
30 | public FloodgateApiHolder() {
31 | this.floodgateApi = FloodgateApi.getInstance();
32 | }
33 |
34 | public boolean isFloodgatePlayer(UUID uuid) {
35 | return this.floodgateApi.isFloodgatePlayer(uuid);
36 | }
37 |
38 | public int getPrefixLength() {
39 | return this.floodgateApi.getPlayerPrefix().length();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/handler/AuthSessionHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.handler;
19 |
20 | import at.favre.lib.crypto.bcrypt.BCrypt;
21 | import com.google.common.primitives.Longs;
22 | import com.j256.ormlite.dao.Dao;
23 | import com.velocitypowered.api.proxy.Player;
24 | import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
25 | import dev.samstevens.totp.code.CodeVerifier;
26 | import dev.samstevens.totp.code.DefaultCodeGenerator;
27 | import dev.samstevens.totp.code.DefaultCodeVerifier;
28 | import dev.samstevens.totp.time.SystemTimeProvider;
29 | import io.netty.buffer.ByteBuf;
30 | import io.whitfin.siphash.SipHasher;
31 | import java.nio.charset.StandardCharsets;
32 | import java.sql.SQLException;
33 | import java.text.MessageFormat;
34 | import java.util.List;
35 | import java.util.Locale;
36 | import java.util.UUID;
37 | import java.util.concurrent.ScheduledFuture;
38 | import java.util.concurrent.TimeUnit;
39 | import java.util.stream.Collectors;
40 | import net.elytrium.commons.kyori.serialization.Serializer;
41 | import net.elytrium.limboapi.api.Limbo;
42 | import net.elytrium.limboapi.api.LimboSessionHandler;
43 | import net.elytrium.limboapi.api.player.LimboPlayer;
44 | import net.elytrium.limboauth.LimboAuth;
45 | import net.elytrium.limboauth.Settings;
46 | import net.elytrium.limboauth.event.PostAuthorizationEvent;
47 | import net.elytrium.limboauth.event.PostRegisterEvent;
48 | import net.elytrium.limboauth.event.TaskEvent;
49 | import net.elytrium.limboauth.migration.MigrationHash;
50 | import net.elytrium.limboauth.model.RegisteredPlayer;
51 | import net.elytrium.limboauth.model.SQLRuntimeException;
52 | import net.kyori.adventure.bossbar.BossBar;
53 | import net.kyori.adventure.text.Component;
54 | import net.kyori.adventure.title.Title;
55 | import org.checkerframework.checker.nullness.qual.Nullable;
56 |
57 | public class AuthSessionHandler implements LimboSessionHandler {
58 |
59 | public static final CodeVerifier TOTP_CODE_VERIFIER = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider());
60 | private static final BCrypt.Verifyer HASH_VERIFIER = BCrypt.verifyer();
61 | private static final BCrypt.Hasher HASHER = BCrypt.withDefaults();
62 |
63 | private static Component ratelimited;
64 | private static BossBar.Color bossbarColor;
65 | private static BossBar.Overlay bossbarOverlay;
66 | private static Component ipLimitKick;
67 | private static Component databaseErrorKick;
68 | private static String wrongNicknameCaseKick;
69 | private static Component timesUp;
70 | private static Component registerSuccessful;
71 | @Nullable
72 | private static Title registerSuccessfulTitle;
73 | private static Component[] loginWrongPassword;
74 | private static Component loginWrongPasswordKick;
75 | private static Component totp;
76 | @Nullable
77 | private static Title totpTitle;
78 | private static Component register;
79 | @Nullable
80 | private static Title registerTitle;
81 | private static Component[] login;
82 | @Nullable
83 | private static Title loginTitle;
84 | private static Component registerDifferentPasswords;
85 | private static Component registerPasswordTooLong;
86 | private static Component registerPasswordTooShort;
87 | private static Component registerPasswordUnsafe;
88 | private static Component loginSuccessful;
89 | private static Component sessionExpired;
90 | @Nullable
91 | private static Title loginSuccessfulTitle;
92 | @Nullable
93 | private static MigrationHash migrationHash;
94 |
95 | private final Dao playerDao;
96 | private final Player proxyPlayer;
97 | private final LimboAuth plugin;
98 |
99 | private final long joinTime = System.currentTimeMillis();
100 | private final BossBar bossBar = BossBar.bossBar(
101 | Component.empty(),
102 | 1.0F,
103 | bossbarColor,
104 | bossbarOverlay
105 | );
106 | private final boolean loginOnlyByMod = Settings.IMP.MAIN.MOD.ENABLED && Settings.IMP.MAIN.MOD.LOGIN_ONLY_BY_MOD;
107 |
108 | @Nullable
109 | private RegisteredPlayer playerInfo;
110 |
111 | private ScheduledFuture> authMainTask;
112 |
113 | private LimboPlayer player;
114 | private int attempts = Settings.IMP.MAIN.LOGIN_ATTEMPTS;
115 | private boolean totpState;
116 | private String tempPassword;
117 | private boolean tokenReceived;
118 |
119 | public AuthSessionHandler(Dao playerDao, Player proxyPlayer, LimboAuth plugin, @Nullable RegisteredPlayer playerInfo) {
120 | this.playerDao = playerDao;
121 | this.proxyPlayer = proxyPlayer;
122 | this.plugin = plugin;
123 | this.playerInfo = playerInfo;
124 | }
125 |
126 | @Override
127 | public void onSpawn(Limbo server, LimboPlayer player) {
128 | this.player = player;
129 |
130 | if (Settings.IMP.MAIN.DISABLE_FALLING) {
131 | this.player.disableFalling();
132 | } else {
133 | this.player.enableFalling();
134 | }
135 |
136 | Serializer serializer = LimboAuth.getSerializer();
137 |
138 | if (this.playerInfo == null) {
139 | try {
140 | String ip = this.proxyPlayer.getRemoteAddress().getAddress().getHostAddress();
141 | List alreadyRegistered = this.playerDao.queryForEq(RegisteredPlayer.IP_FIELD, ip);
142 | if (alreadyRegistered != null) {
143 | int sizeOfValidRegistrations = alreadyRegistered.size();
144 | if (Settings.IMP.MAIN.IP_LIMIT_VALID_TIME > 0) {
145 | for (RegisteredPlayer registeredPlayer : alreadyRegistered.stream()
146 | .filter(registeredPlayer -> registeredPlayer.getRegDate() < System.currentTimeMillis() - Settings.IMP.MAIN.IP_LIMIT_VALID_TIME)
147 | .collect(Collectors.toList())) {
148 | registeredPlayer.setIP("");
149 | this.playerDao.update(registeredPlayer);
150 | --sizeOfValidRegistrations;
151 | }
152 | }
153 |
154 | if (sizeOfValidRegistrations >= Settings.IMP.MAIN.IP_LIMIT_REGISTRATIONS) {
155 | this.proxyPlayer.disconnect(ipLimitKick);
156 | return;
157 | }
158 | }
159 | } catch (SQLException e) {
160 | this.proxyPlayer.disconnect(databaseErrorKick);
161 | throw new SQLRuntimeException(e);
162 | }
163 | } else {
164 | if (!this.proxyPlayer.getUsername().equals(this.playerInfo.getNickname())) {
165 | this.proxyPlayer.disconnect(serializer.deserialize(
166 | MessageFormat.format(wrongNicknameCaseKick, this.playerInfo.getNickname(), this.proxyPlayer.getUsername()))
167 | );
168 | return;
169 | }
170 |
171 | this.plugin.addAuthenticatingPlayer(player.getProxyPlayer().getUsername(), this);
172 | }
173 |
174 | boolean bossBarEnabled = !this.loginOnlyByMod && Settings.IMP.MAIN.ENABLE_BOSSBAR;
175 | int authTime = Settings.IMP.MAIN.AUTH_TIME;
176 | float multiplier = 1000.0F / authTime;
177 | this.authMainTask = this.player.getScheduledExecutor().scheduleWithFixedDelay(() -> {
178 | if (System.currentTimeMillis() - this.joinTime > authTime) {
179 | this.proxyPlayer.disconnect(timesUp);
180 | } else {
181 | if (bossBarEnabled) {
182 | float secondsLeft = (authTime - (System.currentTimeMillis() - this.joinTime)) / 1000.0F;
183 | this.bossBar.name(serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.BOSSBAR, (int) secondsLeft)));
184 | // It's possible, that the progress value can overcome 1, e.g. 1.0000001.
185 | this.bossBar.progress(Math.min(1.0F, secondsLeft * multiplier));
186 | }
187 | }
188 | }, 0, 1, TimeUnit.SECONDS);
189 |
190 | if (bossBarEnabled) {
191 | this.proxyPlayer.showBossBar(this.bossBar);
192 | }
193 |
194 | if (!this.loginOnlyByMod) {
195 | this.sendMessage(true);
196 | }
197 | }
198 |
199 | @Override
200 | public void onChat(String message) {
201 | if (this.loginOnlyByMod) {
202 | return;
203 | }
204 |
205 | if (!LimboAuth.RATELIMITER.attempt(this.proxyPlayer.getRemoteAddress().getAddress())) {
206 | this.proxyPlayer.sendMessage(AuthSessionHandler.ratelimited);
207 | return;
208 | }
209 |
210 | String[] args = message.split(" ");
211 | if (args.length != 0 && this.checkArgsLength(args.length)) {
212 | Command command = Command.parse(args[0]);
213 | if (command == Command.REGISTER && !this.totpState && this.playerInfo == null) {
214 | String password = args[1];
215 | if (this.checkPasswordsRepeat(args) && this.checkPasswordLength(password) && this.checkPasswordStrength(password)) {
216 | this.saveTempPassword(password);
217 | RegisteredPlayer registeredPlayer = new RegisteredPlayer(this.proxyPlayer).setPassword(password);
218 |
219 | try {
220 | this.playerDao.create(registeredPlayer);
221 | this.playerInfo = registeredPlayer;
222 | } catch (SQLException e) {
223 | this.proxyPlayer.disconnect(databaseErrorKick);
224 | throw new SQLRuntimeException(e);
225 | }
226 |
227 | this.proxyPlayer.sendMessage(registerSuccessful);
228 | if (registerSuccessfulTitle != null) {
229 | this.proxyPlayer.showTitle(registerSuccessfulTitle);
230 | }
231 |
232 | this.plugin.getServer().getEventManager()
233 | .fire(new PostRegisterEvent(this::finishAuth, this.player, this.playerInfo, this.tempPassword))
234 | .thenAcceptAsync(this::finishAuth);
235 | }
236 |
237 | // {@code return} placed here (not above), because
238 | // AuthSessionHandler#checkPasswordsRepeat, AuthSessionHandler#checkPasswordLength, and AuthSessionHandler#checkPasswordStrength methods are
239 | // invoking Player#sendMessage that sends its own message in case if the return value is false.
240 | // If we don't place {@code return} here, an another message (AuthSessionHandler#sendMessage) will be sent.
241 | return;
242 | } else if (command == Command.LOGIN && !this.totpState && this.playerInfo != null) {
243 | String password = args[1];
244 | this.saveTempPassword(password);
245 |
246 | if (password.length() > 0 && checkPassword(password, this.playerInfo, this.playerDao)) {
247 | if (this.playerInfo.getTotpToken().isEmpty()) {
248 | this.finishLogin();
249 | } else {
250 | this.totpState = true;
251 | this.sendMessage(true);
252 | }
253 | } else if (--this.attempts != 0) {
254 | this.proxyPlayer.sendMessage(loginWrongPassword[this.attempts - 1]);
255 | this.checkBruteforceAttempts();
256 | } else {
257 | this.proxyPlayer.disconnect(loginWrongPasswordKick);
258 | }
259 |
260 | return;
261 | } else if (command == Command.TOTP && this.totpState && this.playerInfo != null) {
262 | if (TOTP_CODE_VERIFIER.isValidCode(this.playerInfo.getTotpToken(), args[1])) {
263 | this.finishLogin();
264 | return;
265 | } else {
266 | this.checkBruteforceAttempts();
267 | }
268 | }
269 | }
270 |
271 | this.sendMessage(false);
272 | }
273 |
274 | @Override
275 | public void onGeneric(Object packet) {
276 | if (Settings.IMP.MAIN.MOD.ENABLED && packet instanceof PluginMessagePacket) {
277 | PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
278 | String channel = pluginMessage.getChannel();
279 |
280 | if (channel.equals("MC|Brand") || channel.equals("minecraft:brand")) {
281 | // Minecraft can't handle the plugin message immediately after going to the PLAY
282 | // state, so we have to postpone sending it
283 | if (Settings.IMP.MAIN.MOD.ENABLED) {
284 | this.proxyPlayer.sendPluginMessage(this.plugin.getChannelIdentifier(this.proxyPlayer), new byte[0]);
285 | }
286 | } else if (channel.equals(this.plugin.getChannelIdentifier(this.proxyPlayer).getId())) {
287 | if (this.tokenReceived) {
288 | this.checkBruteforceAttempts();
289 | this.proxyPlayer.disconnect(Component.empty());
290 | return;
291 | }
292 |
293 | this.tokenReceived = true;
294 |
295 | if (this.playerInfo == null) {
296 | return;
297 | }
298 |
299 | ByteBuf data = pluginMessage.content();
300 |
301 | if (data.readableBytes() < 16) {
302 | this.checkBruteforceAttempts();
303 | this.proxyPlayer.sendMessage(sessionExpired);
304 | return;
305 | }
306 |
307 | long issueTime = data.readLong();
308 | long hash = data.readLong();
309 |
310 | if (this.playerInfo.getTokenIssuedAt() > issueTime) {
311 | this.proxyPlayer.sendMessage(sessionExpired);
312 | return;
313 | }
314 |
315 | byte[] lowercaseNicknameSerialized = this.playerInfo.getLowercaseNickname().getBytes(StandardCharsets.UTF_8);
316 | long correctHash = SipHasher.init(Settings.IMP.MAIN.MOD.VERIFY_KEY)
317 | .update(lowercaseNicknameSerialized)
318 | .update(Longs.toByteArray(issueTime))
319 | .digest();
320 |
321 | if (hash != correctHash) {
322 | this.checkBruteforceAttempts();
323 | this.proxyPlayer.sendMessage(sessionExpired);
324 | return;
325 | }
326 |
327 | this.finishAuth();
328 | }
329 | }
330 | }
331 |
332 | private void checkBruteforceAttempts() {
333 | this.plugin.incrementBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress());
334 | if (this.plugin.getBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress()) >= Settings.IMP.MAIN.BRUTEFORCE_MAX_ATTEMPTS) {
335 | this.proxyPlayer.disconnect(loginWrongPasswordKick);
336 | }
337 | }
338 |
339 | private void saveTempPassword(String password) {
340 | this.tempPassword = password;
341 | }
342 |
343 | @Override
344 | public void onDisconnect() {
345 | if (this.authMainTask != null) {
346 | this.authMainTask.cancel(true);
347 | }
348 |
349 | this.proxyPlayer.hideBossBar(this.bossBar);
350 | this.plugin.removeAuthenticatingPlayer(this.player.getProxyPlayer().getUsername());
351 | }
352 |
353 | private void sendMessage(boolean sendTitle) {
354 | if (this.totpState) {
355 | this.proxyPlayer.sendMessage(totp);
356 | if (sendTitle && totpTitle != null) {
357 | this.proxyPlayer.showTitle(totpTitle);
358 | }
359 | } else if (this.playerInfo == null) {
360 | this.proxyPlayer.sendMessage(register);
361 | if (sendTitle && registerTitle != null) {
362 | this.proxyPlayer.showTitle(registerTitle);
363 | }
364 | } else {
365 | this.proxyPlayer.sendMessage(login[this.attempts - 1]);
366 | if (sendTitle && loginTitle != null) {
367 | this.proxyPlayer.showTitle(loginTitle);
368 | }
369 | }
370 | }
371 |
372 | private boolean checkArgsLength(int argsLength) {
373 | if (this.playerInfo == null && Settings.IMP.MAIN.REGISTER_NEED_REPEAT_PASSWORD) {
374 | return argsLength == 3;
375 | } else {
376 | return argsLength == 2;
377 | }
378 | }
379 |
380 | private boolean checkPasswordsRepeat(String[] args) {
381 | if (!Settings.IMP.MAIN.REGISTER_NEED_REPEAT_PASSWORD || args[1].equals(args[2])) {
382 | return true;
383 | } else {
384 | this.proxyPlayer.sendMessage(registerDifferentPasswords);
385 | return false;
386 | }
387 | }
388 |
389 | private boolean checkPasswordLength(String password) {
390 | int length = password.length();
391 | if (length > Settings.IMP.MAIN.MAX_PASSWORD_LENGTH) {
392 | this.proxyPlayer.sendMessage(registerPasswordTooLong);
393 | return false;
394 | } else if (length < Settings.IMP.MAIN.MIN_PASSWORD_LENGTH) {
395 | this.proxyPlayer.sendMessage(registerPasswordTooShort);
396 | return false;
397 | } else {
398 | return true;
399 | }
400 | }
401 |
402 | private boolean checkPasswordStrength(String password) {
403 | if (Settings.IMP.MAIN.CHECK_PASSWORD_STRENGTH && this.plugin.getUnsafePasswords().contains(password)) {
404 | this.proxyPlayer.sendMessage(registerPasswordUnsafe);
405 | return false;
406 | } else {
407 | return true;
408 | }
409 | }
410 |
411 | public void finishLogin() {
412 | this.proxyPlayer.sendMessage(loginSuccessful);
413 | if (loginSuccessfulTitle != null) {
414 | this.proxyPlayer.showTitle(loginSuccessfulTitle);
415 | }
416 |
417 | this.plugin.clearBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress());
418 |
419 | this.plugin.getServer().getEventManager()
420 | .fire(new PostAuthorizationEvent(this::finishAuth, this.player, this.playerInfo, this.tempPassword))
421 | .thenAcceptAsync(this::finishAuth);
422 | }
423 |
424 | private void finishAuth(TaskEvent event) {
425 | if (event.getResult() == TaskEvent.Result.CANCEL) {
426 | this.proxyPlayer.disconnect(event.getReason());
427 | return;
428 | } else if (event.getResult() == TaskEvent.Result.WAIT) {
429 | return;
430 | }
431 |
432 | this.finishAuth();
433 | }
434 |
435 | private void finishAuth() {
436 | if (Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.CLEAR_AFTER_LOGIN) {
437 | this.proxyPlayer.clearTitle();
438 | }
439 |
440 | try {
441 | this.plugin.updateLoginData(this.proxyPlayer);
442 | } catch (SQLException e) {
443 | throw new SQLRuntimeException(e);
444 | } catch (Throwable e) {
445 | e.printStackTrace();
446 | }
447 |
448 | this.plugin.cacheAuthUser(this.proxyPlayer);
449 | this.player.disconnect();
450 | }
451 |
452 | public static void reload() {
453 | Serializer serializer = LimboAuth.getSerializer();
454 | AuthSessionHandler.ratelimited = serializer.deserialize(Settings.IMP.MAIN.STRINGS.RATELIMITED);
455 | bossbarColor = Settings.IMP.MAIN.BOSSBAR_COLOR;
456 | bossbarOverlay = Settings.IMP.MAIN.BOSSBAR_OVERLAY;
457 | ipLimitKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.IP_LIMIT_KICK);
458 | databaseErrorKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.DATABASE_ERROR_KICK);
459 | wrongNicknameCaseKick = Settings.IMP.MAIN.STRINGS.WRONG_NICKNAME_CASE_KICK;
460 | timesUp = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TIMES_UP);
461 | registerSuccessful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL);
462 | if (Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_SUBTITLE.isEmpty()) {
463 | registerSuccessfulTitle = null;
464 | } else {
465 | registerSuccessfulTitle = Title.title(
466 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_TITLE),
467 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_SUBTITLE),
468 | Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()
469 | );
470 | }
471 | int loginAttempts = Settings.IMP.MAIN.LOGIN_ATTEMPTS;
472 | loginWrongPassword = new Component[loginAttempts];
473 | for (int i = 0; i < loginAttempts; ++i) {
474 | loginWrongPassword[i] = serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD, i + 1));
475 | }
476 | loginWrongPasswordKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD_KICK);
477 | totp = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP);
478 | if (Settings.IMP.MAIN.STRINGS.TOTP_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.TOTP_SUBTITLE.isEmpty()) {
479 | totpTitle = null;
480 | } else {
481 | totpTitle = Title.title(
482 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_TITLE),
483 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_SUBTITLE),
484 | Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()
485 | );
486 | }
487 | register = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER);
488 | if (Settings.IMP.MAIN.STRINGS.REGISTER_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.REGISTER_SUBTITLE.isEmpty()) {
489 | registerTitle = null;
490 | } else {
491 | registerTitle = Title.title(
492 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_TITLE),
493 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUBTITLE),
494 | Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()
495 | );
496 | }
497 | login = new Component[loginAttempts];
498 | for (int i = 0; i < loginAttempts; ++i) {
499 | login[i] = serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN, i + 1));
500 | }
501 | if (Settings.IMP.MAIN.STRINGS.LOGIN_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.LOGIN_SUBTITLE.isEmpty()) {
502 | loginTitle = null;
503 | } else {
504 | loginTitle = Title.title(
505 | serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_TITLE, loginAttempts)),
506 | serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_SUBTITLE, loginAttempts)),
507 | Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()
508 | );
509 | }
510 | registerDifferentPasswords = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_DIFFERENT_PASSWORDS);
511 | registerPasswordTooLong = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_TOO_LONG);
512 | registerPasswordTooShort = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_TOO_SHORT);
513 | registerPasswordUnsafe = serializer.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_UNSAFE);
514 | loginSuccessful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL);
515 | sessionExpired = serializer.deserialize(Settings.IMP.MAIN.STRINGS.MOD_SESSION_EXPIRED);
516 | if (Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_SUBTITLE.isEmpty()) {
517 | loginSuccessfulTitle = null;
518 | } else {
519 | loginSuccessfulTitle = Title.title(
520 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_TITLE),
521 | serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_SUBTITLE),
522 | Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()
523 | );
524 | }
525 |
526 | migrationHash = Settings.IMP.MAIN.MIGRATION_HASH;
527 | }
528 |
529 | public static boolean checkPassword(String password, RegisteredPlayer player, Dao playerDao) {
530 | String hash = player.getHash();
531 | boolean isCorrect = HASH_VERIFIER.verify(
532 | password.getBytes(StandardCharsets.UTF_8),
533 | hash.replace("BCRYPT$", "$2a$").getBytes(StandardCharsets.UTF_8)
534 | ).verified;
535 |
536 | if (!isCorrect && migrationHash != null) {
537 | isCorrect = migrationHash.checkPassword(hash, password);
538 | if (isCorrect) {
539 | player.setPassword(password);
540 | try {
541 | playerDao.update(player);
542 | } catch (SQLException e) {
543 | throw new SQLRuntimeException(e);
544 | }
545 | }
546 | }
547 |
548 | return isCorrect;
549 | }
550 |
551 | public static RegisteredPlayer fetchInfo(Dao playerDao, UUID uuid) {
552 | try {
553 | List playerList = playerDao.queryForEq(RegisteredPlayer.PREMIUM_UUID_FIELD, uuid.toString());
554 | return (playerList != null ? playerList.size() : 0) == 0 ? null : playerList.get(0);
555 | } catch (SQLException e) {
556 | throw new SQLRuntimeException(e);
557 | }
558 | }
559 |
560 | public static RegisteredPlayer fetchInfo(Dao playerDao, String nickname) {
561 | return AuthSessionHandler.fetchInfoLowercased(playerDao, nickname.toLowerCase(Locale.ROOT));
562 | }
563 |
564 | public static RegisteredPlayer fetchInfoLowercased(Dao playerDao, String nickname) {
565 | try {
566 | List playerList = playerDao.queryForEq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, nickname);
567 | return (playerList != null ? playerList.size() : 0) == 0 ? null : playerList.get(0);
568 | } catch (SQLException e) {
569 | throw new SQLRuntimeException(e);
570 | }
571 | }
572 |
573 | /**
574 | * Use {@link RegisteredPlayer#genHash(String)} or {@link RegisteredPlayer#setPassword}
575 | */
576 | @Deprecated()
577 | public static String genHash(String password) {
578 | return HASHER.hashToString(Settings.IMP.MAIN.BCRYPT_COST, password.toCharArray());
579 | }
580 |
581 |
582 | private enum Command {
583 |
584 | INVALID,
585 | REGISTER,
586 | LOGIN,
587 | TOTP;
588 |
589 | static Command parse(String command) {
590 | if (Settings.IMP.MAIN.REGISTER_COMMAND.contains(command)) {
591 | return Command.REGISTER;
592 | } else if (Settings.IMP.MAIN.LOGIN_COMMAND.contains(command)) {
593 | return Command.LOGIN;
594 | } else if (Settings.IMP.MAIN.TOTP_COMMAND.contains(command)) {
595 | return Command.TOTP;
596 | } else {
597 | return Command.INVALID;
598 | }
599 | }
600 | }
601 | }
602 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/listener/AuthListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.listener;
19 |
20 | import com.j256.ormlite.dao.Dao;
21 | import com.j256.ormlite.stmt.UpdateBuilder;
22 | import com.velocitypowered.api.event.PostOrder;
23 | import com.velocitypowered.api.event.Subscribe;
24 | import com.velocitypowered.api.event.connection.PostLoginEvent;
25 | import com.velocitypowered.api.event.connection.PreLoginEvent;
26 | import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
27 | import com.velocitypowered.api.event.player.GameProfileRequestEvent;
28 | import com.velocitypowered.api.proxy.InboundConnection;
29 | import com.velocitypowered.api.util.UuidUtils;
30 | import com.velocitypowered.proxy.connection.MinecraftConnection;
31 | import com.velocitypowered.proxy.connection.client.InitialInboundConnection;
32 | import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
33 | import java.lang.invoke.MethodHandle;
34 | import java.lang.invoke.MethodHandles;
35 | import java.sql.SQLException;
36 | import java.util.Locale;
37 | import java.util.UUID;
38 | import java.util.concurrent.TimeUnit;
39 | import net.elytrium.commons.utils.reflection.ReflectionException;
40 | import net.elytrium.limboapi.api.event.LoginLimboRegisterEvent;
41 | import net.elytrium.limboauth.LimboAuth;
42 | import net.elytrium.limboauth.LimboAuth.CachedPremiumUser;
43 | import net.elytrium.limboauth.LimboAuth.PremiumState;
44 | import net.elytrium.limboauth.Settings;
45 | import net.elytrium.limboauth.floodgate.FloodgateApiHolder;
46 | import net.elytrium.limboauth.handler.AuthSessionHandler;
47 | import net.elytrium.limboauth.model.RegisteredPlayer;
48 | import net.elytrium.limboauth.model.SQLRuntimeException;
49 | import net.kyori.adventure.text.Component;
50 |
51 | // TODO: Customizable events priority
52 | public class AuthListener {
53 |
54 | private static final MethodHandle DELEGATE_FIELD;
55 | //private static final MethodHandle LOGIN_FIELD;
56 |
57 | private final LimboAuth plugin;
58 | private final Dao playerDao;
59 | private final FloodgateApiHolder floodgateApi;
60 | private final Component errorOccurred;
61 |
62 | public AuthListener(LimboAuth plugin, Dao playerDao, FloodgateApiHolder floodgateApi) {
63 | this.plugin = plugin;
64 | this.playerDao = playerDao;
65 | this.floodgateApi = floodgateApi;
66 |
67 | this.errorOccurred = LimboAuth.getSerializer().deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED);
68 | }
69 |
70 | @Subscribe(order = PostOrder.LATE)
71 | public void onPreLoginEvent(PreLoginEvent event) {
72 | // Ignore this event if it is was denied by other plugin
73 | if (!event.getResult().isAllowed()) {
74 | return;
75 | }
76 |
77 | try {
78 | String username = event.getUsername();
79 | if (!event.getResult().isForceOfflineMode()) {
80 | if (this.plugin.isPremium(username)) {
81 | event.setResult(PreLoginEvent.PreLoginComponentResult.forceOnlineMode());
82 |
83 | try {
84 | if (!Settings.IMP.MAIN.ONLINE_MODE_NEED_AUTH_STRICT) {
85 | CachedPremiumUser premiumUser = this.plugin.getPremiumCache(username);
86 | MinecraftConnection connection = this.getConnection(event.getConnection());
87 | if (!connection.isClosed() && premiumUser != null && !premiumUser.isForcePremium()
88 | && this.plugin.isPremiumInternal(username.toLowerCase(Locale.ROOT)).getState() == PremiumState.UNKNOWN) {
89 | this.plugin.getPendingLogins().add(username);
90 |
91 | // As Velocity doesnt have any events for our usecase, just inject into netty
92 | connection.getChannel().closeFuture().addListener(future -> {
93 | // Player has failed premium verfication client-side, mark as offline-mode
94 | if (this.plugin.getPendingLogins().remove(username)) {
95 | this.plugin.setPremiumCacheLowercased(username.toLowerCase(Locale.ROOT), false);
96 | }
97 | });
98 | }
99 | }
100 | } catch (Throwable throwable) {
101 | throw new IllegalStateException("failed to track authentication process", throwable);
102 | }
103 | } else {
104 | event.setResult(PreLoginEvent.PreLoginComponentResult.forceOfflineMode());
105 | }
106 | } else {
107 | try {
108 | MinecraftConnection connection = this.getConnection(event.getConnection());
109 | if (!connection.isClosed()) {
110 | this.plugin.saveForceOfflineMode(username);
111 |
112 | // As Velocity doesnt have any events for our usecase, just inject into netty
113 | connection.getChannel().closeFuture().addListener(future -> {
114 | this.plugin.unsetForcedPreviously(username);
115 | });
116 | }
117 | } catch (Throwable throwable) {
118 | throw new IllegalStateException("failed to track client disconnection", throwable);
119 | }
120 | }
121 | } catch (Throwable throwable) {
122 | event.setResult(PreLoginComponentResult.denied(this.errorOccurred));
123 | throw throwable;
124 | }
125 | }
126 |
127 | private MinecraftConnection getConnection(InboundConnection inbound) throws Throwable {
128 | LoginInboundConnection inboundConnection = (LoginInboundConnection) inbound;
129 | InitialInboundConnection initialInbound = (InitialInboundConnection) DELEGATE_FIELD.invokeExact(inboundConnection);
130 | return initialInbound.getConnection();
131 | }
132 |
133 | // Temporarily disabled because some clients send UUID version 4 (random UUID) even if the player is cracked
134 | /*
135 | private boolean isPremiumByIdentifiedKey(InboundConnection inbound) throws Throwable {
136 | LoginInboundConnection inboundConnection = (LoginInboundConnection) inbound;
137 | InitialInboundConnection initialInbound = (InitialInboundConnection) DELEGATE_FIELD.invokeExact(inboundConnection);
138 | MinecraftConnection connection = initialInbound.getConnection();
139 | InitialLoginSessionHandler handler = (InitialLoginSessionHandler) connection.getSessionHandler();
140 |
141 | ServerLogin packet = (ServerLogin) LOGIN_FIELD.invokeExact(handler);
142 | if (packet == null) {
143 | return false;
144 | }
145 |
146 | UUID holder = packet.getHolderUuid();
147 | if (holder == null) {
148 | return false;
149 | }
150 |
151 | return holder.version() != 3;
152 | }
153 | */
154 |
155 | @Subscribe
156 | public void onPostLogin(PostLoginEvent event) {
157 | UUID uuid = event.getPlayer().getUniqueId();
158 | Runnable postLoginTask = this.plugin.getPostLoginTasks().remove(uuid);
159 | if (postLoginTask != null) {
160 | // We need to delay for player's client to finish switching the server, it takes a little time.
161 | this.plugin.getServer().getScheduler()
162 | .buildTask(this.plugin, postLoginTask)
163 | .delay(Settings.IMP.MAIN.PREMIUM_AND_FLOODGATE_MESSAGES_DELAY, TimeUnit.MILLISECONDS)
164 | .schedule();
165 | }
166 | }
167 |
168 | @Subscribe
169 | public void onLoginLimboRegister(LoginLimboRegisterEvent event) {
170 | // Player has completed online-mode authentication, can be sure that the player has premium account
171 | if (event.getPlayer().isOnlineMode()) {
172 | CachedPremiumUser premiumUser = this.plugin.getPremiumCache(event.getPlayer().getUsername());
173 | if (premiumUser != null) {
174 | premiumUser.setForcePremium(true);
175 | }
176 |
177 | this.plugin.getPendingLogins().remove(event.getPlayer().getUsername());
178 | }
179 |
180 | if (this.plugin.needAuth(event.getPlayer())) {
181 | event.addOnJoinCallback(() -> this.plugin.authPlayer(event.getPlayer()));
182 | }
183 | }
184 |
185 | @Subscribe(order = PostOrder.FIRST)
186 | public void onGameProfileRequest(GameProfileRequestEvent event) {
187 | if (Settings.IMP.MAIN.SAVE_UUID && (this.floodgateApi == null || !this.floodgateApi.isFloodgatePlayer(event.getOriginalProfile().getId()))) {
188 | RegisteredPlayer registeredPlayer = AuthSessionHandler.fetchInfo(this.playerDao, event.getOriginalProfile().getId());
189 |
190 | if (registeredPlayer != null && !registeredPlayer.getUuid().isEmpty()) {
191 | event.setGameProfile(event.getOriginalProfile().withId(UUID.fromString(registeredPlayer.getUuid())));
192 | return;
193 | }
194 | registeredPlayer = AuthSessionHandler.fetchInfo(this.playerDao, event.getUsername());
195 |
196 | if (registeredPlayer != null) {
197 | String currentUuid = registeredPlayer.getUuid();
198 |
199 | if (currentUuid.isEmpty()) {
200 | try {
201 | registeredPlayer.setUuid(event.getGameProfile().getId().toString());
202 | this.playerDao.update(registeredPlayer);
203 | } catch (SQLException e) {
204 | throw new SQLRuntimeException(e);
205 | }
206 | } else {
207 | event.setGameProfile(event.getOriginalProfile().withId(UUID.fromString(currentUuid)));
208 | }
209 | }
210 | } else if (event.isOnlineMode()) {
211 | try {
212 | UpdateBuilder updateBuilder = this.playerDao.updateBuilder();
213 | updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, event.getUsername().toLowerCase(Locale.ROOT));
214 | updateBuilder.updateColumnValue(RegisteredPlayer.HASH_FIELD, "");
215 | updateBuilder.update();
216 | } catch (SQLException e) {
217 | throw new SQLRuntimeException(e);
218 | }
219 | }
220 |
221 | if (Settings.IMP.MAIN.FORCE_OFFLINE_UUID) {
222 | event.setGameProfile(event.getOriginalProfile().withId(UuidUtils.generateOfflinePlayerUuid(event.getUsername())));
223 | }
224 |
225 | if (!event.isOnlineMode() && !Settings.IMP.MAIN.OFFLINE_MODE_PREFIX.isEmpty()) {
226 | event.setGameProfile(event.getOriginalProfile().withName(Settings.IMP.MAIN.OFFLINE_MODE_PREFIX + event.getUsername()));
227 | }
228 |
229 | if (event.isOnlineMode() && !Settings.IMP.MAIN.ONLINE_MODE_PREFIX.isEmpty()) {
230 | event.setGameProfile(event.getOriginalProfile().withName(Settings.IMP.MAIN.ONLINE_MODE_PREFIX + event.getUsername()));
231 | }
232 | }
233 |
234 | static {
235 | try {
236 | DELEGATE_FIELD = MethodHandles.privateLookupIn(LoginInboundConnection.class, MethodHandles.lookup())
237 | .findGetter(LoginInboundConnection.class, "delegate", InitialInboundConnection.class);
238 | //LOGIN_FIELD = MethodHandles.privateLookupIn(InitialLoginSessionHandler.class, MethodHandles.lookup())
239 | // .findGetter(InitialLoginSessionHandler.class, "login",ServerLoginPacket.class);
240 | } catch (NoSuchFieldException | IllegalAccessException e) {
241 | throw new ReflectionException(e);
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/listener/BackendEndpointsListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.listener;
19 |
20 | import com.google.common.io.ByteArrayDataInput;
21 | import com.google.common.io.ByteArrayDataOutput;
22 | import com.google.common.io.ByteStreams;
23 | import com.velocitypowered.api.event.Subscribe;
24 | import com.velocitypowered.api.event.connection.PluginMessageEvent;
25 | import com.velocitypowered.api.event.connection.PluginMessageEvent.ForwardResult;
26 | import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
27 | import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
28 | import com.velocitypowered.proxy.connection.MinecraftConnection;
29 | import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
30 | import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
31 | import io.netty.buffer.Unpooled;
32 | import java.util.AbstractMap.SimpleEntry;
33 | import java.util.Map;
34 | import java.util.function.Function;
35 | import net.elytrium.limboauth.LimboAuth;
36 | import net.elytrium.limboauth.Settings;
37 | import net.elytrium.limboauth.backend.Endpoint;
38 | import net.elytrium.limboauth.backend.type.LongDatabaseEndpoint;
39 | import net.elytrium.limboauth.backend.type.StringDatabaseEndpoint;
40 | import net.elytrium.limboauth.backend.type.StringEndpoint;
41 | import net.elytrium.limboauth.backend.type.UnknownEndpoint;
42 | import net.elytrium.limboauth.model.RegisteredPlayer;
43 |
44 | public class BackendEndpointsListener {
45 |
46 | public static final ChannelIdentifier API_CHANNEL = MinecraftChannelIdentifier.create("limboauth", "backend_api");
47 |
48 | public static final Map> TYPES = Map.ofEntries(
49 | new SimpleEntry<>("available_endpoints", plugin -> new StringEndpoint(plugin, "available_endpoints",
50 | username -> String.join(",", Settings.IMP.MAIN.BACKEND_API.ENABLED_ENDPOINTS))),
51 | new SimpleEntry<>("premium_state", lauth -> new StringEndpoint(lauth, "premium_state",
52 | username -> lauth.isPremiumInternal(username).getState().name())),
53 | new SimpleEntry<>("hash", plugin -> new StringDatabaseEndpoint(plugin, "hash", RegisteredPlayer::getHash)),
54 | new SimpleEntry<>("totp_token", plugin -> new StringDatabaseEndpoint(plugin, "totp_token", RegisteredPlayer::getTotpToken)),
55 | new SimpleEntry<>("reg_date", plugin -> new LongDatabaseEndpoint(plugin, "reg_date", RegisteredPlayer::getRegDate)),
56 | new SimpleEntry<>("uuid", plugin -> new StringDatabaseEndpoint(plugin, "uuid", RegisteredPlayer::getUuid)),
57 | new SimpleEntry<>("premium_uuid", plugin -> new StringDatabaseEndpoint(plugin, "premium_uuid", RegisteredPlayer::getPremiumUuid)),
58 | new SimpleEntry<>("ip", plugin -> new StringDatabaseEndpoint(plugin, "ip", RegisteredPlayer::getIP)),
59 | new SimpleEntry<>("login_ip", plugin -> new StringDatabaseEndpoint(plugin, "login_ip", RegisteredPlayer::getLoginIp)),
60 | new SimpleEntry<>("login_date", plugin -> new LongDatabaseEndpoint(plugin, "login_date", RegisteredPlayer::getLoginDate)),
61 | new SimpleEntry<>("token_issued_at", plugin -> new LongDatabaseEndpoint(plugin, "token_issued_at", RegisteredPlayer::getTokenIssuedAt))
62 | );
63 |
64 | private final LimboAuth plugin;
65 |
66 | public BackendEndpointsListener(LimboAuth plugin) {
67 | this.plugin = plugin;
68 |
69 | plugin.getServer().getChannelRegistrar().register(API_CHANNEL);
70 | }
71 |
72 | @Subscribe
73 | public void onRequest(PluginMessageEvent event) {
74 | if (event.getIdentifier() != API_CHANNEL) {
75 | return;
76 | }
77 |
78 | event.setResult(ForwardResult.handled());
79 | if (!(event.getSource() instanceof VelocityServerConnection server) || !server.isActive()) {
80 | return;
81 | }
82 |
83 | Endpoint endpoint;
84 | ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
85 | String dataType = in.readUTF();
86 | Function typeFunc = TYPES.get(dataType);
87 | if (typeFunc == null) {
88 | endpoint = new UnknownEndpoint(this.plugin, dataType);
89 | } else {
90 | endpoint = typeFunc.apply(this.plugin);
91 | endpoint.read(in);
92 | }
93 |
94 | // Please do not use ServerConnection::sendPluginMessage as it can easly throw exception due to delayed response
95 | // 1) Player can quit the server causing ServerConnection::sendPluginMessage to throw exception
96 | // 2) There are no proper way to check if ServerConnection is not closed
97 | MinecraftConnection connection = server.getConnection();
98 | if (connection != null && !connection.isClosed()) {
99 | ByteArrayDataOutput output = ByteStreams.newDataOutput();
100 | endpoint.write(output);
101 | connection.write(new PluginMessagePacket(API_CHANNEL.getId(), Unpooled.wrappedBuffer(output.toByteArray())));
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/migration/MigrationHash.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.migration;
19 |
20 | import com.google.common.hash.Hashing;
21 | import de.mkammerer.argon2.Argon2;
22 | import de.mkammerer.argon2.Argon2Factory;
23 | import java.nio.charset.StandardCharsets;
24 | import java.security.MessageDigest;
25 | import java.security.NoSuchAlgorithmException;
26 | import org.apache.commons.codec.binary.Hex;
27 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
28 |
29 | @SuppressWarnings("unused")
30 | public enum MigrationHash {
31 |
32 | AUTHME((hash, password) -> {
33 | String[] args = hash.split("\\$"); // $SHA$salt$hash
34 | return args.length == 4 && args[3].equals(getDigest(getDigest(password, "SHA-256") + args[2], "SHA-256"));
35 | }),
36 | AUTHME_NP((hash, password) -> {
37 | String[] args = hash.split("\\$"); // SHA$salt$hash
38 | return args.length == 3 && args[2].equals(getDigest(getDigest(password, "SHA-256") + args[1], "SHA-256"));
39 | }),
40 | ARGON2(new Argon2Verifier()),
41 | SHA512_DBA((hash, password) -> {
42 | String[] args = hash.split("\\$"); // SHA$salt$hash
43 | return args.length == 3 && args[2].equals(getDigest(getDigest(password, "SHA-512") + args[1], "SHA-512"));
44 | }),
45 | SHA512_NP((hash, password) -> {
46 | String[] args = hash.split("\\$"); // SHA$salt$hash
47 | return args.length == 3 && args[2].equals(getDigest(password + args[1], "SHA-512"));
48 | }),
49 | SHA512_P((hash, password) -> {
50 | String[] args = hash.split("\\$"); // $SHA$salt$hash
51 | return args.length == 4 && args[3].equals(getDigest(password + args[2], "SHA-512"));
52 | }),
53 | SHA256_NP((hash, password) -> {
54 | String[] args = hash.split("\\$"); // SHA$salt$hash
55 | return args.length == 3 && args[2].equals(getDigest(password + args[1], "SHA-256"));
56 | }),
57 | SHA256_P((hash, password) -> {
58 | String[] args = hash.split("\\$"); // $SHA$salt$hash
59 | return args.length == 4 && args[3].equals(getDigest(password + args[2], "SHA-256"));
60 | }),
61 | MD5((hash, password) -> hash.equals(getDigest(password, "MD5"))),
62 | MOON_SHA256((hash, password) -> {
63 | String[] args = hash.split("\\$"); // $SHA$hash
64 | return args.length == 3 && args[2].equals(getDigest(getDigest(password, "SHA-256"), "SHA-256"));
65 | }),
66 | SHA256_NO_SALT((hash, password) -> {
67 | String[] args = hash.split("\\$"); // $SHA$hash
68 | return args.length == 3 && args[2].equals(getDigest(password, "SHA-256"));
69 | }),
70 | SHA512_NO_SALT((hash, password) -> {
71 | String[] args = hash.split("\\$"); // $SHA$hash
72 | return args.length == 3 && args[2].equals(getDigest(password, "SHA-512"));
73 | }),
74 | SHA512_P_REVERSED_HASH((hash, password) -> {
75 | String[] args = hash.split("\\$"); // $SHA$hash$salt
76 | return args.length == 4 && args[2].equals(getDigest(password + args[3], "SHA-512"));
77 | }),
78 | SHA512_NLOGIN((hash, password) -> {
79 | String[] args = hash.split("\\$"); // $SHA$hash$salt
80 | return args.length == 4 && args[2].equals(getDigest(getDigest(password, "SHA-512") + args[3], "SHA-512"));
81 | }),
82 | @SuppressWarnings("UnstableApiUsage")
83 | CRC32C((hash, password) -> hash.equals(Hashing.crc32c().hashString(password, StandardCharsets.UTF_8).toString())),
84 | PLAINTEXT(String::equals);
85 |
86 | private final MigrationHashVerifier verifier;
87 |
88 | MigrationHash(MigrationHashVerifier verifier) {
89 | this.verifier = verifier;
90 | }
91 |
92 | public boolean checkPassword(String hash, String password) {
93 | return this.verifier.checkPassword(hash, password);
94 | }
95 |
96 | private static String getDigest(String string, String algorithm) {
97 | try {
98 | MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
99 | messageDigest.update(string.getBytes(StandardCharsets.UTF_8));
100 | byte[] array = messageDigest.digest();
101 | return Hex.encodeHexString(array);
102 | } catch (NoSuchAlgorithmException e) {
103 | throw new IllegalArgumentException(e);
104 | }
105 | }
106 |
107 | private static class Argon2Verifier implements MigrationHashVerifier {
108 |
109 | @MonotonicNonNull
110 | private Argon2 argon2;
111 |
112 | @Override
113 | public boolean checkPassword(String hash, String password) {
114 | if (this.argon2 == null) {
115 | this.argon2 = Argon2Factory.create();
116 | }
117 |
118 | return this.argon2.verify(hash, password.getBytes(StandardCharsets.UTF_8));
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/migration/MigrationHashVerifier.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.migration;
19 |
20 | public interface MigrationHashVerifier {
21 |
22 | boolean checkPassword(String hash, String password);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/model/RegisteredPlayer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.model;
19 |
20 | import at.favre.lib.crypto.bcrypt.BCrypt;
21 | import com.j256.ormlite.field.DatabaseField;
22 | import com.j256.ormlite.table.DatabaseTable;
23 | import com.velocitypowered.api.proxy.Player;
24 | import java.net.InetSocketAddress;
25 | import java.util.Locale;
26 | import java.util.UUID;
27 | import net.elytrium.limboauth.Settings;
28 |
29 | @DatabaseTable(tableName = "AUTH")
30 | public class RegisteredPlayer {
31 |
32 | public static final String NICKNAME_FIELD = "NICKNAME";
33 | public static final String LOWERCASE_NICKNAME_FIELD = "LOWERCASENICKNAME";
34 | public static final String HASH_FIELD = "HASH";
35 | public static final String IP_FIELD = "IP";
36 | public static final String LOGIN_IP_FIELD = "LOGINIP";
37 | public static final String TOTP_TOKEN_FIELD = "TOTPTOKEN";
38 | public static final String REG_DATE_FIELD = "REGDATE";
39 | public static final String LOGIN_DATE_FIELD = "LOGINDATE";
40 | public static final String UUID_FIELD = "UUID";
41 | public static final String PREMIUM_UUID_FIELD = "PREMIUMUUID";
42 | public static final String TOKEN_ISSUED_AT_FIELD = "ISSUEDTIME";
43 |
44 | private static final BCrypt.Hasher HASHER = BCrypt.withDefaults();
45 |
46 | @DatabaseField(canBeNull = false, columnName = NICKNAME_FIELD)
47 | private String nickname;
48 |
49 | @DatabaseField(id = true, columnName = LOWERCASE_NICKNAME_FIELD)
50 | private String lowercaseNickname;
51 |
52 | @DatabaseField(canBeNull = false, columnName = HASH_FIELD)
53 | private String hash = "";
54 |
55 | @DatabaseField(columnName = IP_FIELD, index = true)
56 | private String ip;
57 |
58 | @DatabaseField(columnName = TOTP_TOKEN_FIELD)
59 | private String totpToken = "";
60 |
61 | @DatabaseField(columnName = REG_DATE_FIELD)
62 | private Long regDate = System.currentTimeMillis();
63 |
64 | @DatabaseField(columnName = UUID_FIELD)
65 | private String uuid = "";
66 |
67 | @DatabaseField(columnName = RegisteredPlayer.PREMIUM_UUID_FIELD, index = true)
68 | private String premiumUuid = "";
69 |
70 | @DatabaseField(columnName = LOGIN_IP_FIELD)
71 | private String loginIp;
72 |
73 | @DatabaseField(columnName = LOGIN_DATE_FIELD)
74 | private Long loginDate = System.currentTimeMillis();
75 |
76 | @DatabaseField(columnName = TOKEN_ISSUED_AT_FIELD)
77 | private Long tokenIssuedAt = System.currentTimeMillis();
78 |
79 | @Deprecated
80 | public RegisteredPlayer(String nickname, String lowercaseNickname,
81 | String hash, String ip, String totpToken, Long regDate, String uuid, String premiumUuid, String loginIp, Long loginDate) {
82 | this.nickname = nickname;
83 | this.lowercaseNickname = lowercaseNickname;
84 | this.hash = hash;
85 | this.ip = ip;
86 | this.totpToken = totpToken;
87 | this.regDate = regDate;
88 | this.uuid = uuid;
89 | this.premiumUuid = premiumUuid;
90 | this.loginIp = loginIp;
91 | this.loginDate = loginDate;
92 | }
93 |
94 | public RegisteredPlayer(Player player) {
95 | this(player.getUsername(), player.getUniqueId(), player.getRemoteAddress());
96 | }
97 |
98 | public RegisteredPlayer(String nickname, UUID uuid, InetSocketAddress ip) {
99 | this(nickname, uuid.toString(), ip.getAddress().getHostAddress());
100 | }
101 |
102 | public RegisteredPlayer(String nickname, String uuid, String ip) {
103 | this.nickname = nickname;
104 | this.lowercaseNickname = nickname.toLowerCase(Locale.ROOT);
105 | this.uuid = uuid;
106 | this.ip = ip;
107 | this.loginIp = ip;
108 | }
109 |
110 | public RegisteredPlayer() {
111 |
112 | }
113 |
114 | public static String genHash(String password) {
115 | return HASHER.hashToString(Settings.IMP.MAIN.BCRYPT_COST, password.toCharArray());
116 | }
117 |
118 | public RegisteredPlayer setNickname(String nickname) {
119 | this.nickname = nickname;
120 | this.lowercaseNickname = nickname.toLowerCase(Locale.ROOT);
121 |
122 | return this;
123 | }
124 |
125 | public String getNickname() {
126 | return this.nickname == null ? this.lowercaseNickname : this.nickname;
127 | }
128 |
129 | public String getLowercaseNickname() {
130 | return this.lowercaseNickname;
131 | }
132 |
133 | public RegisteredPlayer setPassword(String password) {
134 | this.hash = genHash(password);
135 | this.tokenIssuedAt = System.currentTimeMillis();
136 |
137 | return this;
138 | }
139 |
140 | public RegisteredPlayer setHash(String hash) {
141 | this.hash = hash;
142 | this.tokenIssuedAt = System.currentTimeMillis();
143 |
144 | return this;
145 | }
146 |
147 | public String getHash() {
148 | return this.hash == null ? "" : this.hash;
149 | }
150 |
151 | public RegisteredPlayer setIP(String ip) {
152 | this.ip = ip;
153 |
154 | return this;
155 | }
156 |
157 | public String getIP() {
158 | return this.ip == null ? "" : this.ip;
159 | }
160 |
161 | public RegisteredPlayer setTotpToken(String totpToken) {
162 | this.totpToken = totpToken;
163 |
164 | return this;
165 | }
166 |
167 | public String getTotpToken() {
168 | return this.totpToken == null ? "" : this.totpToken;
169 | }
170 |
171 | public RegisteredPlayer setRegDate(Long regDate) {
172 | this.regDate = regDate;
173 |
174 | return this;
175 | }
176 |
177 | public long getRegDate() {
178 | return this.regDate == null ? Long.MIN_VALUE : this.regDate;
179 | }
180 |
181 | public RegisteredPlayer setUuid(String uuid) {
182 | this.uuid = uuid;
183 |
184 | return this;
185 | }
186 |
187 | public String getUuid() {
188 | return this.uuid == null ? "" : this.uuid;
189 | }
190 |
191 | public RegisteredPlayer setPremiumUuid(String premiumUuid) {
192 | this.premiumUuid = premiumUuid;
193 |
194 | return this;
195 | }
196 |
197 | public RegisteredPlayer setPremiumUuid(UUID premiumUuid) {
198 | this.premiumUuid = premiumUuid.toString();
199 |
200 | return this;
201 | }
202 |
203 | public String getPremiumUuid() {
204 | return this.premiumUuid == null ? "" : this.premiumUuid;
205 | }
206 |
207 | public String getLoginIp() {
208 | return this.loginIp == null ? "" : this.loginIp;
209 | }
210 |
211 | public RegisteredPlayer setLoginIp(String loginIp) {
212 | this.loginIp = loginIp;
213 |
214 | return this;
215 | }
216 |
217 | public long getLoginDate() {
218 | return this.loginDate == null ? Long.MIN_VALUE : this.loginDate;
219 | }
220 |
221 | public RegisteredPlayer setLoginDate(Long loginDate) {
222 | this.loginDate = loginDate;
223 |
224 | return this;
225 | }
226 |
227 | public long getTokenIssuedAt() {
228 | return this.tokenIssuedAt == null ? Long.MIN_VALUE : this.tokenIssuedAt;
229 | }
230 |
231 | public RegisteredPlayer setTokenIssuedAt(Long tokenIssuedAt) {
232 | this.tokenIssuedAt = tokenIssuedAt;
233 |
234 | return this;
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/main/java/net/elytrium/limboauth/model/SQLRuntimeException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth.model;
19 |
20 | public class SQLRuntimeException extends RuntimeException {
21 |
22 | public SQLRuntimeException(Throwable cause) {
23 | this("An unexpected internal error was caught during the database SQL operations.", cause);
24 | }
25 |
26 | public SQLRuntimeException(String message, Throwable cause) {
27 | super(message, cause);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/templates/net/elytrium/limboauth/BuildConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2021 - 2025 Elytrium
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | package net.elytrium.limboauth;
19 |
20 | // The constants are replaced before compilation.
21 | public class BuildConstants {
22 |
23 | public static final String AUTH_VERSION = "${version}";
24 | }
25 |
--------------------------------------------------------------------------------