Merge branch 'master' into ip-check

This commit is contained in:
Gabriele C 2018-07-16 13:35:03 +02:00 committed by GitHub
commit 1a1ba31bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
176 changed files with 4091 additions and 1324 deletions

View File

@ -1,49 +0,0 @@
version: 2
jobs:
build_and_test_jdk8:
working_directory: ~/authmereloaded-jdk8
docker:
- image: circleci/openjdk:8-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
keys:
- authmereloaded-{{ checksum "pom.xml" }}
- authmereloaded-
- run: mvn -T 2 dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/*.jar
build_and_test_jdk9:
working_directory: ~/authmereloaded-jdk9
docker:
- image: circleci/openjdk:9-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 package
- store_test_results:
path: target/surefire-reports
- run: cp ./target/*.jar $CIRCLE_ARTIFACTS
workflows:
version: 2
build_and_test:
jobs:
- build_and_test_jdk8
- build_and_test_jdk9

52
.circleci/config.yml Normal file
View File

@ -0,0 +1,52 @@
version: 2
jobs:
build_and_test_jdk8:
working_directory: ~/authmereloaded-jdk8
docker:
- image: circleci/openjdk:8-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
keys:
- authmereloaded-{{ checksum "pom.xml" }}
- authmereloaded-
- run: mvn -T 2 -B dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 -B package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/*.jar
build_and_test_jdk10:
working_directory: ~/authmereloaded-jdk10
docker:
- image: circleci/openjdk:10-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
keys:
- authmereloaded-{{ checksum "pom.xml" }}
- authmereloaded-
- run: mvn -T 2 -B dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 -B package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/*.jar
workflows:
version: 2
build_and_test:
jobs:
- build_and_test_jdk8
- build_and_test_jdk10

View File

@ -8,7 +8,6 @@
| **Code quality:** | [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded) [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master) |
| **Jenkins CI:** | [![Jenkins Status](https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=ci.codemc.org)](https://ci.codemc.org/) [![Build Status](https://ci.codemc.org/buildStatus/icon?job=AuthMe/AuthMeReloaded)](https://ci.codemc.org/job/AuthMe/job/AuthMeReloaded) ![Build Tests](https://img.shields.io/jenkins/t/https/ci.codemc.org/job/AuthMe/job/AuthMeReloaded.svg) |
| **Other CIs:** | [![CircleCI](https://circleci.com/gh/AuthMe/AuthMeReloaded.svg?style=svg)](https://circleci.com/gh/AuthMe/AuthMeReloaded) |
| **Dependencies:** | [![Dependency Status](https://gemnasium.com/badges/github.com/AuthMe/AuthMeReloaded.svg)](https://gemnasium.com/github.com/AuthMe/AuthMeReloaded) |
## Description

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Feb 02 20:09:14 CET 2018. See docs/commands/commands.tpl.md -->
<!-- File auto-generated on Sun Apr 22 11:00:10 CEST 2018. See docs/commands/commands.tpl.md -->
## AuthMe Commands
You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >`
@ -85,6 +85,15 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
- **/changepassword** &lt;oldPassword> &lt;newPassword>: Command to change your password using AuthMeReloaded.
<br />Requires `authme.player.changepassword`
- **/changepassword help** [query]: View detailed help for /changepassword commands.
- **/totp**: Performs actions related to two-factor authentication.
- **/totp code** &lt;code>: Processes the two-factor authentication code during login.
- **/totp add**: Enables two-factor authentication for your account.
<br />Requires `authme.player.totpadd`
- **/totp confirm** &lt;code>: Saves the generated TOTP secret after confirmation.
<br />Requires `authme.player.totpadd`
- **/totp remove** &lt;code>: Disables two-factor authentication for your account.
<br />Requires `authme.player.totpremove`
- **/totp help** [query]: View detailed help for /totp commands.
- **/captcha** &lt;captcha>: Captcha command for AuthMeReloaded.
<br />Requires `authme.player.captcha`
- **/captcha help** [query]: View detailed help for /captcha commands.
@ -95,4 +104,4 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Feb 02 20:09:14 CET 2018
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:10 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Sun Jan 21 18:49:44 CET 2018. See docs/config/config.tpl.md -->
<!-- File auto-generated on Mon May 21 09:08:25 CEST 2018. See docs/config/config.tpl.md -->
## AuthMe Configuration
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@ -37,12 +37,16 @@ DataSource:
mySQLRealName: 'realname'
# Column for storing players passwords
mySQLColumnPassword: 'password'
# Column for storing players passwords salts
mySQLColumnSalt: ''
# Column for storing players emails
mySQLColumnEmail: 'email'
# Column for storing if a player is logged in or not
mySQLColumnLogged: 'isLogged'
# Column for storing if a player has a valid session or not
mySQLColumnHasSession: 'hasSession'
# Column for storing a player's TOTP key (for two-factor authentication)
mySQLtotpKey: 'totp'
# Column for storing the player's last IP
mySQLColumnIp: 'ip'
# Column for storing players lastlogins
@ -69,8 +73,6 @@ DataSource:
# You should set this at least 30 seconds less than mysql server wait_timeout
maxLifetime: 1800
ExternalBoardOptions:
# Column for storing players passwords salts
mySQLColumnSalt: ''
# Column for storing players groups
mySQLColumnGroup: ''
# -1 means disabled. If you want that only activated players
@ -138,6 +140,8 @@ settings:
- '/reg'
- '/email'
- '/captcha'
- '/2fa'
- '/totp'
# Max number of allowed registrations per IP
# The value 0 means an unlimited number of registrations!
maxRegPerIp: 1
@ -384,7 +388,7 @@ Protection:
# Apply the protection also to registered usernames
enableProtectionRegistered: true
# Countries allowed to join the server and register. For country codes, see
# http://dev.maxmind.com/geoip/legacy/codes/iso3166/
# https://dev.maxmind.com/geoip/legacy/codes/iso3166/
# PLEASE USE QUOTES!
countries:
- 'US'
@ -404,6 +408,9 @@ Protection:
antiBotDuration: 10
# Delay in seconds before the antibot activation
antiBotDelay: 60
quickCommands:
# Kicks the player that issued a command before the defined time after the join process
denyCommandsBeforeMilliseconds: 1000
Purge:
# If enabled, AuthMe automatically purges old, unused accounts
useAutoPurge: false
@ -555,4 +562,4 @@ To change settings on a running server, save your changes to config.yml and use
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Jan 21 18:49:44 CET 2018
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 09:08:25 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Dec 01 19:16:17 CET 2017. See docs/permissions/permission_nodes.tpl.md -->
<!-- File auto-generated on Mon May 21 08:43:08 CEST 2018. See docs/permissions/permission_nodes.tpl.md -->
## AuthMe Permission Nodes
The following are the permission nodes that are currently supported by the latest dev builds.
@ -30,6 +30,7 @@ The following are the permission nodes that are currently supported by the lates
- **authme.admin.switchantibot** Administrator command to toggle the AntiBot protection status.
- **authme.admin.unregister** Administrator command to unregister an existing user.
- **authme.admin.updatemessages** Permission to use the update messages command.
- **authme.allowchatbeforelogin** Permission to send chat messages before being logged in.
- **authme.allowmultipleaccounts** Permission to be able to register multiple accounts.
- **authme.bypassantibot** Permission node to bypass AntiBot protection.
- **authme.bypasscountrycheck** Permission to bypass the GeoIp country code check.
@ -57,13 +58,16 @@ The following are the permission nodes that are currently supported by the lates
- **authme.player.email.see** Command permission to see the own email address.
- **authme.player.login** Command permission to login.
- **authme.player.logout** Command permission to logout.
- **authme.player.protection.quickcommandsprotection** Permission that enables on join quick commands checks for the player.
- **authme.player.register** Command permission to register.
- **authme.player.security.verificationcode** Permission to use the email verification codes feature.
- **authme.player.seeownaccounts** Permission to use to see own other accounts.
- **authme.player.totpadd** Permission to enable two-factor authentication.
- **authme.player.totpremove** Permission to disable two-factor authentication.
- **authme.player.unregister** Command permission to unregister.
- **authme.vip** When the server is full and someone with this permission joins the server, someone will be kicked.
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Dec 01 19:16:17 CET 2017
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 08:43:08 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Feb 02 20:09:17 CET 2018. See docs/translations/translations.tpl.md -->
<!-- File auto-generated on Mon Jun 25 21:53:35 CEST 2018. See docs/translations/translations.tpl.md -->
# AuthMe Translations
The following translations are available in AuthMe. Set `messagesLanguage` to the language code
@ -8,37 +8,37 @@ in your config.yml to use the language, or use another language code to start a
Code | Language | Translated | &nbsp;
---- | -------- | ---------: | ------
[en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 86% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=86&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 48% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=48&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 51% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=51&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 54% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=54&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 53% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=53&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 76% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=76&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 92% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=92&h=5&txtpad=1" alt="bar" />
[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 42% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=42&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 45% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=45&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 48% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=48&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 47% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=47&h=5&txtpad=1" alt="bar" />
[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 40% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa4400&w=40&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 36% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa4400&w=36&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 86% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=86&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 71% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=71&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 73% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=73&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 92% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=92&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 76% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=76&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 63% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=63&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 77% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=77&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 65% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=65&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Feb 02 20:09:17 CET 2018
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Jun 25 21:53:35 CEST 2018

64
pom.xml
View File

@ -73,9 +73,13 @@
<bukkitplugin.authors>Xephi, sgdc3, DNx5, timvisee, games647, ljacqu, Gnat008</bukkitplugin.authors>
<!-- Change Bukkit Version HERE! -->
<bukkit.version>1.12.2-R0.1-SNAPSHOT</bukkit.version>
<bukkit.version>1.13-pre7-R0.1-SNAPSHOT</bukkit.version>
</properties>
<prerequisites>
<maven>3.3.9</maven>
</prerequisites>
<!-- Jenkins profile -->
<profiles>
<!-- Set the buildNumber using the jenkins env. variable -->
@ -131,12 +135,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -150,7 +154,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.0</version>
<version>0.8.1</version>
<executions>
<execution>
<id>pre-unit-test</id>
@ -169,10 +173,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
<version>2.21.0</version>
<configuration>
<!-- Force the right file encoding during unit testing -->
<argLine>-Dfile.encoding=${project.build.sourceEncoding} @{argLine}</argLine>
<!-- Set language to English in order to get consistent results for localized time formatting -->
<argLine>-Dfile.encoding=${project.build.sourceEncoding} -Duser.language=en @{argLine}</argLine>
<systemPropertyVariables>
<project.skipExtendedHashTests>${project.skipExtendedHashTests}</project.skipExtendedHashTests>
</systemPropertyVariables>
@ -181,12 +186,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-javadoc</id>
@ -226,7 +231,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
@ -251,12 +256,8 @@
<shadedPattern>fr.xephi.authme.libs.com.google</shadedPattern>
</relocation>
<relocation>
<pattern>ch.jalu.injector</pattern>
<shadedPattern>fr.xephi.authme.libs.jalu.injector</shadedPattern>
</relocation>
<relocation>
<pattern>ch.jalu.configme</pattern>
<shadedPattern>fr.xephi.authme.libs.ch.jalu.configme</shadedPattern>
<pattern>ch.jalu</pattern>
<shadedPattern>fr.xephi.authme.libs.ch.jalu</shadedPattern>
</relocation>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
@ -286,6 +287,10 @@
<pattern>de.mkammerer</pattern>
<shadedPattern>fr.xephi.authme.libs.de.mkammerer</shadedPattern>
</relocation>
<relocation>
<pattern>com.warrenstrange</pattern>
<shadedPattern>fr.xephi.authme.libs.com.warrenstrange</shadedPattern>
</relocation>
<relocation>
<pattern>javax.inject</pattern>
<shadedPattern>fr.xephi.authme.libs.javax.inject</shadedPattern>
@ -387,7 +392,7 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
<version>2.8.5</version>
<optional>true</optional>
</dependency>
@ -395,7 +400,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>24.1-jre</version>
<version>25.1-jre</version>
<optional>true</optional>
</dependency>
@ -436,7 +441,7 @@
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.8</version>
<version>3.2.0</version>
<optional>true</optional>
<exclusions>
<exclusion>
@ -469,6 +474,14 @@
<optional>true</optional>
</dependency>
<!-- TOTP client -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.1.5</version>
<optional>true</optional>
</dependency>
<!-- Spigot API, http://www.spigotmc.org/ -->
<dependency>
<groupId>org.spigotmc</groupId>
@ -545,7 +558,7 @@
<dependency>
<groupId>me.lucko.luckperms</groupId>
<artifactId>luckperms-api</artifactId>
<version>4.1</version>
<version>4.2</version>
<scope>provided</scope>
</dependency>
@ -788,6 +801,13 @@
</exclusions>
</dependency>
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>datasourcecolumns</artifactId>
<version>0.1.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<!-- Unit Testing Libraries -->
<dependency>
@ -808,7 +828,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
<version>2.16.0</version>
<version>2.19.0</version>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
@ -821,13 +841,13 @@
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.21.0.1</version>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
<version>1.4.197</version>
<scope>test</scope>
</dependency>

View File

@ -2,9 +2,7 @@ package fr.xephi.authme;
import ch.jalu.injector.Injector;
import ch.jalu.injector.InjectorBuilder;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.api.NewAPI;
import fr.xephi.authme.command.CommandHandler;
import fr.xephi.authme.datasource.DataSource;
@ -35,9 +33,6 @@ import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.task.CleanupTask;
import fr.xephi.authme.task.purge.PurgeService;
import fr.xephi.authme.util.ExceptionUtils;
import java.io.File;
import org.apache.commons.lang.SystemUtils;
import org.bukkit.Server;
import org.bukkit.command.Command;
@ -48,6 +43,8 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.java.JavaPluginLoader;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.File;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE;
import static fr.xephi.authme.util.Utils.isClassLoaded;
@ -160,7 +157,7 @@ public class AuthMe extends JavaPlugin {
// Sponsor messages
ConsoleLogger.info("Development builds are available on our jenkins, thanks to FastVM.io");
ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader "
ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader "
+ "as virtual server provider!");
// Successful message

View File

@ -5,7 +5,7 @@ import fr.xephi.authme.output.LogLevel;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import java.io.File;
import java.io.FileWriter;
@ -101,7 +101,7 @@ public final class ConsoleLogger {
* @param th The Throwable to log
*/
public static void logException(String message, Throwable th) {
warning(message + " " + StringUtils.formatException(th));
warning(message + " " + ExceptionUtils.formatException(th));
writeLog(Throwables.getStackTraceAsString(th));
}

View File

@ -33,13 +33,18 @@ import fr.xephi.authme.command.executable.changepassword.ChangePasswordCommand;
import fr.xephi.authme.command.executable.email.AddEmailCommand;
import fr.xephi.authme.command.executable.email.ChangeEmailCommand;
import fr.xephi.authme.command.executable.email.EmailBaseCommand;
import fr.xephi.authme.command.executable.email.EmailSetPasswordCommand;
import fr.xephi.authme.command.executable.email.ProcessCodeCommand;
import fr.xephi.authme.command.executable.email.RecoverEmailCommand;
import fr.xephi.authme.command.executable.email.SetPasswordCommand;
import fr.xephi.authme.command.executable.email.ShowEmailCommand;
import fr.xephi.authme.command.executable.login.LoginCommand;
import fr.xephi.authme.command.executable.logout.LogoutCommand;
import fr.xephi.authme.command.executable.register.RegisterCommand;
import fr.xephi.authme.command.executable.totp.AddTotpCommand;
import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand;
import fr.xephi.authme.command.executable.totp.RemoveTotpCommand;
import fr.xephi.authme.command.executable.totp.TotpBaseCommand;
import fr.xephi.authme.command.executable.totp.TotpCodeCommand;
import fr.xephi.authme.command.executable.unregister.UnregisterCommand;
import fr.xephi.authme.command.executable.verification.VerificationCommand;
import fr.xephi.authme.permission.AdminPermission;
@ -55,6 +60,9 @@ import java.util.List;
*/
public class CommandInitializer {
private static final boolean OPTIONAL = true;
private static final boolean MANDATORY = false;
private List<CommandDescription> commands;
public CommandInitializer() {
@ -84,7 +92,7 @@ public class CommandInitializer {
.labels("login", "l", "log")
.description("Login command")
.detailedDescription("Command to log in using AuthMeReloaded.")
.withArgument("password", "Login password", false)
.withArgument("password", "Login password", MANDATORY)
.permission(PlayerPermission.LOGIN)
.executableCommand(LoginCommand.class)
.register();
@ -105,8 +113,8 @@ public class CommandInitializer {
.labels("register", "reg")
.description("Register an account")
.detailedDescription("Command to register using AuthMeReloaded.")
.withArgument("password", "Password", true)
.withArgument("verifyPassword", "Verify password", true)
.withArgument("password", "Password", OPTIONAL)
.withArgument("verifyPassword", "Verify password", OPTIONAL)
.permission(PlayerPermission.REGISTER)
.executableCommand(RegisterCommand.class)
.register();
@ -117,7 +125,7 @@ public class CommandInitializer {
.labels("unregister", "unreg")
.description("Unregister an account")
.detailedDescription("Command to unregister using AuthMeReloaded.")
.withArgument("password", "Password", false)
.withArgument("password", "Password", MANDATORY)
.permission(PlayerPermission.UNREGISTER)
.executableCommand(UnregisterCommand.class)
.register();
@ -128,19 +136,22 @@ public class CommandInitializer {
.labels("changepassword", "changepass", "cp")
.description("Change password of an account")
.detailedDescription("Command to change your password using AuthMeReloaded.")
.withArgument("oldPassword", "Old password", false)
.withArgument("newPassword", "New password", false)
.withArgument("oldPassword", "Old password", MANDATORY)
.withArgument("newPassword", "New password", MANDATORY)
.permission(PlayerPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordCommand.class)
.register();
// Create totp base command
CommandDescription totpBase = buildTotpBaseCommand();
// Register the base captcha command
CommandDescription captchaBase = CommandDescription.builder()
.parent(null)
.labels("captcha")
.description("Captcha command")
.detailedDescription("Captcha command for AuthMeReloaded.")
.withArgument("captcha", "The Captcha", false)
.withArgument("captcha", "The Captcha", MANDATORY)
.permission(PlayerPermission.CAPTCHA)
.executableCommand(CaptchaCommand.class)
.register();
@ -151,21 +162,13 @@ public class CommandInitializer {
.labels("verification")
.description("Verification command")
.detailedDescription("Command to complete the verification process for AuthMeReloaded.")
.withArgument("code", "The code", false)
.withArgument("code", "The code", MANDATORY)
.permission(PlayerPermission.VERIFICATION_CODE)
.executableCommand(VerificationCommand.class)
.register();
List<CommandDescription> baseCommands = ImmutableList.of(
authMeBase,
emailBase,
loginBase,
logoutBase,
registerBase,
unregisterBase,
changePasswordBase,
captchaBase,
verificationBase);
List<CommandDescription> baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase,
registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase);
setHelpOnAllBases(baseCommands);
commands = baseCommands;
@ -191,8 +194,8 @@ public class CommandInitializer {
.labels("register", "reg", "r")
.description("Register a player")
.detailedDescription("Register the specified player with the specified password.")
.withArgument("player", "Player name", false)
.withArgument("password", "Password", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("password", "Password", MANDATORY)
.permission(AdminPermission.REGISTER)
.executableCommand(RegisterAdminCommand.class)
.register();
@ -203,7 +206,7 @@ public class CommandInitializer {
.labels("unregister", "unreg", "unr")
.description("Unregister a player")
.detailedDescription("Unregister the specified player.")
.withArgument("player", "Player name", false)
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.UNREGISTER)
.executableCommand(UnregisterAdminCommand.class)
.register();
@ -214,7 +217,7 @@ public class CommandInitializer {
.labels("forcelogin", "login")
.description("Enforce login player")
.detailedDescription("Enforce the specified player to login.")
.withArgument("player", "Online player name", true)
.withArgument("player", "Online player name", OPTIONAL)
.permission(AdminPermission.FORCE_LOGIN)
.executableCommand(ForceLoginCommand.class)
.register();
@ -225,8 +228,8 @@ public class CommandInitializer {
.labels("password", "changepassword", "changepass", "cp")
.description("Change a player's password")
.detailedDescription("Change the password of a player.")
.withArgument("player", "Player name", false)
.withArgument("pwd", "New password", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("pwd", "New password", MANDATORY)
.permission(AdminPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordAdminCommand.class)
.register();
@ -237,7 +240,7 @@ public class CommandInitializer {
.labels("lastlogin", "ll")
.description("Player's last login")
.detailedDescription("View the date of the specified players last login.")
.withArgument("player", "Player name", true)
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.LAST_LOGIN)
.executableCommand(LastLoginCommand.class)
.register();
@ -248,7 +251,7 @@ public class CommandInitializer {
.labels("accounts", "account")
.description("Display player accounts")
.detailedDescription("Display all accounts of a player by his player name or IP.")
.withArgument("player", "Player name or IP", true)
.withArgument("player", "Player name or IP", OPTIONAL)
.permission(AdminPermission.ACCOUNTS)
.executableCommand(AccountsCommand.class)
.register();
@ -259,7 +262,7 @@ public class CommandInitializer {
.labels("email", "mail", "getemail", "getmail")
.description("Display player's email")
.detailedDescription("Display the email address of the specified player if set.")
.withArgument("player", "Player name", true)
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.GET_EMAIL)
.executableCommand(GetEmailCommand.class)
.register();
@ -270,8 +273,8 @@ public class CommandInitializer {
.labels("setemail", "setmail", "chgemail", "chgmail")
.description("Change player's email")
.detailedDescription("Change the email address of the specified player.")
.withArgument("player", "Player name", false)
.withArgument("email", "Player email", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("email", "Player email", MANDATORY)
.permission(AdminPermission.CHANGE_EMAIL)
.executableCommand(SetEmailCommand.class)
.register();
@ -282,7 +285,7 @@ public class CommandInitializer {
.labels("getip", "ip")
.description("Get player's IP")
.detailedDescription("Get the IP address of the specified online player.")
.withArgument("player", "Player name", false)
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.GET_IP)
.executableCommand(GetIpCommand.class)
.register();
@ -333,7 +336,7 @@ public class CommandInitializer {
.labels("purge", "delete")
.description("Purge old data")
.detailedDescription("Purge old AuthMeReloaded data longer than the specified number of days ago.")
.withArgument("days", "Number of days", false)
.withArgument("days", "Number of days", MANDATORY)
.permission(AdminPermission.PURGE)
.executableCommand(PurgeCommand.class)
.register();
@ -344,8 +347,8 @@ public class CommandInitializer {
.labels("purgeplayer")
.description("Purges the data of one player")
.detailedDescription("Purges data of the given player.")
.withArgument("player", "The player to purge", false)
.withArgument("options", "'force' to run without checking if player is registered", true)
.withArgument("player", "The player to purge", MANDATORY)
.withArgument("options", "'force' to run without checking if player is registered", OPTIONAL)
.permission(AdminPermission.PURGE_PLAYER)
.executableCommand(PurgePlayerCommand.class)
.register();
@ -367,7 +370,7 @@ public class CommandInitializer {
"resetlastposition", "resetlastpos")
.description("Purge player's last position")
.detailedDescription("Purge the last know position of the specified player or all of them.")
.withArgument("player/*", "Player name or * for all players", false)
.withArgument("player/*", "Player name or * for all players", MANDATORY)
.permission(AdminPermission.PURGE_LAST_POSITION)
.executableCommand(PurgeLastPositionCommand.class)
.register();
@ -388,7 +391,7 @@ public class CommandInitializer {
.labels("switchantibot", "toggleantibot", "antibot")
.description("Switch AntiBot mode")
.detailedDescription("Switch or toggle the AntiBot mode to the specified state.")
.withArgument("mode", "ON / OFF", true)
.withArgument("mode", "ON / OFF", OPTIONAL)
.permission(AdminPermission.SWITCH_ANTIBOT)
.executableCommand(SwitchAntiBotCommand.class)
.register();
@ -419,7 +422,7 @@ public class CommandInitializer {
.description("Converter command")
.detailedDescription("Converter command for AuthMeReloaded.")
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / "
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", true)
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", OPTIONAL)
.permission(AdminPermission.CONVERTER)
.executableCommand(ConverterCommand.class)
.register();
@ -447,9 +450,9 @@ public class CommandInitializer {
.labels("debug", "dbg")
.description("Debug features")
.detailedDescription("Allows various operations for debugging.")
.withArgument("child", "The child to execute", true)
.withArgument("arg", "argument (depends on debug section)", true)
.withArgument("arg", "argument (depends on debug section)", true)
.withArgument("child", "The child to execute", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.permission(DebugSectionPermissions.DEBUG_COMMAND)
.executableCommand(DebugCommand.class)
.register();
@ -488,8 +491,8 @@ public class CommandInitializer {
.labels("add", "addemail", "addmail")
.description("Add Email")
.detailedDescription("Add a new email address to your account.")
.withArgument("email", "Email address", false)
.withArgument("verifyEmail", "Email address verification", false)
.withArgument("email", "Email address", MANDATORY)
.withArgument("verifyEmail", "Email address verification", MANDATORY)
.permission(PlayerPermission.ADD_EMAIL)
.executableCommand(AddEmailCommand.class)
.register();
@ -500,8 +503,8 @@ public class CommandInitializer {
.labels("change", "changeemail", "changemail")
.description("Change Email")
.detailedDescription("Change an email address of your account.")
.withArgument("oldEmail", "Old email address", false)
.withArgument("newEmail", "New email address", false)
.withArgument("oldEmail", "Old email address", MANDATORY)
.withArgument("newEmail", "New email address", MANDATORY)
.permission(PlayerPermission.CHANGE_EMAIL)
.executableCommand(ChangeEmailCommand.class)
.register();
@ -513,7 +516,7 @@ public class CommandInitializer {
.description("Recover password using email")
.detailedDescription("Recover your account using an Email address by sending a mail containing "
+ "a new password.")
.withArgument("email", "Email address", false)
.withArgument("email", "Email address", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(RecoverEmailCommand.class)
.register();
@ -524,7 +527,7 @@ public class CommandInitializer {
.labels("code")
.description("Submit code to recover password")
.detailedDescription("Recover your account by submitting a code delivered to your email.")
.withArgument("code", "Recovery code", false)
.withArgument("code", "Recovery code", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(ProcessCodeCommand.class)
.register();
@ -535,14 +538,74 @@ public class CommandInitializer {
.labels("setpassword")
.description("Set new password after recovery")
.detailedDescription("Set a new password after successfully recovering your account.")
.withArgument("password", "New password", false)
.withArgument("password", "New password", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(SetPasswordCommand.class)
.executableCommand(EmailSetPasswordCommand.class)
.register();
return emailBase;
}
/**
* Creates a command description object for {@code /totp} including its children.
*
* @return the totp base command description
*/
private CommandDescription buildTotpBaseCommand() {
// Register the base totp command
CommandDescription totpBase = CommandDescription.builder()
.parent(null)
.labels("totp", "2fa")
.description("TOTP commands")
.detailedDescription("Performs actions related to two-factor authentication.")
.executableCommand(TotpBaseCommand.class)
.register();
// Register the base totp code
CommandDescription.builder()
.parent(totpBase)
.labels("code", "c")
.description("Command for logging in")
.detailedDescription("Processes the two-factor authentication code during login.")
.withArgument("code", "The TOTP code to use to log in", MANDATORY)
.executableCommand(TotpCodeCommand.class)
.register();
// Register totp add
CommandDescription.builder()
.parent(totpBase)
.labels("add")
.description("Enables TOTP")
.detailedDescription("Enables two-factor authentication for your account.")
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(AddTotpCommand.class)
.register();
// Register totp confirm
CommandDescription.builder()
.parent(totpBase)
.labels("confirm")
.description("Enables TOTP after successful code")
.detailedDescription("Saves the generated TOTP secret after confirmation.")
.withArgument("code", "Code from the given secret from /totp add", MANDATORY)
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(ConfirmTotpCommand.class)
.register();
// Register totp remove
CommandDescription.builder()
.parent(totpBase)
.labels("remove")
.description("Removes TOTP")
.detailedDescription("Disables two-factor authentication for your account.")
.withArgument("code", "Current 2FA code", MANDATORY)
.permission(PlayerPermission.DISABLE_TWO_FACTOR_AUTH)
.executableCommand(RemoveTotpCommand.class)
.register();
return totpBase;
}
/**
* Sets the help command on all base commands, e.g. to register /authme help or /register help.
*
@ -557,7 +620,7 @@ public class CommandInitializer {
.labels(helpCommandLabels)
.description("View help")
.detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.")
.withArgument("query", "The command or query to view help for.", true)
.withArgument("query", "The command or query to view help for.", OPTIONAL)
.executableCommand(HelpCommand.class)
.register();
}

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
@ -25,8 +25,8 @@ public class GetEmailCommand implements ExecutableCommand {
public void executeCommand(CommandSender sender, List<String> arguments) {
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
DataSourceResult<String> email = dataSource.getEmail(playerName);
if (email.playerExists()) {
DataSourceValue<String> email = dataSource.getEmail(playerName);
if (email.rowExists()) {
sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue());
} else {
commonService.send(sender, MessageKey.UNKNOWN_USER);

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.command.executable.authme;
import com.google.common.primitives.Ints;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.task.purge.PurgeService;
import org.bukkit.ChatColor;
@ -26,10 +27,8 @@ public class PurgeCommand implements ExecutableCommand {
String daysStr = arguments.get(0);
// Convert the days string to an integer value, and make sure it's valid
int days;
try {
days = Integer.parseInt(daysStr);
} catch (NumberFormatException ex) {
Integer days = Ints.tryParse(daysStr);
if (days == null) {
sender.sendMessage(ChatColor.RED + "The value you've entered is invalid!");
return;
}

View File

@ -7,6 +7,7 @@ import fr.xephi.authme.service.HelpTranslationGenerator;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
@ -24,8 +25,8 @@ public class UpdateHelpMessagesCommand implements ExecutableCommand {
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
try {
helpTranslationGenerator.updateHelpFile();
sender.sendMessage("Successfully updated the help file");
File updatedFile = helpTranslationGenerator.updateHelpFile();
sender.sendMessage("Successfully updated the help file '" + updatedFile.getName() + "'");
helpMessagesService.reloadMessagesFile();
} catch (IOException e) {
sender.sendMessage("Could not update help file: " + e.getMessage());

View File

@ -77,6 +77,7 @@ class PlayerAuthViewer implements DebugSection {
HashedPassword hashedPass = auth.getPassword();
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
sender.sendMessage("TOTP code (partial): '" + safeSubstring(auth.getTotpKey(), 3) + "'");
}
/**

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.mail.SendMailSsl;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
@ -82,8 +82,8 @@ class TestEmailSender implements DebugSection {
*/
private String getEmail(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
DataSourceResult<String> emailResult = dataSource.getEmail(sender.getName());
if (!emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(sender.getName());
if (!emailResult.rowExists()) {
sender.sendMessage(ChatColor.RED + "Please provide an email address, "
+ "e.g. /authme debug mail test@example.com");
return null;

View File

@ -4,6 +4,7 @@ import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
@ -80,6 +81,6 @@ public class CaptchaCommand extends PlayerCommand {
String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
}
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
}
}

View File

@ -18,7 +18,7 @@ import java.util.List;
/**
* Command for changing password following successful recovery.
*/
public class SetPasswordCommand extends PlayerCommand {
public class EmailSetPasswordCommand extends PlayerCommand {
@Inject
private DataSource dataSource;
@ -45,11 +45,14 @@ public class SetPasswordCommand extends PlayerCommand {
if (!result.hasError()) {
HashedPassword hashedPassword = passwordSecurity.computeHash(password, name);
dataSource.updatePassword(name, hashedPassword);
recoveryService.removeFromSuccessfulRecovery(player);
ConsoleLogger.info("Player '" + name + "' has changed their password from recovery");
commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS);
} else {
commonService.send(player, result.getMessageKey(), result.getArgs());
}
} else {
commonService.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED);
}
}
}

View File

@ -1,10 +1,10 @@
package fr.xephi.authme.command.executable.email;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.BukkitService;
@ -58,8 +58,8 @@ public class RecoverEmailCommand extends PlayerCommand {
return;
}
DataSourceResult<String> emailResult = dataSource.getEmail(playerName);
if (!emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(playerName);
if (!emailResult.rowExists()) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return;
}

View File

@ -18,7 +18,7 @@ public class LoginCommand extends PlayerCommand {
@Override
public void runCommand(Player player, List<String> arguments) {
final String password = arguments.get(0);
String password = arguments.get(0);
management.performLogin(player, password);
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to enable TOTP.
*/
public class AddTotpCommand extends PlayerCommand {
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player);
messages.send(player, MessageKey.TWO_FACTOR_CREATE,
createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl());
messages.send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
}
}
}

View File

@ -0,0 +1,71 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command to enable TOTP by supplying the proper code as confirmation.
*/
public class ConfirmTotpCommand extends PlayerCommand {
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private DataSource dataSource;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() != null) {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
} else {
verifyTotpCodeConfirmation(player, auth, arguments.get(0));
}
}
private void verifyTotpCodeConfirmation(Player player, PlayerAuth auth, String inputTotpCode) {
final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player);
if (totpDetails == null) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE);
} else {
boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode);
if (isCodeValid) {
generateTotpService.removeGenerateTotpKey(player);
insertTotpKeyIntoDatabase(player, auth, totpDetails);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE);
}
}
}
private void insertTotpKeyIntoDatabase(Player player, PlayerAuth auth, TotpGenerationResult totpDetails) {
if (dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey())) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS);
auth.setTotpKey(totpDetails.getTotpKey());
playerCache.updatePlayer(auth);
ConsoleLogger.info("Player '" + player.getName() + "' has successfully added a TOTP key to their account");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,59 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to remove 2FA authentication.
*/
public class RemoveTotpCommand extends PlayerCommand {
@Inject
private DataSource dataSource;
@Inject
private PlayerCache playerCache;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR);
} else {
if (totpAuthenticator.checkCode(auth, arguments.get(0))) {
removeTotpKeyFromDatabase(player, auth);
} else {
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}
private void removeTotpKeyFromDatabase(Player player, PlayerAuth auth) {
if (dataSource.removeTotpKey(auth.getNickname())) {
auth.setTotpKey(null);
playerCache.updatePlayer(auth);
messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS);
ConsoleLogger.info("Player '" + player.getName() + "' removed their TOTP key");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,29 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.help.HelpProvider;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
/**
* Base command for /totp.
*/
public class TotpBaseCommand implements ExecutableCommand {
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp"));
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN);
}
}

View File

@ -0,0 +1,76 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.process.login.AsynchronousLogin;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* TOTP code command for processing the 2FA code during the login process.
*/
public class TotpCodeCommand extends PlayerCommand {
@Inject
private LimboService limboService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private DataSource dataSource;
@Inject
private AsynchronousLogin asynchronousLogin;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (playerCache.isAuthenticated(player.getName())) {
messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
PlayerAuth auth = dataSource.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.REGISTER_MESSAGE);
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(player.getName());
if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) {
processCode(player, auth, arguments.get(0));
} else {
ConsoleLogger.debug(() -> "Aborting TOTP check for player '" + player.getName()
+ "'. Invalid limbo state: " + (limbo == null ? "no limbo" : limbo.getState()));
messages.send(player, MessageKey.LOGIN_MESSAGE);
}
}
private void processCode(Player player, PlayerAuth auth, String inputCode) {
boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode);
if (isCodeValid) {
ConsoleLogger.debug("Successfully checked TOTP code for `{0}`", player.getName());
asynchronousLogin.performLogin(player, auth);
} else {
ConsoleLogger.debug("Input TOTP code was invalid for player `{0}`", player.getName());
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}

View File

@ -1,7 +1,7 @@
package fr.xephi.authme.data;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.mail.EmailService;
@ -103,8 +103,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
*/
public boolean hasEmail(String name) {
boolean result = false;
DataSourceResult<String> emailResult = dataSource.getEmail(name);
if (emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(name);
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
result = true;
@ -130,8 +130,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
* @param name the name of the player to generate a code for
*/
private void generateCode(String name) {
DataSourceResult<String> emailResult = dataSource.getEmail(name);
if (emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(name);
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
String code = RandomStringUtils.generateNum(6); // 6 digits code
@ -162,7 +162,7 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
*
* @param name the name of the player to generate a code for
*/
public void verify(String name){
public void verify(String name) {
verifiedPlayers.add(name.toLowerCase());
}

View File

@ -26,6 +26,7 @@ public class PlayerAuth {
/** The player's name in the correct casing, e.g. "Xephi". */
private String realName;
private HashedPassword password;
private String totpKey;
private String email;
private String lastIp;
private int groupId;
@ -160,6 +161,14 @@ public class PlayerAuth {
this.registrationDate = registrationDate;
}
public String getTotpKey() {
return totpKey;
}
public void setTotpKey(String totpKey) {
this.totpKey = totpKey;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PlayerAuth)) {
@ -195,6 +204,7 @@ public class PlayerAuth {
private String name;
private String realName;
private HashedPassword password;
private String totpKey;
private String lastIp;
private String email;
private int groupId = -1;
@ -219,6 +229,7 @@ public class PlayerAuth {
auth.nickname = checkNotNull(name).toLowerCase();
auth.realName = firstNonNull(realName, "Player");
auth.password = firstNonNull(password, new HashedPassword(""));
auth.totpKey = totpKey;
auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email;
auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value
auth.groupId = groupId;
@ -258,6 +269,11 @@ public class PlayerAuth {
return password(new HashedPassword(hash, salt));
}
public Builder totpKey(String totpKey) {
this.totpKey = totpKey;
return this;
}
public Builder lastIp(String lastIp) {
this.lastIp = lastIp;
return this;

View File

@ -32,7 +32,7 @@ public enum AllowFlightRestoreType {
}
},
/** Always set flight enabled to false. */
/** The user's flight handling is not modified. */
NOTHING {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {

View File

@ -0,0 +1,11 @@
package fr.xephi.authme.data.limbo;
public enum LimboMessageType {
REGISTER,
LOG_IN,
TOTP_CODE
}

View File

@ -23,6 +23,7 @@ public class LimboPlayer {
private final float flySpeed;
private BukkitTask timeoutTask = null;
private MessageTask messageTask = null;
private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED;
public LimboPlayer(Location loc, boolean operator, Collection<String> groups, boolean fly, float walkSpeed,
float flySpeed) {
@ -124,4 +125,12 @@ public class LimboPlayer {
setMessageTask(null);
setTimeoutTask(null);
}
public LimboPlayerState getState() {
return state;
}
public void setState(LimboPlayerState state) {
this.state = state;
}
}

View File

@ -0,0 +1,9 @@
package fr.xephi.authme.data.limbo;
public enum LimboPlayerState {
PASSWORD_REQUIRED,
TOTP_REQUIRED
}

View File

@ -45,11 +45,11 @@ class LimboPlayerTaskManager {
*
* @param player the player
* @param limbo the associated limbo player of the player
* @param isRegistered whether the player is registered or not (needed to determine the message in the task)
* @param messageType message type
*/
void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) {
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
MessageResult result = getMessageKey(player.getName(), isRegistered);
MessageResult result = getMessageKey(player.getName(), messageType);
if (interval > 0) {
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
MessageTask messageTask = new MessageTask(player, joinMessage);
@ -89,12 +89,14 @@ class LimboPlayerTaskManager {
* Returns the appropriate message key according to the registration status and settings.
*
* @param name the player's name
* @param isRegistered whether or not the username is registered
* @param messageType the message to show
* @return the message key to display to the user
*/
private MessageResult getMessageKey(String name, boolean isRegistered) {
if (isRegistered) {
private MessageResult getMessageKey(String name, LimboMessageType messageType) {
if (messageType == LimboMessageType.LOG_IN) {
return new MessageResult(MessageKey.LOGIN_MESSAGE);
} else if (messageType == LimboMessageType.TOTP_CODE) {
return new MessageResult(MessageKey.TWO_FACTOR_CODE_REQUIRED);
} else if (registrationCaptchaManager.isCaptchaRequired(name)) {
final String captchaCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(name);
return new MessageResult(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captchaCode);

View File

@ -69,7 +69,8 @@ public class LimboService {
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerMessageTask(player, limboPlayer,
isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER);
taskManager.registerTimeoutTask(player, limboPlayer);
helper.revokeLimboStates(player);
authGroupHandler.setGroup(player, limboPlayer,
@ -134,7 +135,7 @@ public class LimboService {
Optional<LimboPlayer> limboPlayer = getLimboOrLogError(player, "reset tasks");
limboPlayer.ifPresent(limbo -> {
taskManager.registerTimeoutTask(player, limbo);
taskManager.registerMessageTask(player, limbo, true);
taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN);
});
authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED);
}
@ -143,11 +144,11 @@ public class LimboService {
* Resets the message task associated with the player's LimboPlayer.
*
* @param player the player to set a new message task for
* @param isRegistered whether or not the player is registered
* @param messageType the message to show for the limbo player
*/
public void resetMessageTask(Player player, boolean isRegistered) {
public void resetMessageTask(Player player, LimboMessageType messageType) {
getLimboOrLogError(player, "reset message task")
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered));
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType));
}
/**

View File

@ -3,7 +3,6 @@ package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.LimboSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
@ -20,9 +19,6 @@ import static fr.xephi.authme.util.Utils.isCollectionEmpty;
*/
class LimboServiceHelper {
@Inject
private SpawnLoader spawnLoader;
@Inject
private PermissionsManager permissionsManager;

View File

@ -0,0 +1,169 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumns;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.security.crypts.HashedPassword;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import static ch.jalu.datasourcecolumns.data.UpdateValues.with;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Common type for SQL-based data sources. Classes implementing this
* must ensure that {@link #columnsHandler} is initialized on creation.
*/
public abstract class AbstractSqlDataSource implements DataSource {
protected AuthMeColumnsHandler columnsHandler;
@Override
public boolean isAuthAvailable(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists();
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public HashedPassword getPassword(String user) {
try {
DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT);
if (values.rowExists()) {
return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT));
}
} catch (SQLException e) {
logSqlException(e);
}
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
return columnsHandler.insert(auth,
AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT,
AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP);
}
@Override
public boolean hasSession(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public boolean updateSession(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME);
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
return columnsHandler.update(user,
with(AuthMeColumns.PASSWORD, password.getHash())
.and(AuthMeColumns.SALT, password.getSalt()).build());
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
return columnsHandler.update(auth,
AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z,
AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH);
}
@Override
public List<String> getAllAuthsByIp(String ip) {
try {
return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME);
} catch (SQLException e) {
logSqlException(e);
return Collections.emptyList();
}
}
@Override
public int countAuthsByEmail(String email) {
return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email));
}
@Override
public boolean updateEmail(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.EMAIL);
}
@Override
public boolean isLogged(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public void setLogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1);
}
@Override
public void setUnlogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0);
}
@Override
public void grantSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1);
}
@Override
public void revokeSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0);
}
@Override
public void purgeLogged() {
columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0);
}
@Override
public int getAccountsRegistered() {
return columnsHandler.count(new AlwaysTruePredicate<>());
}
@Override
public boolean updateRealName(String user, String realName) {
return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName);
}
@Override
public DataSourceValue<String> getEmail(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.EMAIL);
} catch (SQLException e) {
logSqlException(e);
return DataSourceValueImpl.unknownRow();
}
}
}

View File

@ -1,5 +1,7 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@ -244,10 +246,10 @@ public class CacheDataSource implements DataSource {
}
@Override
public DataSourceResult<String> getEmail(String user) {
public DataSourceValue<String> getEmail(String user) {
return cachedAuths.getUnchecked(user)
.map(auth -> DataSourceResult.of(auth.getEmail()))
.orElse(DataSourceResult.unknownPlayer());
.map(auth -> DataSourceValueImpl.of(auth.getEmail()))
.orElse(DataSourceValueImpl.unknownRow());
}
@Override
@ -268,6 +270,15 @@ public class CacheDataSource implements DataSource {
return source.getRecentlyLoggedInPlayers();
}
@Override
public boolean setTotpKey(String user, String totpKey) {
boolean result = source.setTotpKey(user, totpKey);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName);

View File

@ -14,6 +14,7 @@ public final class Columns {
public final String REAL_NAME;
public final String PASSWORD;
public final String SALT;
public final String TOTP_KEY;
public final String LAST_IP;
public final String LAST_LOGIN;
public final String GROUP;
@ -35,6 +36,7 @@ public final class Columns {
REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME);
PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD);
SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT);
TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY);
LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP);
LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN);
GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP);

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.security.crypts.HashedPassword;
@ -216,7 +217,7 @@ public interface DataSource extends Reloadable {
* @param user the user to retrieve an email for
* @return the email saved for the user, or null if user or email is not present
*/
DataSourceResult<String> getEmail(String user);
DataSourceValue<String> getEmail(String user);
/**
* Return all players of the database.
@ -232,6 +233,25 @@ public interface DataSource extends Reloadable {
*/
List<PlayerAuth> getRecentlyLoggedInPlayers();
/**
* Sets the given TOTP key to the player's account.
*
* @param user the name of the player to modify
* @param totpKey the totp key to set
* @return True upon success, false upon failure
*/
boolean setTotpKey(String user, String totpKey);
/**
* Removes the TOTP key if present of the given player's account.
*
* @param user the name of the player to modify
* @return True upon success, false upon failure
*/
default boolean removeTotpKey(String user) {
return setTotpKey(user, null);
}
/**
* Reload the data source.
*/

View File

@ -1,53 +0,0 @@
package fr.xephi.authme.datasource;
/**
* Wraps a value and allows to specify whether a value is missing or the player is not registered.
*/
public final class DataSourceResult<T> {
/** Instance used when a player does not exist. */
private static final DataSourceResult UNKNOWN_PLAYER = new DataSourceResult<>(null);
private final T value;
private DataSourceResult(T value) {
this.value = value;
}
/**
* Returns a {@link DataSourceResult} for the given value.
*
* @param value the value to wrap
* @param <T> the value's type
* @return DataSourceResult object for the given value
*/
public static <T> DataSourceResult<T> of(T value) {
return new DataSourceResult<>(value);
}
/**
* Returns a {@link DataSourceResult} specifying that the player does not exist.
*
* @param <T> the value type
* @return data source result for unknown player
*/
public static <T> DataSourceResult<T> unknownPlayer() {
return UNKNOWN_PLAYER;
}
/**
* @return whether the player of the associated value exists
*/
public boolean playerExists() {
return this != UNKNOWN_PLAYER;
}
/**
* Returns the value. It is {@code null} if the player is unknown. It is also {@code null}
* if the player exists but does not have the value defined.
*
* @return the value, or null
*/
public T getValue() {
return value;
}
}

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
@ -366,7 +367,7 @@ public class FlatFile implements DataSource {
}
@Override
public DataSourceResult<String> getEmail(String user) {
public DataSourceValue<String> getEmail(String user) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@ -398,6 +399,11 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public boolean setTotpKey(String user, String totpKey) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
/**
* Creates a PlayerAuth object from the read data.
*

View File

@ -5,13 +5,12 @@ import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.util.StringUtils;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@ -32,7 +31,7 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
* MySQL data source.
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class MySQL implements DataSource {
public class MySQL extends AbstractSqlDataSource {
private boolean useSsl;
private String host;
@ -99,6 +98,7 @@ public class MySQL implements DataSource {
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS);
this.col = new Columns(settings);
this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings);
this.sqlExtension = extensionsFactory.buildExtension(col);
this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE);
this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME);
@ -252,6 +252,11 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED);
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("MySQL setup finished");
}
@ -262,40 +267,6 @@ public class MySQL implements DataSource {
}
}
@Override
public boolean isAuthAvailable(String user) {
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
return rs.next();
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public HashedPassword getPassword(String user) {
boolean useSalt = !col.SALT.isEmpty();
String sql = "SELECT " + col.PASSWORD
+ (useSalt ? ", " + col.SALT : "")
+ " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new HashedPassword(rs.getString(col.PASSWORD),
useSalt ? rs.getString(col.SALT) : null);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;";
@ -318,33 +289,9 @@ public class MySQL implements DataSource {
@Override
public boolean saveAuth(PlayerAuth auth) {
super.saveAuth(auth);
try (Connection con = getConnection()) {
// TODO ljacqu 20171104: Replace with generic columns util to clean this up
boolean useSalt = !col.SALT.isEmpty() || !StringUtils.isEmpty(auth.getPassword().getSalt());
boolean hasEmail = auth.getEmail() != null;
String emailPlaceholder = hasEmail ? "?" : "DEFAULT";
String sql = "INSERT INTO " + tableName + "("
+ col.NAME + "," + col.PASSWORD + "," + col.REAL_NAME
+ "," + col.EMAIL + "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP
+ (useSalt ? "," + col.SALT : "")
+ ") VALUES (?,?,?," + emailPlaceholder + ",?,?" + (useSalt ? ",?" : "") + ");";
try (PreparedStatement pst = con.prepareStatement(sql)) {
int index = 1;
pst.setString(index++, auth.getNickname());
pst.setString(index++, auth.getPassword().getHash());
pst.setString(index++, auth.getRealName());
if (hasEmail) {
pst.setString(index++, auth.getEmail());
}
pst.setObject(index++, auth.getRegistrationDate());
pst.setString(index++, auth.getRegistrationIp());
if (useSalt) {
pst.setString(index++, auth.getPassword().getSalt());
}
pst.executeUpdate();
}
if (!columnOthers.isEmpty()) {
for (String column : columnOthers) {
try (PreparedStatement pst = con.prepareStatement(
@ -364,59 +311,6 @@ public class MySQL implements DataSource {
return false;
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
user = user.toLowerCase();
try (Connection con = getConnection()) {
boolean useSalt = !col.SALT.isEmpty();
if (useSalt) {
String sql = String.format("UPDATE %s SET %s = ?, %s = ? WHERE %s = ?;",
tableName, col.PASSWORD, col.SALT, col.NAME);
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, password.getHash());
pst.setString(2, password.getSalt());
pst.setString(3, user);
pst.executeUpdate();
}
} else {
String sql = String.format("UPDATE %s SET %s = ? WHERE %s = ?;",
tableName, col.PASSWORD, col.NAME);
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, password.getHash());
pst.setString(2, user);
pst.executeUpdate();
}
}
sqlExtension.changePassword(user, password, con);
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateSession(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET "
+ col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getLastIp());
pst.setObject(2, auth.getLastLogin());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
@ -454,42 +348,6 @@ public class MySQL implements DataSource {
return false;
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
String sql = "UPDATE " + tableName
+ " SET " + col.LASTLOC_X + " =?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, "
+ col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=?"
+ " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setDouble(1, auth.getQuitLocX());
pst.setDouble(2, auth.getQuitLocY());
pst.setDouble(3, auth.getQuitLocZ());
pst.setString(4, auth.getWorld());
pst.setFloat(5, auth.getYaw());
pst.setFloat(6, auth.getPitch());
pst.setString(7, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateEmail(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.EMAIL + " =? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getEmail());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
if (ds != null && !ds.isClosed()) {
@ -497,39 +355,6 @@ public class MySQL implements DataSource {
}
}
@Override
public List<String> getAllAuthsByIp(String ip) {
List<String> result = new ArrayList<>();
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, ip);
try (ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(col.NAME));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return result;
}
@Override
public int countAuthsByEmail(String email) {
String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE UPPER(" + col.EMAIL + ") = UPPER(?)";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, email);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public void purgeRecords(Collection<String> toPurge) {
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
@ -548,140 +373,6 @@ public class MySQL implements DataSource {
return DataSourceType.MYSQL;
}
@Override
public boolean isLogged(String user) {
String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
return rs.next() && (rs.getInt(col.IS_LOGGED) == 1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void setLogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void setUnlogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public boolean hasSession(String user) {
String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
return rs.next() && (rs.getInt(col.HAS_SESSION) == 1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void grantSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void revokeSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void purgeLogged() {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setInt(2, 1);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public int getAccountsRegistered() {
int result = 0;
String sql = "SELECT COUNT(*) FROM " + tableName;
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
if (rs.next()) {
result = rs.getInt(1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return result;
}
@Override
public boolean updateRealName(String user, String realName) {
String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, realName);
pst.setString(2, user);
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public DataSourceResult<String> getEmail(String user) {
String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return DataSourceResult.of(rs.getString(1));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return DataSourceResult.unknownPlayer();
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
@ -732,6 +423,20 @@ public class MySQL implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
/**
* Creates a {@link PlayerAuth} object with the data from the provided result set.
*
@ -746,6 +451,7 @@ public class MySQL implements DataSource {
.name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL))

View File

@ -3,10 +3,9 @@ package fr.xephi.authme.datasource;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.StringUtils;
import java.io.File;
import java.sql.Connection;
@ -29,7 +28,7 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
* SQLite data source.
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class SQLite implements DataSource {
public class SQLite extends AbstractSqlDataSource {
private final Settings settings;
private final File dataFolder;
@ -71,6 +70,7 @@ public class SQLite implements DataSource {
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.col = new Columns(settings);
this.con = connection;
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
@ -85,6 +85,7 @@ public class SQLite implements DataSource {
ConsoleLogger.debug("SQLite driver loaded");
this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db");
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
@ -172,6 +173,11 @@ public class SQLite implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';");
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("SQLite Setup finished");
}
@ -204,40 +210,6 @@ public class SQLite implements DataSource {
}
}
@Override
public boolean isAuthAvailable(String user) {
String sql = "SELECT 1 FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
return rs.next();
}
} catch (SQLException ex) {
ConsoleLogger.warning(ex.getMessage());
return false;
}
}
@Override
public HashedPassword getPassword(String user) {
boolean useSalt = !col.SALT.isEmpty();
String sql = "SELECT " + col.PASSWORD
+ (useSalt ? ", " + col.SALT : "")
+ " FROM " + tableName + " WHERE " + col.NAME + "=?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new HashedPassword(rs.getString(col.PASSWORD),
useSalt ? rs.getString(col.SALT) : null);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);";
@ -254,95 +226,6 @@ public class SQLite implements DataSource {
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
PreparedStatement pst = null;
try {
HashedPassword password = auth.getPassword();
if (col.SALT.isEmpty()) {
if (!StringUtils.isEmpty(auth.getPassword().getSalt())) {
ConsoleLogger.warning("Warning! Detected hashed password with separate salt but the salt column "
+ "is not set in the config!");
}
pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD
+ "," + col.REAL_NAME + "," + col.EMAIL
+ "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP
+ ") VALUES (?,?,?,?,?,?);");
pst.setString(1, auth.getNickname());
pst.setString(2, password.getHash());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getEmail());
pst.setLong(5, auth.getRegistrationDate());
pst.setString(6, auth.getRegistrationIp());
pst.executeUpdate();
} else {
pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD
+ "," + col.REAL_NAME + "," + col.EMAIL
+ "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP + "," + col.SALT
+ ") VALUES (?,?,?,?,?,?,?);");
pst.setString(1, auth.getNickname());
pst.setString(2, password.getHash());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getEmail());
pst.setLong(5, auth.getRegistrationDate());
pst.setString(6, auth.getRegistrationIp());
pst.setString(7, password.getSalt());
pst.executeUpdate();
}
} catch (SQLException ex) {
logSqlException(ex);
} finally {
close(pst);
}
return true;
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
user = user.toLowerCase();
boolean useSalt = !col.SALT.isEmpty();
String sql = "UPDATE " + tableName + " SET " + col.PASSWORD + " = ?"
+ (useSalt ? ", " + col.SALT + " = ?" : "")
+ " WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)){
pst.setString(1, password.getHash());
if (useSalt) {
pst.setString(2, password.getSalt());
pst.setString(3, user);
} else {
pst.setString(2, user);
}
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateSession(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, "
+ col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)){
pst.setString(1, auth.getLastIp());
pst.setObject(2, auth.getLastLogin());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
@ -390,42 +273,6 @@ public class SQLite implements DataSource {
return false;
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET "
+ col.LASTLOC_X + "=?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, "
+ col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=? "
+ "WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setDouble(1, auth.getQuitLocX());
pst.setDouble(2, auth.getQuitLocY());
pst.setDouble(3, auth.getQuitLocZ());
pst.setString(4, auth.getWorld());
pst.setFloat(5, auth.getYaw());
pst.setFloat(6, auth.getPitch());
pst.setString(7, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateEmail(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.EMAIL + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getEmail());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
try {
@ -437,180 +284,11 @@ public class SQLite implements DataSource {
}
}
@Override
public List<String> getAllAuthsByIp(String ip) {
List<String> countIp = new ArrayList<>();
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, ip);
try (ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
countIp.add(rs.getString(col.NAME));
}
return countIp;
}
} catch (SQLException ex) {
logSqlException(ex);
}
return new ArrayList<>();
}
@Override
public int countAuthsByEmail(String email) {
String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE " + col.EMAIL + " = ? COLLATE NOCASE;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, email);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public DataSourceType getType() {
return DataSourceType.SQLITE;
}
@Override
public boolean isLogged(String user) {
String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(col.IS_LOGGED) == 1;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void setLogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void setUnlogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public boolean hasSession(String user) {
String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(col.HAS_SESSION) == 1;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void grantSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void revokeSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void purgeLogged() {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setInt(2, 1);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public int getAccountsRegistered() {
String sql = "SELECT COUNT(*) FROM " + tableName + ";";
try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public boolean updateRealName(String user, String realName) {
String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, realName);
pst.setString(2, user);
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public DataSourceResult<String> getEmail(String user) {
String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return DataSourceResult.of(rs.getString(1));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return DataSourceResult.unknownPlayer();
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
@ -655,6 +333,21 @@ public class SQLite implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
@ -663,6 +356,7 @@ public class SQLite implements DataSource {
.email(row.getString(col.EMAIL))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.registrationDate(row.getLong(col.REGISTRATION_DATE))
@ -695,16 +389,6 @@ public class SQLite implements DataSource {
+ currentTimestamp + ", to all " + updatedRows + " rows");
}
private static void close(Statement st) {
if (st != null) {
try {
st.close();
} catch (SQLException ex) {
logSqlException(ex);
}
}
}
private static void close(Connection con) {
if (con != null) {
try {

View File

@ -0,0 +1,82 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createFloat;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createInteger;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createLong;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString;
/**
* Contains column definitions for the AuthMe table.
*/
public final class AuthMeColumns {
public static final PlayerAuthColumn<String> NAME = createString(
DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname);
public static final PlayerAuthColumn<String> NICK_NAME = createString(
DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName);
public static final PlayerAuthColumn<String> PASSWORD = createString(
DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash());
public static final PlayerAuthColumn<String> SALT = createString(
DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL);
public static final PlayerAuthColumn<String> EMAIL = createString(
DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL);
public static final PlayerAuthColumn<String> LAST_IP = createString(
DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp);
public static final PlayerAuthColumn<Integer> GROUP_ID = createInteger(
DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL);
public static final PlayerAuthColumn<Long> LAST_LOGIN = createLong(
DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin);
public static final PlayerAuthColumn<String> REGISTRATION_IP = createString(
DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp);
public static final PlayerAuthColumn<Long> REGISTRATION_DATE = createLong(
DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate);
// --------
// Location columns
// --------
public static final PlayerAuthColumn<Double> LOCATION_X = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX);
public static final PlayerAuthColumn<Double> LOCATION_Y = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY);
public static final PlayerAuthColumn<Double> LOCATION_Z = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ);
public static final PlayerAuthColumn<String> LOCATION_WORLD = createString(
DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld);
public static final PlayerAuthColumn<Float> LOCATION_YAW = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw);
public static final PlayerAuthColumn<Float> LOCATION_PITCH = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch);
// --------
// Columns not on PlayerAuth
// --------
public static final DataSourceColumn<Integer> IS_LOGGED = createInteger(
DatabaseSettings.MYSQL_COL_ISLOGGED);
public static final DataSourceColumn<Integer> HAS_SESSION = createInteger(
DatabaseSettings.MYSQL_COL_HASSESSION);
private AuthMeColumns() {
}
}

View File

@ -0,0 +1,83 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.StandardTypes;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Util class for initializing {@link DataSourceColumn} objects.
*/
final class AuthMeColumnsFactory {
private AuthMeColumnsFactory() {
}
static DataSourceColumn<Integer> createInteger(Property<String> nameProperty,
ColumnOptions... options) {
return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty,
isOptional(options), hasDefaultForNull(options));
}
static PlayerAuthColumn<Integer> createInteger(Property<String> nameProperty,
Function<PlayerAuth, Integer> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Long> createLong(Property<String> nameProperty,
Function<PlayerAuth, Long> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<String> createString(Property<String> nameProperty,
Function<PlayerAuth, String> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Double> createDouble(Property<String> nameProperty,
Function<PlayerAuth, Double> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Float> createFloat(Property<String> nameProperty,
Function<PlayerAuth, Float> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options);
}
private static <T> PlayerAuthColumn<T> createInternal(ColumnType<T> type, Property<String> nameProperty,
Function<PlayerAuth, T> authGetter,
ColumnOptions... options) {
return new PlayerAuthColumn<>(type, nameProperty, isOptional(options), hasDefaultForNull(options), authGetter);
}
private static boolean isOptional(ColumnOptions[] options) {
return containsInArray(ColumnOptions.OPTIONAL, options);
}
private static boolean hasDefaultForNull(ColumnOptions[] options) {
return containsInArray(ColumnOptions.DEFAULT_FOR_NULL, options);
}
private static boolean containsInArray(ColumnOptions needle, ColumnOptions[] haystack) {
for (ColumnOptions option : haystack) {
if (option == needle) {
return true;
}
}
return false;
}
enum ColumnOptions {
OPTIONAL,
DEFAULT_FOR_NULL
}
}

View File

@ -0,0 +1,206 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.data.UpdateValues;
import ch.jalu.datasourcecolumns.predicate.Predicate;
import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator;
import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler;
import ch.jalu.datasourcecolumns.sqlimplementation.statementgenerator.ConnectionSupplier;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forConnectionPool;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forSingleConnection;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Wrapper of {@link SqlColumnsHandler} for the AuthMe data table.
* Wraps exceptions and provides better support for operations based on a {@link PlayerAuth} object.
*/
public final class AuthMeColumnsHandler {
private final SqlColumnsHandler<ColumnContext, String> internalHandler;
private AuthMeColumnsHandler(SqlColumnsHandler<ColumnContext, String> internalHandler) {
this.internalHandler = internalHandler;
}
/**
* Creates a column handler for SQLite.
*
* @param connection the connection to the database
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, false);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forSingleConnection(connection, tableName, nameColumn, columnContext)
.setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, true))
);
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Creates a column handler for MySQL.
*
* @param connectionSupplier supplier of connections from the connection pool
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForMySql(ConnectionSupplier connectionSupplier, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, true);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forConnectionPool(connectionSupplier, tableName, nameColumn, columnContext));
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Changes a column from a specific row to the given value.
*
* @param name name of the account to modify
* @param column the column to modify
* @param value the value to set the column to
* @param <T> the column type
* @return true upon success, false otherwise
*/
public <T> boolean update(String name, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(name, column, value);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the values as retrieved from the PlayerAuth object.
*
* @param auth the player auth object to modify and to get values from
* @param columns the columns to update in the row
* @return true upon success, false otherwise
*/
public boolean update(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.update(auth.getNickname(), auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the given values.
*
* @param name the name of the account to modify
* @param updateValues the values to set on the row
* @return true upon success, false otherwise
*/
public boolean update(String name, UpdateValues<ColumnContext> updateValues) {
try {
return internalHandler.update(name.toLowerCase(), updateValues);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Sets the given value to the provided column for all rows which match the predicate.
*
* @param predicate the predicate to filter rows by
* @param column the column to modify on the matched rows
* @param value the new value to set
* @param <T> the column type
* @return number of modified rows
*/
public <T> int update(Predicate<ColumnContext> predicate, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(predicate, column, value);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
/**
* Retrieves the given column from a given row.
*
* @param name the account name to look up
* @param column the column whose value should be retrieved
* @param <T> the column type
* @return the result of the lookup
* @throws SQLException .
*/
public <T> DataSourceValue<T> retrieve(String name, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(), column);
}
/**
* Retrieves multiple values from a given row.
*
* @param name the account name to look up
* @param columns the columns to retrieve
* @return map-like object with the requested values
* @throws SQLException .
*/
public DataSourceValues retrieve(String name, DataSourceColumn<?>... columns) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(), columns);
}
/**
* Retrieves a column's value for all rows that satisfy the given predicate.
*
* @param predicate the predicate to fulfill
* @param column the column to retrieve from the matching rows
* @param <T> the column's value type
* @return the values of the matching rows
* @throws SQLException .
*/
public <T> List<T> retrieve(Predicate<ColumnContext> predicate, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(predicate, column);
}
/**
* Inserts the given values into a new row, as taken from the player auth.
*
* @param auth the player auth to get values from
* @param columns the columns to insert
* @return true upon success, false otherwise
*/
public boolean insert(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.insert(auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Returns the number of rows that match the provided predicate.
*
* @param predicate the predicate to test the rows for
* @return number of rows fulfilling the predicate
*/
public int count(Predicate<ColumnContext> predicate) {
try {
return internalHandler.count(predicate);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
}

View File

@ -0,0 +1,35 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.settings.Settings;
import java.util.HashMap;
import java.util.Map;
/**
* Context for resolving the properties of {@link AuthMeColumns} entries.
*/
public class ColumnContext {
private final Settings settings;
private final Map<DataSourceColumn<?>, String> columnNames = new HashMap<>();
private final boolean hasDefaultSupport;
/**
* Constructor.
*
* @param settings plugin settings
* @param hasDefaultSupport whether or not the underlying database has support for the {@code DEFAULT} keyword
*/
public ColumnContext(Settings settings, boolean hasDefaultSupport) {
this.settings = settings;
this.hasDefaultSupport = hasDefaultSupport;
}
public String getName(DataSourceColumn<?> column) {
return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty()));
}
public boolean hasDefaultSupport() {
return hasDefaultSupport;
}
}

View File

@ -0,0 +1,57 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.Column;
import ch.jalu.datasourcecolumns.ColumnType;
/**
* Basic {@link Column} implementation for AuthMe.
*
* @param <T> column type
*/
public class DataSourceColumn<T> implements Column<T, ColumnContext> {
private final ColumnType<T> columnType;
private final Property<String> nameProperty;
private final boolean isOptional;
private final boolean useDefaultForNull;
/**
* Constructor.
*
* @param type type of the column
* @param nameProperty property defining the column name
* @param isOptional whether or not the column can be skipped (if name is configured to empty string)
* @param useDefaultForNull whether SQL DEFAULT should be used for null values (if supported by the database)
*/
DataSourceColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull) {
this.columnType = type;
this.nameProperty = nameProperty;
this.isOptional = isOptional;
this.useDefaultForNull = useDefaultForNull;
}
public Property<String> getNameProperty() {
return nameProperty;
}
@Override
public String resolveName(ColumnContext columnContext) {
return columnContext.getName(this);
}
@Override
public ColumnType<T> getType() {
return columnType;
}
@Override
public boolean isColumnUsed(ColumnContext columnContext) {
return !isOptional || !resolveName(columnContext).isEmpty();
}
@Override
public boolean useDefaultForNullValue(ColumnContext columnContext) {
return useDefaultForNull && columnContext.hasDefaultSupport();
}
}

View File

@ -0,0 +1,32 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.DependentColumn;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Implementation for columns which can also be retrieved from a {@link PlayerAuth} object.
*
* @param <T> column type
*/
public class PlayerAuthColumn<T> extends DataSourceColumn<T> implements DependentColumn<T, ColumnContext, PlayerAuth> {
private final Function<PlayerAuth, T> playerAuthGetter;
/*
* Constructor. See parent class for details.
*/
PlayerAuthColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull,
Function<PlayerAuth, T> playerAuthGetter) {
super(type, nameProperty, isOptional, useDefaultForNull);
this.playerAuthGetter = playerAuthGetter;
}
@Override
public T getValueFromDependent(PlayerAuth auth) {
return playerAuthGetter.apply(auth);
}
}

View File

@ -0,0 +1,70 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* This event is called when a player uses the register command,
* it's fired even when a user does a /register with invalid arguments.
* {@link #setCanRegister(boolean) event.setCanRegister(false)} prevents the player from registering.
*/
public class AuthMeAsyncPreRegisterEvent extends CustomEvent {
private static final HandlerList handlers = new HandlerList();
private final Player player;
private boolean canRegister = true;
/**
* Constructor.
*
* @param player The player
* @param isAsync True if the event is async, false otherwise
*/
public AuthMeAsyncPreRegisterEvent(Player player, boolean isAsync) {
super(isAsync);
this.player = player;
}
/**
* Return the player concerned by this event.
*
* @return The player who executed a valid {@code /login} command
*/
public Player getPlayer() {
return player;
}
/**
* Return whether the player is allowed to register.
*
* @return True if the player can log in, false otherwise
*/
public boolean canRegister() {
return canRegister;
}
/**
* Define whether or not the player may register.
*
* @param canRegister True to allow the player to log in; false to prevent him
*/
public void setCanRegister(boolean canRegister) {
this.canRegister = canRegister;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,87 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import javax.annotation.Nullable;
/**
* This event is called when a player adds or changes his email address.
*/
public class EmailChangedEvent extends CustomEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private final Player player;
private final String oldEmail;
private final String newEmail;
private boolean isCancelled;
/**
* Constructor
*
* @param player The player that changed email
* @param oldEmail Old email player had on file. Can be null when user adds an email
* @param newEmail New email that player tries to set. In case of adding email, this will contain
* the email is trying to set.
* @param isAsync should this event be called asynchronously?
*/
public EmailChangedEvent(Player player, @Nullable String oldEmail, String newEmail, boolean isAsync) {
super(isAsync);
this.player = player;
this.oldEmail = oldEmail;
this.newEmail = newEmail;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
/**
* Gets the player who changes the email
*
* @return The player who changed the email
*/
public Player getPlayer() {
return player;
}
/**
* Gets the old email in case user tries to change existing email.
*
* @return old email stored on file. Can be null when user never had an email and adds a new one.
*/
public @Nullable String getOldEmail() {
return this.oldEmail;
}
/**
* Gets the new email.
*
* @return the email user is trying to set. If user adds email and never had one before,
* this is where such email can be found.
*/
public String getNewEmail() {
return this.newEmail;
}
@Override
public void setCancelled(boolean cancelled) {
this.isCancelled = cancelled;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,47 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Event fired when a player has successfully registered.
*/
public class RegisterEvent extends CustomEvent {
private static final HandlerList handlers = new HandlerList();
private final Player player;
/**
* Constructor.
*
* @param player The player
*/
public RegisterEvent(Player player) {
this.player = player;
}
/**
* Return the player that has successfully logged in or registered.
*
* @return The player
*/
public Player getPlayer() {
return player;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -1,11 +1,14 @@
package fr.xephi.authme.listener;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.QuickCommandsProtectionManager;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.permission.handlers.PermissionLoadUserException;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.AntiBotService;
import fr.xephi.authme.service.BukkitService;
@ -51,6 +54,8 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;
import javax.inject.Inject;
import java.util.HashSet;
import java.util.Set;
import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS;
import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT;
@ -63,7 +68,7 @@ public class PlayerListener implements Listener {
@Inject
private Settings settings;
@Inject
private Messages m;
private Messages messages;
@Inject
private DataSource dataSource;
@Inject
@ -92,6 +97,7 @@ public class PlayerListener implements Listener {
private BungeeSender bungeeSender;
private boolean isAsyncPlayerPreLoginEventCalled = false;
private Set<String> unresolvedPlayerHostname = new HashSet<>();
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
@ -105,12 +111,12 @@ public class PlayerListener implements Listener {
final Player player = event.getPlayer();
if (!quickCommandsProtectionManager.isAllowed(player.getName())) {
event.setCancelled(true);
player.kickPlayer(m.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK));
player.kickPlayer(messages.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK));
return;
}
if (listenerService.shouldCancelEvent(player)) {
event.setCancelled(true);
m.send(player, MessageKey.DENIED_COMMAND);
messages.send(player, MessageKey.DENIED_COMMAND);
}
}
@ -121,10 +127,18 @@ public class PlayerListener implements Listener {
}
final Player player = event.getPlayer();
if (listenerService.shouldCancelEvent(player)) {
final boolean mayPlayerSendChat = !listenerService.shouldCancelEvent(player)
|| permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN);
if (mayPlayerSendChat) {
removeUnauthorizedRecipients(event);
} else {
event.setCancelled(true);
m.send(player, MessageKey.DENIED_CHAT);
} else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) {
messages.send(player, MessageKey.DENIED_CHAT);
}
}
private void removeUnauthorizedRecipients(AsyncPlayerChatEvent event) {
if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) {
event.getRecipients().removeIf(listenerService::shouldCancelEvent);
if (event.getRecipients().isEmpty()) {
event.setCancelled(true);
@ -252,6 +266,15 @@ public class PlayerListener implements Listener {
return;
}
// getAddress() sometimes returning null if not yet resolved
// skip it and let PlayerLoginEvent to handle it
if (event.getAddress() == null) {
unresolvedPlayerHostname.add(event.getName());
return;
} else {
unresolvedPlayerHostname.remove(event.getName());
}
final String name = event.getName();
if (validationService.isUnrestricted(name)) {
@ -260,15 +283,19 @@ public class PlayerListener implements Listener {
// Keep pre-UUID compatibility
try {
permissionsManager.loadUserData(event.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(name);
try {
permissionsManager.loadUserData(event.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(name);
}
} catch (PermissionLoadUserException e) {
ConsoleLogger.logException("Unable to load the permission data of user " + name, e);
}
try {
runOnJoinChecks(JoiningPlayer.fromName(name), event.getAddress().getHostAddress());
} catch (FailedVerificationException e) {
event.setKickMessage(m.retrieveSingle(name, e.getReason(), e.getArgs()));
event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs()));
event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
}
}
@ -292,11 +319,13 @@ public class PlayerListener implements Listener {
return;
}
if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT)) {
if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT)
|| unresolvedPlayerHostname.remove(name)) {
try {
runOnJoinChecks(JoiningPlayer.fromPlayerObject(player), event.getAddress().getHostAddress());
} catch (FailedVerificationException e) {
event.setKickMessage(m.retrieveSingle(player, e.getReason(), e.getArgs()));
event.setKickMessage(messages.retrieveSingle(player, e.getReason(), e.getArgs()));
event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
}
}

View File

@ -125,7 +125,7 @@ public enum MessageKey {
/** Forgot your password? Please use the command: /email recovery &lt;yourEmail&gt; */
FORGOT_PASSWORD_MESSAGE("recovery.forgot_password_hint"),
/** To login you have to solve a captcha code, please use the command: /captcha %captcha_code */
/** To log in you have to solve a captcha code, please use the command: /captcha %captcha_code */
USAGE_CAPTCHA("captcha.usage_captcha", "%captcha_code"),
/** Wrong captcha, please type "/captcha %captcha_code" into the chat! */
@ -134,7 +134,7 @@ public enum MessageKey {
/** Captcha code solved correctly! */
CAPTCHA_SUCCESS("captcha.valid_captcha"),
/** To register you have to solve a captcha code first, please use the command: /captcha %captcha_code */
/** To register you have to solve a captcha first, please use the command: /captcha %captcha_code */
CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha.captcha_for_registration", "%captcha_code"),
/** Valid captcha! You may now register with /register */
@ -167,12 +167,18 @@ public enum MessageKey {
/** Email address successfully added to your account! */
EMAIL_ADDED_SUCCESS("email.added"),
/** Adding email was not allowed */
EMAIL_ADD_NOT_ALLOWED("email.add_not_allowed"),
/** Please confirm your email address! */
CONFIRM_EMAIL_MESSAGE("email.request_confirmation"),
/** Email address changed correctly! */
EMAIL_CHANGED_SUCCESS("email.changed"),
/** Changing email was not allowed */
EMAIL_CHANGE_NOT_ALLOWED("email.change_not_allowed"),
/** Your current email address is: %email */
EMAIL_SHOW("email.email_show", "%email"),
@ -195,7 +201,34 @@ public enum MessageKey {
EMAIL_ALREADY_USED_ERROR("email.already_used"),
/** Your secret code is %code. You can scan it from here %url */
TWO_FACTOR_CREATE("misc.two_factor_create", "%code", "%url"),
TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"),
/** Please confirm your code with /2fa confirm &lt;code&gt; */
TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED("two_factor.confirmation_required"),
/** Please submit your two-factor authentication code with /2fa code &lt;code&gt; */
TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"),
/** Two-factor authentication is already enabled for your account! */
TWO_FACTOR_ALREADY_ENABLED("two_factor.already_enabled"),
/** No 2fa key has been generated for you or it has expired. Please run /2fa add */
TWO_FACTOR_ENABLE_ERROR_NO_CODE("two_factor.enable_error_no_code"),
/** Successfully enabled two-factor authentication for your account */
TWO_FACTOR_ENABLE_SUCCESS("two_factor.enable_success"),
/** Wrong code or code has expired. Please run /2fa add */
TWO_FACTOR_ENABLE_ERROR_WRONG_CODE("two_factor.enable_error_wrong_code"),
/** Two-factor authentication is not enabled for your account. Run /2fa add */
TWO_FACTOR_NOT_ENABLED_ERROR("two_factor.not_enabled_error"),
/** Successfully removed two-factor auth from your account */
TWO_FACTOR_REMOVED_SUCCESS("two_factor.removed_success"),
/** Invalid code! */
TWO_FACTOR_INVALID_CODE("two_factor.invalid_code"),
/** You are not the owner of this account. Please choose another name! */
NOT_OWNER_ERROR("on_join_validation.not_owner_error"),
@ -246,27 +279,21 @@ public enum MessageKey {
EMAIL_COOLDOWN_ERROR("email.email_cooldown_error", "%time"),
/**
* The command you are trying to execute is sensitive and requires a verification!
* A verification code has been sent to your email,
* run the command "/verification [code]" to verify your identity.
* This command is sensitive and requires an email verification!
* Check your inbox and follow the email's instructions.
*/
VERIFICATION_CODE_REQUIRED("verification.code_required"),
/** Usage: /verification &lt;code&gt; */
USAGE_VERIFICATION_CODE("verification.command_usage"),
/** Incorrect code, please type "/verification &lt;code&gt;" into the chat! */
/** Incorrect code, please type "/verification &lt;code&gt;" into the chat, using the code you received by email */
INCORRECT_VERIFICATION_CODE("verification.incorrect_code"),
/**
* Your identity has been verified!
* You can now execute every sensitive command within the current session!
*/
/** Your identity has been verified! You can now execute all commands within the current session! */
VERIFICATION_CODE_VERIFIED("verification.success"),
/**
* You can already execute every sensitive command within the current session!
*/
/** You can already execute every sensitive command within the current session! */
VERIFICATION_CODE_ALREADY_VERIFIED("verification.already_verified"),
/** Your code has expired! Execute another sensitive command to get a new code! */

View File

@ -5,7 +5,7 @@ import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.configurationdata.PropertyListBuilder;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.properties.StringProperty;
import ch.jalu.configme.resource.YamlFileResource;
import ch.jalu.configme.resource.PropertyResource;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import fr.xephi.authme.ConsoleLogger;
@ -57,14 +57,16 @@ public class MessageUpdater {
*/
private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) {
// YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe
YamlFileResource userResource = new MigraterYamlFileResource(userFile);
PropertyResource userResource = new MigraterYamlFileResource(userFile);
// Step 1: Migrate any old keys in the file to the new paths
boolean movedOldKeys = migrateOldKeys(userResource);
// Step 2: Take any missing messages from the message files shipped in the AuthMe JAR
// Step 2: Perform newer migrations
boolean movedNewerKeys = migrateKeys(userResource);
// Step 3: Take any missing messages from the message files shipped in the AuthMe JAR
boolean addedMissingKeys = addMissingKeys(jarMessageSource, userResource);
if (movedOldKeys || addedMissingKeys) {
if (movedOldKeys || movedNewerKeys || addedMissingKeys) {
backupMessagesFile(userFile);
SettingsManager settingsManager = new SettingsManager(userResource, null, CONFIGURATION_DATA);
@ -75,7 +77,19 @@ public class MessageUpdater {
return false;
}
private boolean migrateOldKeys(YamlFileResource userResource) {
private boolean migrateKeys(PropertyResource userResource) {
return moveIfApplicable(userResource, "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE.getKey());
}
private static boolean moveIfApplicable(PropertyResource resource, String oldPath, String newPath) {
if (resource.getString(newPath) == null && resource.getString(oldPath) != null) {
resource.setValue(newPath, resource.getString(oldPath));
return true;
}
return false;
}
private boolean migrateOldKeys(PropertyResource userResource) {
boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(userResource);
if (hasChange) {
ConsoleLogger.info("Old keys have been moved to the new ones in your messages_xx.yml file");
@ -83,7 +97,7 @@ public class MessageUpdater {
return hasChange;
}
private boolean addMissingKeys(JarMessageSource jarMessageSource, YamlFileResource userResource) {
private boolean addMissingKeys(JarMessageSource jarMessageSource, PropertyResource userResource) {
List<String> addedKeys = new ArrayList<>();
for (Property<?> property : CONFIGURATION_DATA.getProperties()) {
final String key = property.getPath();
@ -131,6 +145,7 @@ public class MessageUpdater {
.put("captcha", new String[]{"Captcha"})
.put("verification", new String[]{"Verification code"})
.put("time", new String[]{"Time units"})
.put("two_factor", new String[]{"Two-factor authentication"})
.build();
Set<String> addedKeys = new HashSet<>();

View File

@ -8,6 +8,7 @@ import fr.xephi.authme.permission.handlers.BPermissionsHandler;
import fr.xephi.authme.permission.handlers.LuckPermsHandler;
import fr.xephi.authme.permission.handlers.PermissionHandler;
import fr.xephi.authme.permission.handlers.PermissionHandlerException;
import fr.xephi.authme.permission.handlers.PermissionLoadUserException;
import fr.xephi.authme.permission.handlers.PermissionsExHandler;
import fr.xephi.authme.permission.handlers.VaultHandler;
import fr.xephi.authme.permission.handlers.ZPermissionsHandler;
@ -110,7 +111,9 @@ public class PermissionsManager implements Reloadable {
* Creates a permission handler for the provided permission systems if possible.
*
* @param type the permission systems type for which to create a corresponding permission handler
*
* @return the permission handler, or {@code null} if not possible
*
* @throws PermissionHandlerException during initialization of the permission handler
*/
private PermissionHandler createPermissionHandler(PermissionsSystemType type) throws PermissionHandlerException {
@ -228,8 +231,9 @@ public class PermissionsManager implements Reloadable {
/**
* Check if the given player has permission for the given permission node.
*
* @param joiningPlayer The player to check
* @param joiningPlayer The player to check
* @param permissionNode The permission node to verify
*
* @return true if the player has permission, false otherwise
*/
public boolean hasPermission(JoiningPlayer joiningPlayer, PermissionNode permissionNode) {
@ -262,7 +266,7 @@ public class PermissionsManager implements Reloadable {
* Check whether the offline player with the given name has permission for the given permission node.
* This method is used as a last resort when nothing besides the name is known.
*
* @param name The name of the player
* @param name The name of the player
* @param permissionNode The permission node to verify
*
* @return true if the player has permission, false otherwise
@ -317,7 +321,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName The group name.
*
* @return True if the player is in the specified group, false otherwise.
* False is also returned if groups aren't supported by the used permissions system.
* False is also returned if groups aren't supported by the used permissions system.
*/
public boolean isInGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.isInGroup(player, groupName);
@ -330,7 +334,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName The name of the group.
*
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean addGroup(OfflinePlayer player, String groupName) {
if (!isEnabled() || StringUtils.isEmpty(groupName)) {
@ -346,7 +350,7 @@ public class PermissionsManager implements Reloadable {
* @param groupNames The name of the groups to add.
*
* @return True if at least one group was added, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean addGroups(OfflinePlayer player, Collection<String> groupNames) {
// If no permissions system is used, return false
@ -373,7 +377,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName The name of the group.
*
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean removeGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.removeFromGroup(player, groupName);
@ -386,7 +390,7 @@ public class PermissionsManager implements Reloadable {
* @param groupNames The name of the groups to remove.
*
* @return True if at least one group was removed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean removeGroups(OfflinePlayer player, Collection<String> groupNames) {
// If no permissions system is used, return false
@ -414,7 +418,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName The name of the group.
*
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean setGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.setGroup(player, groupName);
@ -428,7 +432,7 @@ public class PermissionsManager implements Reloadable {
* @param player The player to remove all groups from.
*
* @return True if succeed, false otherwise.
* False will also be returned if this feature isn't supported for the used permissions system.
* False will also be returned if this feature isn't supported for the used permissions system.
*/
public boolean removeAllGroups(OfflinePlayer player) {
// If no permissions system is used, return false
@ -443,15 +447,47 @@ public class PermissionsManager implements Reloadable {
return removeGroups(player, groupNames);
}
public void loadUserData(UUID uuid) {
if(!isEnabled()) {
/**
* Loads the permission data of the given player.
*
* @param offlinePlayer the offline player.
* @return true if the load was successful.
*/
public boolean loadUserData(OfflinePlayer offlinePlayer) {
try {
try {
loadUserData(offlinePlayer.getUniqueId());
} catch (NoSuchMethodError e) {
loadUserData(offlinePlayer.getName());
}
} catch (PermissionLoadUserException e) {
ConsoleLogger.logException("Unable to load the permission data of user " + offlinePlayer.getName(), e);
return false;
}
return true;
}
/**
* Loads the permission data of the given player unique identifier.
*
* @param uuid the {@link UUID} of the player.
* @throws PermissionLoadUserException if the action failed.
*/
public void loadUserData(UUID uuid) throws PermissionLoadUserException {
if (!isEnabled()) {
return;
}
handler.loadUserData(uuid);
}
public void loadUserData(String name) {
if(!isEnabled()) {
/**
* Loads the permission data of the given player name.
*
* @param name the name of the player.
* @throws PermissionLoadUserException if the action failed.
*/
public void loadUserData(String name) throws PermissionLoadUserException {
if (!isEnabled()) {
return;
}
handler.loadUserData(name);

View File

@ -73,7 +73,17 @@ public enum PlayerPermission implements PermissionNode {
/**
* Permission that enables on join quick commands checks for the player.
*/
QUICK_COMMANDS_PROTECTION("authme.player.protection.quickcommandsprotection");
QUICK_COMMANDS_PROTECTION("authme.player.protection.quickcommandsprotection"),
/**
* Permission to enable two-factor authentication.
*/
ENABLE_TWO_FACTOR_AUTH("authme.player.totpadd"),
/**
* Permission to disable two-factor authentication.
*/
DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove");
/**
* The permission node.

View File

@ -34,7 +34,12 @@ public enum PlayerStatePermission implements PermissionNode {
/**
* Permission to bypass the GeoIp country code check.
*/
BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED);
BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED),
/**
* Permission to send chat messages before being logged in.
*/
ALLOW_CHAT_BEFORE_LOGIN("authme.allowchatbeforelogin", DefaultPermission.NOT_ALLOWED);
/**
* The permission node.
@ -42,7 +47,7 @@ public enum PlayerStatePermission implements PermissionNode {
private String node;
/**
* The default permission level
* The default permission level.
*/
private DefaultPermission defaultPermission;

View File

@ -189,22 +189,21 @@ public class LuckPermsHandler implements PermissionHandler {
}
@Override
public void loadUserData(UUID uuid) {
public void loadUserData(UUID uuid) throws PermissionLoadUserException {
try {
luckPermsApi.getUserManager().loadUser(uuid).get(5, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
throw new PermissionLoadUserException("Unable to load the permission data of the user " + uuid, e);
}
}
@Override
public void loadUserData(String name) {
public void loadUserData(String name) throws PermissionLoadUserException {
try {
UUID uuid = luckPermsApi.getStorage().getUUID(name).get(5, TimeUnit.SECONDS);
loadUserData(uuid);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
throw new PermissionLoadUserException("Unable to load the permission data of the user " + name, e);
}
}
}

View File

@ -107,10 +107,9 @@ public interface PermissionHandler {
*/
PermissionsSystemType getPermissionSystem();
default void loadUserData(UUID uuid) {
default void loadUserData(UUID uuid) throws PermissionLoadUserException {
}
default void loadUserData(String name) {
default void loadUserData(String name) throws PermissionLoadUserException {
}
}

View File

@ -0,0 +1,13 @@
package fr.xephi.authme.permission.handlers;
import java.util.UUID;
/**
* Exception thrown when a {@link PermissionHandler#loadUserData(UUID uuid)} request fails.
*/
public class PermissionLoadUserException extends Exception {
public PermissionLoadUserException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -4,8 +4,10 @@ import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.EmailChangedEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
@ -35,6 +37,9 @@ public class AsyncAddEmail implements AsynchronousProcess {
@Inject
private BungeeSender bungeeSender;
@Inject
private BukkitService bukkitService;
AsyncAddEmail() { }
/**
@ -57,6 +62,13 @@ public class AsyncAddEmail implements AsynchronousProcess {
} else if (!validationService.isEmailFreeForRegistration(email, player)) {
service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR);
} else {
EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync
-> new EmailChangedEvent(player, null, email, isAsync));
if (event.isCancelled()) {
ConsoleLogger.info("Could not add email to player '" + player + "' event was cancelled");
service.send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED);
return;
}
auth.setEmail(email);
if (dataSource.updateEmail(auth)) {
playerCache.updatePlayer(auth);

View File

@ -1,10 +1,13 @@
package fr.xephi.authme.process.email;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.EmailChangedEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
@ -32,6 +35,9 @@ public class AsyncChangeEmail implements AsynchronousProcess {
@Inject
private BungeeSender bungeeSender;
@Inject
private BukkitService bukkitService;
AsyncChangeEmail() { }
@ -57,14 +63,22 @@ public class AsyncChangeEmail implements AsynchronousProcess {
} else if (!validationService.isEmailFreeForRegistration(newEmail, player)) {
service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR);
} else {
saveNewEmail(auth, player, newEmail);
saveNewEmail(auth, player, oldEmail, newEmail);
}
} else {
outputUnloggedMessage(player);
}
}
private void saveNewEmail(PlayerAuth auth, Player player, String newEmail) {
private void saveNewEmail(PlayerAuth auth, Player player, String oldEmail, String newEmail) {
EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync
-> new EmailChangedEvent(player, oldEmail, newEmail, isAsync));
if (event.isCancelled()) {
ConsoleLogger.info("Could not change email for player '" + player + "' event was cancelled");
service.send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED);
return;
}
auth.setEmail(newEmail);
if (dataSource.updateEmail(auth)) {
playerCache.updatePlayer(auth);

View File

@ -18,6 +18,7 @@ import fr.xephi.authme.settings.commandconfig.CommandManager;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.GameMode;
import org.bukkit.Server;
@ -183,8 +184,7 @@ public class AsynchronousJoin implements AsynchronousProcess {
private boolean validatePlayerCountForIp(final Player player, String ip) {
if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0
&& !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)
&& !"127.0.0.1".equalsIgnoreCase(ip)
&& !"localhost".equalsIgnoreCase(ip)
&& !InternetProtocolUtils.isLoopbackAddress(ip)
&& countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(

View File

@ -6,6 +6,8 @@ import fr.xephi.authme.data.TempbanManager;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent;
@ -28,6 +30,7 @@ import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.ChatColor;
@ -90,7 +93,13 @@ public class AsynchronousLogin implements AsynchronousProcess {
public void login(Player player, String password) {
PlayerAuth auth = getPlayerAuth(player);
if (auth != null && checkPlayerInfo(player, auth, password)) {
performLogin(player, auth);
if (auth.getTotpKey() != null) {
limboService.resetMessageTask(player, LimboMessageType.TOTP_CODE);
limboService.getLimboPlayer(player.getName()).setState(LimboPlayerState.TOTP_REQUIRED);
// TODO #1141: Check if we should check limbo state before processing password
} else {
performLogin(player, auth);
}
}
}
@ -125,7 +134,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
if (auth == null) {
service.send(player, MessageKey.UNKNOWN_USER);
// Recreate the message task to immediately send the message again as response
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
return null;
}
@ -218,7 +227,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
* @param player the player to log in
* @param auth the associated PlayerAuth object
*/
private void performLogin(Player player, PlayerAuth auth) {
public void performLogin(Player player, PlayerAuth auth) {
if (player.isOnline()) {
final boolean isFirstLogin = (auth.getLastLogin() == null);
@ -317,8 +326,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
// Do not perform the check if player has multiple accounts permission or if IP is localhost
if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) <= 0
|| service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)
|| "127.0.0.1".equalsIgnoreCase(ip)
|| "localhost".equalsIgnoreCase(ip)) {
|| InternetProtocolUtils.isLoopbackAddress(ip)) {
return false;
}

View File

@ -4,16 +4,19 @@ import ch.jalu.injector.factory.SingletonStore;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AuthMeAsyncPreRegisterEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.process.register.executors.RegistrationExecutor;
import fr.xephi.authme.process.register.executors.RegistrationMethod;
import fr.xephi.authme.process.register.executors.RegistrationParameters;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
import fr.xephi.authme.service.bungeecord.MessageType;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
@ -32,6 +35,8 @@ public class AsyncRegister implements AsynchronousProcess {
@Inject
private PlayerCache playerCache;
@Inject
private BukkitService bukkitService;
@Inject
private CommonService service;
@Inject
private SingletonStore<RegistrationExecutor> registrationExecutorFactory;
@ -44,9 +49,9 @@ public class AsyncRegister implements AsynchronousProcess {
/**
* Performs the registration process for the given player.
*
* @param variant the registration method
* @param variant the registration method
* @param parameters the parameters
* @param <P> parameters type
* @param <P> parameters type
*/
public <P extends RegistrationParameters> void register(RegistrationMethod<P> variant, P parameters) {
if (preRegisterCheck(variant, parameters.getPlayer())) {
@ -57,6 +62,14 @@ public class AsyncRegister implements AsynchronousProcess {
}
}
/**
* Checks if the player is able to register, in that case the {@link AuthMeAsyncPreRegisterEvent} is invoked.
*
* @param variant the registration type variant.
* @param player the player which is trying to register.
*
* @return true if the checks are successful and the event hasn't marked the action as denied, false otherwise.
*/
private boolean preRegisterCheck(RegistrationMethod<?> variant, Player player) {
final String name = player.getName().toLowerCase();
if (playerCache.isAuthenticated(name)) {
@ -70,6 +83,12 @@ public class AsyncRegister implements AsynchronousProcess {
return false;
}
AuthMeAsyncPreRegisterEvent event = bukkitService.createAndCallEvent(
isAsync -> new AuthMeAsyncPreRegisterEvent(player, isAsync));
if (!event.canRegister()) {
return false;
}
return variant == RegistrationMethod.API_REGISTRATION || isPlayerIpAllowedToRegister(player);
}
@ -77,11 +96,11 @@ public class AsyncRegister implements AsynchronousProcess {
* Executes the registration.
*
* @param parameters the registration parameters
* @param executor the executor to perform the registration process with
* @param <P> registration params type
* @param executor the executor to perform the registration process with
* @param <P> registration params type
*/
private <P extends RegistrationParameters>
void executeRegistration(P parameters, RegistrationExecutor<P> executor) {
void executeRegistration(P parameters, RegistrationExecutor<P> executor) {
PlayerAuth auth = executor.buildPlayerAuth(parameters);
if (database.saveAuth(auth)) {
executor.executePostPersistAction(parameters);
@ -95,14 +114,14 @@ public class AsyncRegister implements AsynchronousProcess {
* Checks whether the registration threshold has been exceeded for the given player's IP address.
*
* @param player the player to check
*
* @return true if registration may take place, false otherwise (IP check failed)
*/
private boolean isPlayerIpAllowedToRegister(Player player) {
final int maxRegPerIp = service.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP);
final String ip = PlayerUtils.getPlayerIp(player);
if (maxRegPerIp > 0
&& !"127.0.0.1".equalsIgnoreCase(ip)
&& !"localhost".equalsIgnoreCase(ip)
&& !InternetProtocolUtils.isLoopbackAddress(ip)
&& !service.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) {
List<String> otherAccounts = database.getAllAuthsByIp(ip);
if (otherAccounts.size() >= maxRegPerIp) {

View File

@ -2,8 +2,10 @@ package fr.xephi.authme.process.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.events.RegisterEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.SynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
@ -15,6 +17,9 @@ import javax.inject.Inject;
*/
public class ProcessSyncEmailRegister implements SynchronousProcess {
@Inject
private BukkitService bukkitService;
@Inject
private CommonService service;
@ -34,6 +39,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess {
limboService.replaceTasksAfterRegistration(player);
player.saveData();
bukkitService.callEvent(new RegisterEvent(player));
ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player));
}

View File

@ -2,8 +2,10 @@ package fr.xephi.authme.process.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.events.RegisterEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.SynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
import fr.xephi.authme.settings.commandconfig.CommandManager;
@ -31,6 +33,9 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
@Inject
private CommandManager commandManager;
@Inject
private BukkitService bukkitService;
ProcessSyncPasswordRegister() {
}
@ -60,6 +65,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
}
player.saveData();
bukkitService.callEvent(new RegisterEvent(player));
ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player));
// Kick Player after Registration is enabled, kick the player

View File

@ -1,6 +1,7 @@
package fr.xephi.authme.security;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -78,6 +79,20 @@ public final class HashUtils {
return hash.length() > 3 && hash.substring(0, 2).equals("$2");
}
/**
* Checks whether the two strings are equal to each other in a time-constant manner.
* This helps to avoid timing side channel attacks,
* cf. <a href="https://github.com/AuthMe/AuthMeReloaded/issues/1561">issue #1561</a>.
*
* @param string1 first string
* @param string2 second string
* @return true if the strings are equal to each other, false otherwise
*/
public static boolean isEqual(String string1, String string2) {
return MessageDigest.isEqual(
string1.getBytes(StandardCharsets.UTF_8), string2.getBytes(StandardCharsets.UTF_8));
}
/**
* Hash the message with the given algorithm and return the hash in its hexadecimal notation.
*

View File

@ -8,7 +8,7 @@ import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import javax.inject.Inject;
@ -39,7 +39,7 @@ public class BCrypt implements EncryptionMethod {
try {
return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash());
} catch (IllegalArgumentException e) {
ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e));
ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e));
}
return false;
}

View File

@ -3,6 +3,8 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.RECOMMENDED)
public class BCrypt2y extends HexSaltedMethod {
@ -23,7 +25,7 @@ public class BCrypt2y extends HexSaltedMethod {
// The salt is the first 29 characters of the hash
String salt = hash.substring(0, 29);
return hash.equals(computeHash(password, salt, null));
return isEqual(hash, computeHash(password, salt, null));
}
@Override

View File

@ -2,12 +2,12 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import fr.xephi.authme.util.RandomStringUtils;
/**
@ -37,7 +37,7 @@ public class Ipb4 implements EncryptionMethod {
try {
return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash());
} catch (IllegalArgumentException e) {
ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e));
ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e));
}
return false;
}

View File

@ -4,6 +4,8 @@ import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.ACCEPTABLE)
public class Joomla extends HexSaltedMethod {
@ -16,7 +18,7 @@ public class Joomla extends HexSaltedMethod {
public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) {
String hash = hashedPassword.getHash();
String[] hashParts = hash.split(":");
return hashParts.length == 2 && hash.equals(computeHash(password, hashParts[1], null));
return hashParts.length == 2 && isEqual(hash, computeHash(password, hashParts[1], null));
}
@Override

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.security.crypts;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.HashUtils.md5;
public class Md5vB extends HexSaltedMethod {
@ -13,7 +14,7 @@ public class Md5vB extends HexSaltedMethod {
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String[] line = hash.split("\\$");
return line.length == 4 && hash.equals(computeHash(password, line[2], name));
return line.length == 4 && isEqual(hash, computeHash(password, line[2], name));
}
@Override

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.security.crypts;
import com.google.common.primitives.Ints;
import de.rtner.misc.BinTools;
import de.rtner.security.auth.spi.PBKDF2Engine;
import de.rtner.security.auth.spi.PBKDF2Parameters;
@ -38,13 +39,12 @@ public class Pbkdf2 extends HexSaltedMethod {
if (line.length != 4) {
return false;
}
int iterations;
try {
iterations = Integer.parseInt(line[1]);
} catch (NumberFormatException e) {
ConsoleLogger.logException("Cannot read number of rounds for Pbkdf2", e);
Integer iterations = Ints.tryParse(line[1]);
if (iterations == null) {
ConsoleLogger.warning("Cannot read number of rounds for Pbkdf2: '" + line[1] + "'");
return false;
}
String salt = line[2];
byte[] derivedKey = BinTools.hex2bin(line[3]);
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), iterations, derivedKey);

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.security.crypts;
import com.google.common.primitives.Ints;
import de.rtner.security.auth.spi.PBKDF2Engine;
import de.rtner.security.auth.spi.PBKDF2Parameters;
import fr.xephi.authme.ConsoleLogger;
@ -27,13 +28,12 @@ public class Pbkdf2Django extends HexSaltedMethod {
if (line.length != 4) {
return false;
}
int iterations;
try {
iterations = Integer.parseInt(line[1]);
} catch (NumberFormatException e) {
ConsoleLogger.logException("Could not read number of rounds for Pbkdf2Django:", e);
Integer iterations = Ints.tryParse(line[1]);
if (iterations == null) {
ConsoleLogger.warning("Cannot read number of rounds for Pbkdf2Django: '" + line[1] + "'");
return false;
}
String salt = line[2];
byte[] derivedKey = Base64.getDecoder().decode(line[3]);
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), iterations, derivedKey);

View File

@ -10,6 +10,8 @@ import fr.xephi.authme.security.crypts.description.Usage;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Encryption method compatible with phpBB3.
* <p>
@ -43,7 +45,7 @@ public class PhpBB implements EncryptionMethod {
} else if (hash.length() == 34) {
return PhpassSaltedMd5.phpbb_check_hash(password, hash);
} else {
return PhpassSaltedMd5.md5(password).equals(hash);
return isEqual(hash, PhpassSaltedMd5.md5(password));
}
}
@ -153,7 +155,7 @@ public class PhpBB implements EncryptionMethod {
}
private static boolean phpbb_check_hash(String password, String hash) {
return _hash_crypt_private(password, hash).equals(hash);
return isEqual(hash, _hash_crypt_private(password, hash)); // #1561: fix timing issue
}
}
}

View File

@ -1,5 +1,7 @@
package fr.xephi.authme.security.crypts;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common supertype for encryption methods which store their salt separately from the hash.
*/
@ -19,7 +21,7 @@ public abstract class SeparateSaltMethod implements EncryptionMethod {
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return hashedPassword.getHash().equals(computeHash(password, hashedPassword.getSalt(), null));
return isEqual(hashedPassword.getHash(), computeHash(password, hashedPassword.getSalt(), null));
}
@Override

View File

@ -3,6 +3,7 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.HashUtils.sha256;
@Recommendation(Usage.RECOMMENDED)
@ -14,10 +15,10 @@ public class Sha256 extends HexSaltedMethod {
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) {
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String[] line = hash.split("\\$");
return line.length == 4 && hash.equals(computeHash(password, line[2], ""));
return line.length == 4 && isEqual(hash, computeHash(password, line[2], name));
}
@Override

View File

@ -7,6 +7,8 @@ import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.RandomStringUtils;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Hashing algorithm for SMF forums.
* <p>
@ -32,7 +34,7 @@ public class Smf implements EncryptionMethod {
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return computeHash(password, null, name).equals(hashedPassword.getHash());
return isEqual(hashedPassword.getHash(), computeHash(password, null, name));
}
@Override

View File

@ -5,6 +5,8 @@ import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common type for encryption methods which do not use any salt whatsoever.
*/
@ -26,7 +28,7 @@ public abstract class UnsaltedMethod implements EncryptionMethod {
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return hashedPassword.getHash().equals(computeHash(password));
return isEqual(hashedPassword.getHash(), computeHash(password));
}
@Override

View File

@ -5,6 +5,8 @@ import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common supertype of encryption methods that use a player's username
* (or something based on it) as embedded salt.
@ -23,7 +25,7 @@ public abstract class UsernameSaltMethod implements EncryptionMethod {
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return hashedPassword.getHash().equals(computeHash(password, name).getHash());
return isEqual(hashedPassword.getHash(), computeHash(password, name).getHash());
}
@Override

View File

@ -3,6 +3,7 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.crypts.BCryptService.hashpw;
@Recommendation(Usage.RECOMMENDED)
@ -14,12 +15,12 @@ public class Wbb4 extends HexSaltedMethod {
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) {
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
if (hashedPassword.getHash().length() != 60) {
return false;
}
String salt = hashedPassword.getHash().substring(0, 29);
return computeHash(password, salt, null).equals(hashedPassword.getHash());
return isEqual(hashedPassword.getHash(), computeHash(password, salt, name));
}
@Override

View File

@ -12,6 +12,8 @@ import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = 9)
// Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally
@ -115,7 +117,7 @@ public class Wordpress extends UnsaltedMethod {
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String comparedHash = crypt(password, hash);
return comparedHash.equals(hash);
return isEqual(hash, comparedHash);
}
}

View File

@ -3,6 +3,8 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.RECOMMENDED)
public class XAuth extends HexSaltedMethod {
@ -23,14 +25,14 @@ public class XAuth extends HexSaltedMethod {
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) {
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length();
if (saltPos + 12 > hash.length()) {
return false;
}
String salt = hash.substring(saltPos, saltPos + 12);
return hash.equals(computeHash(password, salt, null));
return isEqual(hash, computeHash(password, salt, name));
}
@Override

View File

@ -2,7 +2,7 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -32,7 +32,7 @@ public class XfBCrypt implements EncryptionMethod {
try {
return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash());
} catch (IllegalArgumentException e) {
ConsoleLogger.warning("XfBCrypt checkpw() returned " + StringUtils.formatException(e));
ConsoleLogger.warning("XfBCrypt checkpw() returned " + ExceptionUtils.formatException(e));
}
return false;
}

View File

@ -0,0 +1,69 @@
package fr.xephi.authme.security.totp;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import fr.xephi.authme.util.expiring.ExpiringMap;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
/**
* Handles the generation of new TOTP secrets for players.
*/
public class GenerateTotpService implements HasCleanup {
private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5;
private final ExpiringMap<String, TotpGenerationResult> totpKeys;
@Inject
private TotpAuthenticator totpAuthenticator;
GenerateTotpService() {
this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES);
}
/**
* Generates a new TOTP key and returns the corresponding QR code.
*
* @param player the player to save the TOTP key for
* @return TOTP generation result
*/
public TotpGenerationResult generateTotpKey(Player player) {
TotpGenerationResult credentials = totpAuthenticator.generateTotpKey(player);
totpKeys.put(player.getName().toLowerCase(), credentials);
return credentials;
}
/**
* Returns the generated TOTP secret of a player, if available and not yet expired.
*
* @param player the player to retrieve the TOTP key for
* @return TOTP generation result
*/
public TotpGenerationResult getGeneratedTotpKey(Player player) {
return totpKeys.get(player.getName().toLowerCase());
}
public void removeGenerateTotpKey(Player player) {
totpKeys.remove(player.getName().toLowerCase());
}
/**
* Returns whether the given totp code is correct for the generated TOTP key of the player.
*
* @param player the player to verify the code for
* @param totpCode the totp code to verify with the generated secret
* @return true if the input code is correct, false if the code is invalid or no unexpired totp key is available
*/
public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) {
TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase());
return totpDetails != null && totpAuthenticator.checkCode(totpDetails.getTotpKey(), totpCode);
}
@Override
public void performCleanup() {
totpKeys.removeExpiredEntries();
}
}

View File

@ -0,0 +1,75 @@
package fr.xephi.authme.security.totp;
import com.google.common.primitives.Ints;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import com.warrenstrange.googleauth.IGoogleAuthenticator;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Provides TOTP functions (wrapping a third-party TOTP implementation).
*/
public class TotpAuthenticator {
private final IGoogleAuthenticator authenticator;
private final BukkitService bukkitService;
@Inject
TotpAuthenticator(BukkitService bukkitService) {
this.authenticator = createGoogleAuthenticator();
this.bukkitService = bukkitService;
}
/**
* @return new Google Authenticator instance
*/
protected IGoogleAuthenticator createGoogleAuthenticator() {
return new GoogleAuthenticator();
}
public boolean checkCode(PlayerAuth auth, String totpCode) {
return checkCode(auth.getTotpKey(), totpCode);
}
/**
* Returns whether the given input code matches for the provided TOTP key.
*
* @param totpKey the key to check with
* @param inputCode the input code to verify
* @return true if code is valid, false otherwise
*/
public boolean checkCode(String totpKey, String inputCode) {
Integer totpCode = Ints.tryParse(inputCode);
return totpCode != null && authenticator.authorize(totpKey, totpCode);
}
public TotpGenerationResult generateTotpKey(Player player) {
GoogleAuthenticatorKey credentials = authenticator.createCredentials();
String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(
bukkitService.getIp(), player.getName(), credentials);
return new TotpGenerationResult(credentials.getKey(), qrCodeUrl);
}
public static final class TotpGenerationResult {
private final String totpKey;
private final String authenticatorQrCodeUrl;
public TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) {
this.totpKey = totpKey;
this.authenticatorQrCodeUrl = authenticatorQrCodeUrl;
}
public String getTotpKey() {
return totpKey;
}
public String getAuthenticatorQrCodeUrl() {
return authenticatorQrCodeUrl;
}
}
}

View File

@ -393,4 +393,11 @@ public class BukkitService implements SettingsDependent {
return Optional.empty();
}
}
/**
* @return the IP string that this server is bound to, otherwise empty string
*/
public String getIp() {
return Bukkit.getServer().getIp();
}
}

View File

@ -21,8 +21,9 @@ import fr.xephi.authme.util.InternetProtocolUtils;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
@ -33,6 +34,9 @@ import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import java.util.zip.GZIPInputStream;
@ -55,6 +59,10 @@ public class GeoIpService {
private static final int UPDATE_INTERVAL_DAYS = 30;
// The server for MaxMind doesn't seem to understand RFC1123,
// but every HTTP implementation have to support RFC 1023
private static final String TIME_RFC_1023 = "EEE, dd-MMM-yy HH:mm:ss zzz";
private final Path dataFile;
private final BukkitService bukkitService;
@ -98,13 +106,12 @@ public class GeoIpService {
try {
FileTime lastModifiedTime = Files.getLastModifiedTime(dataFile);
if (Duration.between(lastModifiedTime.toInstant(), Instant.now()).toDays() <= UPDATE_INTERVAL_DAYS) {
databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache());
ConsoleLogger.info(LICENSE);
startReading();
// don't fire the update task - we are up to date
return true;
} else {
ConsoleLogger.debug("GEO Ip database is older than " + UPDATE_INTERVAL_DAYS + " Days");
ConsoleLogger.debug("GEO IP database is older than " + UPDATE_INTERVAL_DAYS + " Days");
}
} catch (IOException ioEx) {
ConsoleLogger.logException("Failed to load GeoLiteAPI database", ioEx);
@ -112,54 +119,101 @@ public class GeoIpService {
}
}
//set the downloading flag in order to fix race conditions outside
downloading = true;
// File is outdated or doesn't exist - let's try to download the data file!
startDownloadTask();
// use bukkit's cached threads
bukkitService.runTaskAsynchronously(this::updateDatabase);
return false;
}
/**
* Create a thread which will attempt to download new data from the GeoLite website.
* Tries to update the database by downloading a new version from the website.
*/
private void startDownloadTask() {
downloading = true;
private void updateDatabase() {
ConsoleLogger.info("Downloading GEO IP database, because the old database is older than "
+ UPDATE_INTERVAL_DAYS + " days or doesn't exist");
// use bukkit's cached threads
bukkitService.runTaskAsynchronously(() -> {
ConsoleLogger.info("Downloading GEO IP database, because the old database is outdated or doesn't exist");
Path tempFile = null;
try {
// download database to temporarily location
tempFile = Files.createTempFile(ARCHIVE_FILE, null);
try (OutputStream out = Files.newOutputStream(tempFile)) {
Resources.copy(new URL(ARCHIVE_URL), out);
}
// MD5 checksum verification
String targetChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8);
if (!verifyChecksum(Hashing.md5(), tempFile, targetChecksum)) {
return;
}
// tar extract database and copy to target destination
if (!extractDatabase(tempFile, dataFile)) {
ConsoleLogger.warning("Cannot find database inside downloaded GEO IP file at " + tempFile);
return;
}
ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile);
//only set this value to false on success otherwise errors could lead to endless download triggers
downloading = false;
} catch (IOException ioEx) {
ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx);
} finally {
// clean up
if (tempFile != null) {
FileUtils.delete(tempFile.toFile());
}
Path tempFile = null;
try {
// download database to temporarily location
tempFile = Files.createTempFile(ARCHIVE_FILE, null);
if (!downloadDatabaseArchive(tempFile)) {
ConsoleLogger.info("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now.");
startReading();
return;
}
});
// MD5 checksum verification
String expectedChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8);
verifyChecksum(Hashing.md5(), tempFile, expectedChecksum);
// tar extract database and copy to target destination
extractDatabase(tempFile, dataFile);
//only set this value to false on success otherwise errors could lead to endless download triggers
ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile);
startReading();
} catch (IOException ioEx) {
ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx);
} finally {
// clean up
if (tempFile != null) {
FileUtils.delete(tempFile.toFile());
}
}
}
private void startReading() throws IOException {
databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache());
ConsoleLogger.info(LICENSE);
// clear downloading flag, because we now have working reader instance
downloading = false;
}
/**
* Downloads the archive to the destination file if it's newer than the locally version.
*
* @param lastModified modification timestamp of the already present file
* @param destination save file
* @return false if we already have the newest version, true if successful
* @throws IOException if failed during downloading and writing to destination file
*/
private boolean downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(ARCHIVE_URL).openConnection();
if (lastModified != null) {
// Only download if we actually need a newer version - this field is specified in GMT zone
ZonedDateTime zonedTime = lastModified.atZone(ZoneId.of("GMT"));
String timeFormat = DateTimeFormatter.ofPattern(TIME_RFC_1023).format(zonedTime);
connection.addRequestProperty("If-Modified-Since", timeFormat);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
//we already have the newest version
connection.getInputStream().close();
return false;
}
Files.copy(connection.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
return true;
}
/**
* Downloads the archive to the destination file if it's newer than the locally version.
*
* @param destination save file
* @return false if we already have the newest version, true if successful
* @throws IOException if failed during downloading and writing to destination file
*/
private boolean downloadDatabaseArchive(Path destination) throws IOException {
Instant lastModified = null;
if (Files.exists(dataFile)) {
lastModified = Files.getLastModifiedTime(dataFile).toInstant();
}
return downloadDatabaseArchive(lastModified, destination);
}
/**
@ -168,19 +222,15 @@ public class GeoIpService {
* @param function the checksum function like MD5, SHA256 used to generate the checksum from the file
* @param file the file we want to calculate the checksum from
* @param expectedChecksum the expected checksum
* @return true if equal, false otherwise
* @throws IOException on I/O error reading the file
* @throws IOException on I/O error reading the file or the checksum verification failed
*/
private boolean verifyChecksum(HashFunction function, Path file, String expectedChecksum) throws IOException {
private void verifyChecksum(HashFunction function, Path file, String expectedChecksum) throws IOException {
HashCode actualHash = function.hashBytes(Files.readAllBytes(file));
HashCode expectedHash = HashCode.fromString(expectedChecksum);
if (Objects.equals(actualHash, expectedHash)) {
return true;
if (!Objects.equals(actualHash, expectedHash)) {
throw new IOException("GEO IP Checksum verification failed. " +
"Expected: " + expectedChecksum + "Actual:" + actualHash);
}
ConsoleLogger.warning("GEO IP checksum verification failed");
ConsoleLogger.warning("Expected: " + expectedHash + " Actual: " + actualHash);
return false;
}
/**
@ -188,38 +238,37 @@ public class GeoIpService {
*
* @param tarInputFile gzipped tar input file where the database is
* @param outputFile destination file for the database
* @return true if the database was found, false otherwise
* @throws IOException on I/O error reading the tar archive or writing the output
* @throws IOException on I/O error reading the tar archive, or writing the output
* @throws FileNotFoundException if the database cannot be found inside the archive
*/
private boolean extractDatabase(Path tarInputFile, Path outputFile) throws IOException {
private void extractDatabase(Path tarInputFile, Path outputFile) throws FileNotFoundException, IOException {
// .gz -> gzipped file
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(tarInputFile));
TarInputStream tarIn = new TarInputStream(new GZIPInputStream(in))) {
TarEntry entry;
while ((entry = tarIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
// filename including folders (absolute path inside the archive)
String filename = entry.getName();
if (filename.endsWith(DATABASE_EXT)) {
// found the database file
Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
// update the last modification date to be same as in the archive
Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant()));
return true;
}
for (TarEntry entry = tarIn.getNextEntry(); entry != null; entry = tarIn.getNextEntry()) {
// filename including folders (absolute path inside the archive)
String filename = entry.getName();
if (entry.isDirectory() || !filename.endsWith(DATABASE_EXT)) {
continue;
}
// found the database file and copy file
Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
// update the last modification date to be same as in the archive
Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant()));
return;
}
}
return false;
throw new FileNotFoundException("Cannot find database inside downloaded GEO IP file at " + tarInputFile);
}
/**
* Get the country code of the given IP address.
*
* @param ip textual IP address to lookup.
* @return two-character ISO 3166-1 alpha code for the country.
* @return two-character ISO 3166-1 alpha code for the country or "--" if it cannot be fetched.
*/
public String getCountryCode(String ip) {
return getCountry(ip).map(Country::getIsoCode).orElse("--");
@ -229,7 +278,7 @@ public class GeoIpService {
* Get the country name of the given IP address.
*
* @param ip textual IP address to lookup.
* @return The name of the country.
* @return The name of the country or "N/A" if it cannot be fetched.
*/
public String getCountryName(String ip) {
return getCountry(ip).map(Country::getName).orElse("N/A");
@ -255,7 +304,7 @@ public class GeoIpService {
try {
InetAddress address = InetAddress.getByName(ip);
//Reader.getCountry() can be null for unknown addresses
// Reader.getCountry() can be null for unknown addresses
return Optional.ofNullable(databaseReader.getCountry(address)).map(CountryResponse::getCountry);
} catch (UnknownHostException e) {
// Ignore invalid ip addresses

View File

@ -44,15 +44,17 @@ public class HelpTranslationGenerator {
/**
* Updates the help file to contain entries for all commands.
*
* @return the help file that has been updated
* @throws IOException if the help file cannot be written to
*/
public void updateHelpFile() throws IOException {
public File updateHelpFile() throws IOException {
String languageCode = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
File helpFile = new File(dataFolder, "messages/help_" + languageCode + ".yml");
Map<String, Object> helpEntries = generateHelpMessageEntries();
String helpEntriesYaml = exportToYaml(helpEntries);
Files.write(helpFile.toPath(), helpEntriesYaml.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
return helpFile;
}
private static String exportToYaml(Map<String, Object> helpEntries) {

View File

@ -121,6 +121,16 @@ public class PasswordRecoveryService implements Reloadable, HasCleanup {
commonService.send(player, MessageKey.RECOVERY_CHANGE_PASSWORD);
}
/**
* Removes a player from the list of successful recovers so that he can
* no longer use the /email setpassword command.
*
* @param player The player to remove.
*/
public void removeFromSuccessfulRecovery(Player player) {
successfulRecovers.remove(player.getName());
}
/**
* Check if a player is able to have emails sent.
*
@ -149,12 +159,7 @@ public class PasswordRecoveryService implements Reloadable, HasCleanup {
String playerAddress = PlayerUtils.getPlayerIp(player);
String storedAddress = successfulRecovers.get(name);
if (storedAddress == null || !playerAddress.equals(storedAddress)) {
messages.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED);
return false;
}
return true;
return storedAddress != null && playerAddress.equals(storedAddress);
}
@Override

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.service;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboPlayer;
@ -63,12 +64,13 @@ public class TeleportationService implements Reloadable {
public void teleportOnJoin(final Player player) {
if (!settings.getProperty(RestrictionSettings.NO_TELEPORT)
&& settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
ConsoleLogger.debug("Teleport on join for player `{0}`", player.getName());
teleportToSpawn(player, playerCache.isAuthenticated(player.getName()));
}
}
/**
* Returns the player's custom on join location
* Returns the player's custom on join location.
*
* @param player the player to process
*
@ -82,10 +84,11 @@ public class TeleportationService implements Reloadable {
SpawnTeleportEvent event = new SpawnTeleportEvent(player, location,
playerCache.isAuthenticated(player.getName()));
bukkitService.callEvent(event);
if(!isEventValid(event)) {
if (!isEventValid(event)) {
return null;
}
ConsoleLogger.debug("Returning custom location for >1.9 join event for player `{0}`", player.getName());
return location;
}
return null;
@ -107,6 +110,7 @@ public class TeleportationService implements Reloadable {
}
if (!player.hasPlayedBefore() || !dataSource.isAuthAvailable(player.getName())) {
ConsoleLogger.debug("Attempting to teleport player `{0}` to first spawn", player.getName());
performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn));
}
}
@ -130,12 +134,15 @@ public class TeleportationService implements Reloadable {
// The world in LimboPlayer is from where the player comes, before any teleportation by AuthMe
if (mustForceSpawnAfterLogin(worldName)) {
ConsoleLogger.debug("Teleporting `{0}` to spawn because of 'force-spawn after login'", player.getName());
teleportToSpawn(player, true);
} else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && auth.getQuitLocY() != 0) {
Location location = buildLocationFromAuth(player, auth);
ConsoleLogger.debug("Teleporting `{0}` after login, based on the player auth", player.getName());
teleportBackFromSpawn(player, location);
} else if (limbo != null && limbo.getLocation() != null) {
ConsoleLogger.debug("Teleporting `{0}` after login, based on the limbo player", player.getName());
teleportBackFromSpawn(player, limbo.getLocation());
}
}

View File

@ -10,6 +10,7 @@ import fr.xephi.authme.output.LogLevel;
import fr.xephi.authme.process.register.RegisterSecondaryArgument;
import fr.xephi.authme.process.register.RegistrationType;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
@ -74,6 +75,7 @@ public class SettingsMigrationService extends PlainMigrationService {
| convertToRegistrationType(resource)
| mergeAndMovePermissionGroupSettings(resource)
| moveDeprecatedHashAlgorithmIntoLegacySection(resource)
| moveSaltColumnConfigWithOtherColumnConfigs(resource)
|| hasDeprecatedProperties(resource);
}
@ -313,6 +315,18 @@ public class SettingsMigrationService extends PlainMigrationService {
return false;
}
/**
* Moves the property for the password salt column name to the same path as all other column name properties.
*
* @param resource The property resource
* @return True if the configuration has changed, false otherwise
*/
private static boolean moveSaltColumnConfigWithOtherColumnConfigs(PropertyResource resource) {
Property<String> oldProperty = newProperty("ExternalBoardOptions.mySQLColumnSalt",
DatabaseSettings.MYSQL_COL_SALT.getDefaultValue());
return moveProperty(oldProperty, DatabaseSettings.MYSQL_COL_SALT, resource);
}
/**
* Retrieves the old config to run a command when alt accounts are detected and sets them to this instance
* for further processing.

View File

@ -198,9 +198,11 @@ public class SpawnLoader implements Reloadable {
// ignore
}
if (spawnLoc != null) {
ConsoleLogger.debug("Spawn location determined as `{0}` for world `{1}`", spawnLoc, world.getName());
return spawnLoc;
}
}
ConsoleLogger.debug("Fall back to default world spawn location. World: `{0}`", world.getName());
return world.getSpawnLocation(); // return default location
}

View File

@ -65,7 +65,7 @@ public final class DatabaseSettings implements SettingsHolder {
@Comment("Column for storing players passwords salts")
public static final Property<String> MYSQL_COL_SALT =
newProperty("ExternalBoardOptions.mySQLColumnSalt", "");
newProperty("DataSource.mySQLColumnSalt", "");
@Comment("Column for storing players emails")
public static final Property<String> MYSQL_COL_EMAIL =
@ -79,6 +79,10 @@ public final class DatabaseSettings implements SettingsHolder {
public static final Property<String> MYSQL_COL_HASSESSION =
newProperty("DataSource.mySQLColumnHasSession", "hasSession");
@Comment("Column for storing a player's TOTP key (for two-factor authentication)")
public static final Property<String> MYSQL_COL_TOTP_KEY =
newProperty("DataSource.mySQLtotpKey", "totp");
@Comment("Column for storing the player's last IP")
public static final Property<String> MYSQL_COL_LAST_IP =
newProperty("DataSource.mySQLColumnIp", "ip");

View File

@ -26,7 +26,7 @@ public final class RestrictionSettings implements SettingsHolder {
@Comment("Allowed commands for unauthenticated players")
public static final Property<List<String>> ALLOW_COMMANDS =
newLowercaseListProperty("settings.restrictions.allowCommands",
"/login", "/register", "/l", "/reg", "/email", "/captcha");
"/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp");
@Comment({
"Max number of allowed registrations per IP",

View File

@ -49,7 +49,7 @@ public class PurgeExecutor {
* players and names.
*
* @param players the players to purge
* @param names names to purge
* @param names names to purge
*/
public void executePurge(Collection<OfflinePlayer> players, Collection<String> names) {
// Purge other data
@ -212,15 +212,13 @@ public class PurgeExecutor {
}
for (OfflinePlayer offlinePlayer : cleared) {
try {
permissionsManager.loadUserData(offlinePlayer.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(offlinePlayer.getName());
if (!permissionsManager.loadUserData(offlinePlayer)) {
ConsoleLogger.warning("Unable to purge the permissions of user " + offlinePlayer + "!");
continue;
}
permissionsManager.removeAllGroups(offlinePlayer);
}
ConsoleLogger.info("AutoPurge: Removed permissions from " + cleared.size() + " player(s).");
}
}

View File

@ -3,6 +3,7 @@ package fr.xephi.authme.task.purge;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.permission.handlers.PermissionLoadUserException;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
@ -73,10 +74,9 @@ class PurgeTask extends BukkitRunnable {
OfflinePlayer offlinePlayer = offlinePlayers[nextPosition];
if (offlinePlayer.getName() != null && toPurge.remove(offlinePlayer.getName().toLowerCase())) {
try {
permissionsManager.loadUserData(offlinePlayer.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(offlinePlayer.getName());
if(!permissionsManager.loadUserData(offlinePlayer)) {
ConsoleLogger.warning("Unable to check if the user " + offlinePlayer.getName() + " can be purged!");
continue;
}
if (!permissionsManager.hasPermissionOffline(offlinePlayer, PlayerStatePermission.BYPASS_PURGE)) {
playerPortion.add(offlinePlayer);

View File

@ -33,4 +33,14 @@ public final class ExceptionUtils {
}
return null;
}
/**
* Format the information from a Throwable as string, retaining the type and its message.
*
* @param th the throwable to process
* @return string with the type of the Throwable and its message, e.g. "[IOException]: Could not open stream"
*/
public static String formatException(Throwable th) {
return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage();
}
}

View File

@ -1,16 +1,13 @@
package fr.xephi.authme.util;
import java.util.regex.Pattern;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Utility class about the InternetProtocol
*/
public final class InternetProtocolUtils {
private static final Pattern LOCAL_ADDRESS_PATTERN =
Pattern.compile("(^127\\.)|(^(0)?10\\.)|(^172\\.(0)?1[6-9]\\.)|(^172\\.(0)?2[0-9]\\.)"
+ "|(^172\\.(0)?3[0-1]\\.)|(^169\\.254\\.)|(^192\\.168\\.)");
// Utility class
private InternetProtocolUtils() {
}
@ -19,10 +16,57 @@ public final class InternetProtocolUtils {
* Checks if the specified address is a private or loopback address
*
* @param address address to check
*
* @return true if the address is a local or loopback address, false otherwise
* @return true if the address is a local (site and link) or loopback address, false otherwise
*/
public static boolean isLocalAddress(String address) {
return LOCAL_ADDRESS_PATTERN.matcher(address).find();
try {
InetAddress inetAddress = InetAddress.getByName(address);
// Examples: 127.0.0.1, localhost or [::1]
return isLoopbackAddress(address)
// Example: 10.0.0.0, 172.16.0.0, 192.168.0.0, fec0::/10 (deprecated)
// Ref: https://en.wikipedia.org/wiki/IP_address#Private_addresses
|| inetAddress.isSiteLocalAddress()
// Example: 169.254.0.0/16, fe80::/10
// Ref: https://en.wikipedia.org/wiki/IP_address#Address_autoconfiguration
|| inetAddress.isLinkLocalAddress()
// non deprecated unique site-local that java doesn't check yet -> fc00::/7
|| isIPv6UniqueSiteLocal(inetAddress);
} catch (UnknownHostException e) {
return false;
}
}
/**
* Checks if the specified address is a loopback address. This can be one of the following:
* <ul>
* <li>127.0.0.1</li>
* <li>localhost</li>
* <li>[::1]</li>
* </ul>
*
* @param address address to check
* @return true if the address is a loopback one
*/
public static boolean isLoopbackAddress(String address) {
try {
InetAddress inetAddress = InetAddress.getByName(address);
return inetAddress.isLoopbackAddress();
} catch (UnknownHostException e) {
return false;
}
}
private static boolean isLoopbackAddress(InetAddress address) {
return address.isLoopbackAddress();
}
private static boolean isIPv6UniqueSiteLocal(InetAddress address) {
// ref: https://en.wikipedia.org/wiki/Unique_local_address
// currently undefined but could be used in the near future fc00::/8
return (address.getAddress()[0] & 0xFF) == 0xFC
// in use for unique site-local fd00::/8
|| (address.getAddress()[0] & 0xFF) == 0xFD;
}
}

Some files were not shown because too many files have changed in this diff Show More