diff --git a/.circleci/circle.yml b/.circleci/circle.yml deleted file mode 100644 index cb1e2ba61..000000000 --- a/.circleci/circle.yml +++ /dev/null @@ -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 diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..c988f79c4 --- /dev/null +++ b/.circleci/config.yml @@ -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 diff --git a/README.md b/README.md index 21910e4ef..5b91291c6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/commands.md b/docs/commands.md index 5bb49c2b9..163399c0e 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,5 @@ - + ## 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** <oldPassword> <newPassword>: Command to change your password using AuthMeReloaded.
Requires `authme.player.changepassword` - **/changepassword help** [query]: View detailed help for /changepassword commands. +- **/totp**: Performs actions related to two-factor authentication. +- **/totp code** <code>: Processes the two-factor authentication code during login. +- **/totp add**: Enables two-factor authentication for your account. +
Requires `authme.player.totpadd` +- **/totp confirm** <code>: Saves the generated TOTP secret after confirmation. +
Requires `authme.player.totpadd` +- **/totp remove** <code>: Disables two-factor authentication for your account. +
Requires `authme.player.totpremove` +- **/totp help** [query]: View detailed help for /totp commands. - **/captcha** <captcha>: Captcha command for AuthMeReloaded.
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 diff --git a/docs/config.md b/docs/config.md index 956da56a2..9444bb83e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## 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 diff --git a/docs/permission_nodes.md b/docs/permission_nodes.md index f3b5edac4..643a327b0 100644 --- a/docs/permission_nodes.md +++ b/docs/permission_nodes.md @@ -1,5 +1,5 @@ - + ## 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 diff --git a/docs/translations.md b/docs/translations.md index 2f778c291..c8ace37b1 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -1,5 +1,5 @@ - + # 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 |   ---- | -------- | ---------: | ------ [en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | bar -[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 86% | bar -[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 90% | bar -[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 90% | bar -[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 90% | bar -[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 90% | bar -[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 100% | bar -[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 90% | bar -[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 48% | bar -[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 51% | bar -[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | bar -[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 54% | bar -[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 98% | bar -[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 53% | bar +[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 76% | bar +[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 80% | bar +[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 80% | bar +[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 80% | bar +[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 80% | bar +[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 92% | bar +[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 80% | bar +[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 42% | bar +[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 45% | bar +[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 89% | bar +[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 48% | bar +[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 87% | bar +[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 47% | bar [it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | bar -[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 98% | bar -[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 40% | bar -[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 90% | bar +[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 89% | bar +[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 36% | bar +[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 80% | bar [pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | bar -[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 90% | bar -[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 90% | bar -[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 98% | bar -[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 90% | bar -[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 86% | bar -[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 71% | bar -[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 87% | bar -[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 100% | bar -[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 90% | bar -[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 73% | bar -[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 98% | bar +[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 100% | bar +[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 80% | bar +[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 92% | bar +[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 80% | bar +[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 76% | bar +[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 63% | bar +[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 77% | bar +[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 89% | bar +[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 80% | bar +[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 65% | bar +[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 87% | 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 diff --git a/pom.xml b/pom.xml index a5c49a3c4..d2213d370 100644 --- a/pom.xml +++ b/pom.xml @@ -73,9 +73,13 @@ Xephi, sgdc3, DNx5, timvisee, games647, ljacqu, Gnat008 - 1.12.2-R0.1-SNAPSHOT + 1.13-pre7-R0.1-SNAPSHOT + + 3.3.9 + + @@ -131,12 +135,12 @@ org.apache.maven.plugins maven-clean-plugin - 3.0.0 + 3.1.0 org.apache.maven.plugins maven-resources-plugin - 3.0.2 + 3.1.0 org.apache.maven.plugins @@ -150,7 +154,7 @@ org.jacoco jacoco-maven-plugin - 0.8.0 + 0.8.1 pre-unit-test @@ -169,10 +173,11 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20.1 + 2.21.0 - -Dfile.encoding=${project.build.sourceEncoding} @{argLine} + + -Dfile.encoding=${project.build.sourceEncoding} -Duser.language=en @{argLine} ${project.skipExtendedHashTests} @@ -181,12 +186,12 @@ org.apache.maven.plugins maven-jar-plugin - 3.0.2 + 3.1.0 org.apache.maven.plugins maven-javadoc-plugin - 3.0.0 + 3.0.1 attach-javadoc @@ -226,7 +231,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.1.0 + 3.1.1 package @@ -251,12 +256,8 @@ fr.xephi.authme.libs.com.google - ch.jalu.injector - fr.xephi.authme.libs.jalu.injector - - - ch.jalu.configme - fr.xephi.authme.libs.ch.jalu.configme + ch.jalu + fr.xephi.authme.libs.ch.jalu com.zaxxer.hikari @@ -286,6 +287,10 @@ de.mkammerer fr.xephi.authme.libs.de.mkammerer + + com.warrenstrange + fr.xephi.authme.libs.com.warrenstrange + javax.inject fr.xephi.authme.libs.javax.inject @@ -387,7 +392,7 @@ com.google.code.gson gson - 2.8.2 + 2.8.5 true @@ -395,7 +400,7 @@ com.google.guava guava - 24.1-jre + 25.1-jre true @@ -436,7 +441,7 @@ com.zaxxer HikariCP - 2.7.8 + 3.2.0 true @@ -469,6 +474,14 @@ true + + + com.warrenstrange + googleauth + 1.1.5 + true + + org.spigotmc @@ -545,7 +558,7 @@ me.lucko.luckperms luckperms-api - 4.1 + 4.2 provided @@ -788,6 +801,13 @@ + + ch.jalu + datasourcecolumns + 0.1.1-SNAPSHOT + true + + @@ -808,7 +828,7 @@ org.mockito mockito-core test - 2.16.0 + 2.19.0 hamcrest-core @@ -821,13 +841,13 @@ org.xerial sqlite-jdbc - 3.21.0.1 + 3.23.1 test com.h2database h2 - 1.4.196 + 1.4.197 test diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 4aeb536c1..6a2b3583c 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index 0d9332985..3e8d59a4a 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -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)); } diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index dc5f740ff..ba48e0112 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -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 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 baseCommands = ImmutableList.of( - authMeBase, - emailBase, - loginBase, - logoutBase, - registerBase, - unregisterBase, - changePasswordBase, - captchaBase, - verificationBase); + List 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(); } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java index a7f327a10..b66914387 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java @@ -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 arguments) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); - DataSourceResult email = dataSource.getEmail(playerName); - if (email.playerExists()) { + DataSourceValue email = dataSource.getEmail(playerName); + if (email.rowExists()) { sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue()); } else { commonService.send(sender, MessageKey.UNKNOWN_USER); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index 58b235ed0..1538061e9 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java index c737b98dd..d790962a5 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java @@ -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 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()); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java index 07e160a40..ae314e098 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java @@ -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) + "'"); } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java index f3580d326..02bd4a213 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java @@ -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 arguments) { if (arguments.isEmpty()) { - DataSourceResult emailResult = dataSource.getEmail(sender.getName()); - if (!emailResult.playerExists()) { + DataSourceValue 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; diff --git a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java index b0da817f6..c486dd042 100644 --- a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java @@ -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); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java similarity index 89% rename from src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java rename to src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java index d5d084aa6..376e8db29 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java @@ -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); } } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index 339980a34..0a3a96945 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -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 emailResult = dataSource.getEmail(playerName); - if (!emailResult.playerExists()) { + DataSourceValue emailResult = dataSource.getEmail(playerName); + if (!emailResult.rowExists()) { commonService.send(player, MessageKey.USAGE_REGISTER); return; } diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index 32a4d87da..57ae86393 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -18,7 +18,7 @@ public class LoginCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments) { - final String password = arguments.get(0); + String password = arguments.get(0); management.performLogin(player, password); } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java new file mode 100644 index 000000000..e8a78bd1d --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -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 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java new file mode 100644 index 000000000..1ab8192be --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -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 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java new file mode 100644 index 000000000..ebcf554c2 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -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 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java new file mode 100644 index 000000000..2b170a030 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java @@ -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 arguments) { + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java new file mode 100644 index 000000000..398759028 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -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 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java index a6ba75c6e..1cd176684 100644 --- a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java +++ b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java @@ -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 emailResult = dataSource.getEmail(name); - if (emailResult.playerExists()) { + DataSourceValue 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 emailResult = dataSource.getEmail(name); - if (emailResult.playerExists()) { + DataSourceValue 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()); } diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 4c8b8ee30..53d74dfba 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -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; diff --git a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java index 523885213..753650b67 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java +++ b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java @@ -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) { diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java b/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java new file mode 100644 index 000000000..4d0af3e5a --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java @@ -0,0 +1,11 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboMessageType { + + REGISTER, + + LOG_IN, + + TOTP_CODE + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java index b7ea415c8..03c3ea138 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java @@ -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 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; + } } diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java new file mode 100644 index 000000000..5940ab206 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java @@ -0,0 +1,9 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboPlayerState { + + PASSWORD_REQUIRED, + + TOTP_REQUIRED + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java index e5f4f65d0..3612e6797 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java index 982f359a8..fae9d44ed 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java @@ -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 = 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)); } /** diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java index b13a260d9..4d63a2d98 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java @@ -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; diff --git a/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java b/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java new file mode 100644 index 000000000..0c40c479d --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java @@ -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 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 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 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 getEmail(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); + } catch (SQLException e) { + logSqlException(e); + return DataSourceValueImpl.unknownRow(); + } + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 39f04a53c..165560b1e 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -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 getEmail(String user) { + public DataSourceValue 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); diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index 946c33de6..0d372a239 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 6f97951db..6ce28852f 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -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 getEmail(String user); + DataSourceValue getEmail(String user); /** * Return all players of the database. @@ -232,6 +233,25 @@ public interface DataSource extends Reloadable { */ List 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. */ diff --git a/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java b/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java deleted file mode 100644 index c005874e2..000000000 --- a/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java +++ /dev/null @@ -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 { - - /** 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 the value's type - * @return DataSourceResult object for the given value - */ - public static DataSourceResult of(T value) { - return new DataSourceResult<>(value); - } - - /** - * Returns a {@link DataSourceResult} specifying that the player does not exist. - * - * @param the value type - * @return data source result for unknown player - */ - public static DataSourceResult 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; - } -} diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index d234da556..ab9bf85c1 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -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 getEmail(String user) { + public DataSourceValue 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. * diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index ce46baadd..3bb187ff1 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -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 getRecordsToPurge(long until) { Set 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 getAllAuthsByIp(String ip) { - List 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 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 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 getAllAuths() { List 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)) diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index ada94afbc..ff72a8018 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -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 getRecordsToPurge(long until) { Set 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 getAllAuthsByIp(String ip) { - List 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 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 getAllAuths() { List 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 { diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java new file mode 100644 index 000000000..5c235095f --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -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 NAME = createString( + DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname); + + public static final PlayerAuthColumn NICK_NAME = createString( + DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName); + + public static final PlayerAuthColumn PASSWORD = createString( + DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash()); + + public static final PlayerAuthColumn SALT = createString( + DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL); + + public static final PlayerAuthColumn EMAIL = createString( + DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL); + + public static final PlayerAuthColumn LAST_IP = createString( + DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp); + + public static final PlayerAuthColumn GROUP_ID = createInteger( + DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL); + + public static final PlayerAuthColumn LAST_LOGIN = createLong( + DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin); + + public static final PlayerAuthColumn REGISTRATION_IP = createString( + DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp); + + public static final PlayerAuthColumn REGISTRATION_DATE = createLong( + DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate); + + // -------- + // Location columns + // -------- + public static final PlayerAuthColumn LOCATION_X = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX); + + public static final PlayerAuthColumn LOCATION_Y = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY); + + public static final PlayerAuthColumn LOCATION_Z = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ); + + public static final PlayerAuthColumn LOCATION_WORLD = createString( + DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld); + + public static final PlayerAuthColumn LOCATION_YAW = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw); + + public static final PlayerAuthColumn LOCATION_PITCH = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch); + + // -------- + // Columns not on PlayerAuth + // -------- + public static final DataSourceColumn IS_LOGGED = createInteger( + DatabaseSettings.MYSQL_COL_ISLOGGED); + + public static final DataSourceColumn HAS_SESSION = createInteger( + DatabaseSettings.MYSQL_COL_HASSESSION); + + + private AuthMeColumns() { + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java new file mode 100644 index 000000000..3400f76cd --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java @@ -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 createInteger(Property nameProperty, + ColumnOptions... options) { + return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty, + isOptional(options), hasDefaultForNull(options)); + } + + static PlayerAuthColumn createInteger(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createLong(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createString(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createDouble(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createFloat(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options); + } + + private static PlayerAuthColumn createInternal(ColumnType type, Property nameProperty, + Function 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 + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java new file mode 100644 index 000000000..50575b4e5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -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 internalHandler; + + private AuthMeColumnsHandler(SqlColumnsHandler 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 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 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 the column type + * @return true upon success, false otherwise + */ + public boolean update(String name, DataSourceColumn 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 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 the column type + * @return number of modified rows + */ + public int update(Predicate predicate, DataSourceColumn 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 the column type + * @return the result of the lookup + * @throws SQLException . + */ + public DataSourceValue retrieve(String name, DataSourceColumn 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 the column's value type + * @return the values of the matching rows + * @throws SQLException . + */ + public List retrieve(Predicate predicate, DataSourceColumn 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 predicate) { + try { + return internalHandler.count(predicate); + } catch (SQLException e) { + logSqlException(e); + return 0; + } + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java new file mode 100644 index 000000000..554266a05 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java @@ -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, 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; + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java new file mode 100644 index 000000000..4b7fa4ca7 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java @@ -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 column type + */ +public class DataSourceColumn implements Column { + + private final ColumnType columnType; + private final Property 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 type, Property nameProperty, boolean isOptional, boolean useDefaultForNull) { + this.columnType = type; + this.nameProperty = nameProperty; + this.isOptional = isOptional; + this.useDefaultForNull = useDefaultForNull; + } + + public Property getNameProperty() { + return nameProperty; + } + + @Override + public String resolveName(ColumnContext columnContext) { + return columnContext.getName(this); + } + + @Override + public ColumnType getType() { + return columnType; + } + + @Override + public boolean isColumnUsed(ColumnContext columnContext) { + return !isOptional || !resolveName(columnContext).isEmpty(); + } + + @Override + public boolean useDefaultForNullValue(ColumnContext columnContext) { + return useDefaultForNull && columnContext.hasDefaultSupport(); + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java new file mode 100644 index 000000000..43d022b13 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java @@ -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 column type + */ +public class PlayerAuthColumn extends DataSourceColumn implements DependentColumn { + + private final Function playerAuthGetter; + + /* + * Constructor. See parent class for details. + */ + PlayerAuthColumn(ColumnType type, Property nameProperty, boolean isOptional, boolean useDefaultForNull, + Function playerAuthGetter) { + super(type, nameProperty, isOptional, useDefaultForNull); + this.playerAuthGetter = playerAuthGetter; + } + + @Override + public T getValueFromDependent(PlayerAuth auth) { + return playerAuthGetter.apply(auth); + } +} diff --git a/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java new file mode 100644 index 000000000..af26ad518 --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java @@ -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; + } + +} diff --git a/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java b/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java new file mode 100644 index 000000000..7d9468cab --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/xephi/authme/events/RegisterEvent.java b/src/main/java/fr/xephi/authme/events/RegisterEvent.java new file mode 100644 index 000000000..2a98d0544 --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/RegisterEvent.java @@ -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; + } + +} diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 7857114e3..0fbc1f902 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -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 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); } } diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 355f14a9a..6171f80aa 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -125,7 +125,7 @@ public enum MessageKey { /** Forgot your password? Please use the command: /email recovery <yourEmail> */ 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 <code> */ + TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED("two_factor.confirmation_required"), + + /** Please submit your two-factor authentication code with /2fa code <code> */ + 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 <code> */ USAGE_VERIFICATION_CODE("verification.command_usage"), - /** Incorrect code, please type "/verification <code>" into the chat! */ + /** Incorrect code, please type "/verification <code>" 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! */ diff --git a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java index 3b429106c..433f0b17b 100644 --- a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java +++ b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -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 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 addedKeys = new HashSet<>(); diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 55845f745..ec1fff9bb 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -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 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 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); diff --git a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java index f95fd09ba..d7427f46a 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -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. diff --git a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java index 2a0517225..667b55d56 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java @@ -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; diff --git a/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java index f01f14c34..0d465ef3c 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java @@ -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); } } - } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java index fe3f54057..831bc583f 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -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 { } - } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java new file mode 100644 index 000000000..697b49182 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java @@ -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); + } +} diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java index 016d6169d..1896bfd3b 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java index 8edd94961..26a5da9e7 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 7fb13bac6..9924eef8e 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -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( diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 193332501..2441ea283 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 5520d50cc..7f52befe2 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -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 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

parameters type + * @param

parameters type */ public

void register(RegistrationMethod

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

registration params type + * @param executor the executor to perform the registration process with + * @param

registration params type */ private

- void executeRegistration(P parameters, RegistrationExecutor

executor) { + void executeRegistration(P parameters, RegistrationExecutor

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 otherAccounts = database.getAllAuthsByIp(ip); if (otherAccounts.size() >= maxRegPerIp) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 6ab4a874a..e740a0ded 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -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)); } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 30e7f59ae..dc8aa136f 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/HashUtils.java b/src/main/java/fr/xephi/authme/security/HashUtils.java index 3578c80f3..642081c6d 100644 --- a/src/main/java/fr/xephi/authme/security/HashUtils.java +++ b/src/main/java/fr/xephi/authme/security/HashUtils.java @@ -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. issue #1561. + * + * @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. * diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java index 02e12d459..8b454c799 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java index cf4807abc..a22a68906 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java index 762897955..c7bfcd65b 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/security/crypts/Joomla.java b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java index 462f5cb28..2ecc1d8d3 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Joomla.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java index c244ec49d..00656964f 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java index 5367a2a12..d9695abc5 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java index f5a0abb63..e32930db1 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java index 70ac322d0..2d641706c 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java @@ -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. *

@@ -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 } } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java index d0dacda4d..c0ec13dd7 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Sha256.java b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java index 1b77a2e44..ce6b25492 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Sha256.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Smf.java b/src/main/java/fr/xephi/authme/security/crypts/Smf.java index 24d28fe6c..e24c1b83d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Smf.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Smf.java @@ -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. *

@@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java b/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java index a8f2040e5..33815ec77 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java index 23101e22a..f5930fcf5 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java index d1d4953d1..f396c5d84 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java index 768b92c5d..f70c09496 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java @@ -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); } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/XAuth.java b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java index 9f921b6ae..62f2e0d71 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XAuth.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java index 3ef4e4301..846807e6c 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java b/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java new file mode 100644 index 000000000..14a6a6bbb --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java @@ -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 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(); + } +} diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java new file mode 100644 index 000000000..4905a521e --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -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; + } + } +} diff --git a/src/main/java/fr/xephi/authme/service/BukkitService.java b/src/main/java/fr/xephi/authme/service/BukkitService.java index 09b4869fb..75fae29c0 100644 --- a/src/main/java/fr/xephi/authme/service/BukkitService.java +++ b/src/main/java/fr/xephi/authme/service/BukkitService.java @@ -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(); + } } diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java index b973b6337..be2746878 100644 --- a/src/main/java/fr/xephi/authme/service/GeoIpService.java +++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java b/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java index 6ecd05490..21407b4f0 100644 --- a/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java +++ b/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java @@ -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 helpEntries = generateHelpMessageEntries(); String helpEntriesYaml = exportToYaml(helpEntries); Files.write(helpFile.toPath(), helpEntriesYaml.getBytes(), StandardOpenOption.TRUNCATE_EXISTING); + return helpFile; } private static String exportToYaml(Map helpEntries) { diff --git a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java index f24b45f35..200aa11c7 100644 --- a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java +++ b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/service/TeleportationService.java b/src/main/java/fr/xephi/authme/service/TeleportationService.java index 10f9e1178..1588c4404 100644 --- a/src/main/java/fr/xephi/authme/service/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/service/TeleportationService.java @@ -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()); } } diff --git a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java index c5ab3fd7a..d95f73ce5 100644 --- a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java +++ b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java @@ -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 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. diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index ea235b3cc..d2f2edbf7 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -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 } diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 66ddd3cd5..0818c2693 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -65,7 +65,7 @@ public final class DatabaseSettings implements SettingsHolder { @Comment("Column for storing players passwords salts") public static final Property MYSQL_COL_SALT = - newProperty("ExternalBoardOptions.mySQLColumnSalt", ""); + newProperty("DataSource.mySQLColumnSalt", ""); @Comment("Column for storing players emails") public static final Property MYSQL_COL_EMAIL = @@ -79,6 +79,10 @@ public final class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_HASSESSION = newProperty("DataSource.mySQLColumnHasSession", "hasSession"); + @Comment("Column for storing a player's TOTP key (for two-factor authentication)") + public static final Property MYSQL_COL_TOTP_KEY = + newProperty("DataSource.mySQLtotpKey", "totp"); + @Comment("Column for storing the player's last IP") public static final Property MYSQL_COL_LAST_IP = newProperty("DataSource.mySQLColumnIp", "ip"); diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 492af4e6d..e769dd9b0 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -26,7 +26,7 @@ public final class RestrictionSettings implements SettingsHolder { @Comment("Allowed commands for unauthenticated players") public static final Property> 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", diff --git a/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java b/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java index a3e42f756..36c951ffc 100644 --- a/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java +++ b/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java @@ -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 players, Collection 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)."); } - } diff --git a/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java b/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java index 27b424150..686bab86d 100644 --- a/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java +++ b/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/util/ExceptionUtils.java b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java index 6a5adde69..fd5ae8852 100644 --- a/src/main/java/fr/xephi/authme/util/ExceptionUtils.java +++ b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java @@ -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(); + } } diff --git a/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java b/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java index 548e1e913..039421548 100644 --- a/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java +++ b/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java @@ -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: + *

    + *
  • 127.0.0.1
  • + *
  • localhost
  • + *
  • [::1]
  • + *
+ * + * @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; } } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index 1f200c0f0..5c8613005 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -66,17 +66,6 @@ public final class StringUtils { return str == null || str.trim().isEmpty(); } - /** - * 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(); - } - /** * Check that the given needle is in the middle of the haystack, i.e. that the haystack * contains the needle and that it is not at the very start or end. diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 8b073bd96..4687a8b99 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -60,7 +60,6 @@ misc: logout: '&2Излязохте успешно!' reload: '&2Конфигурацията и база данните бяха презаредени правилно!' usage_change_password: '&cКоманда: /changepassword Стара-Парола Нова-Парола' - two_factor_create: '&2Кода е %code. Можеш да го провериш оттука: %url' accounts_owned_self: 'Претежаваш %count акаунт/а:' accounts_owned_other: 'Потребителят %name има %count акаунт/а:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cСтарият имейл е грешен, опитайте отново!' invalid: '&cИмейла е невалиден, опитайте с друг!' added: '&2Имейл адреса е добавен!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cМоля потвърди своя имейл адрес!' changed: '&2Имейл адреса е сменен!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Твоят имейл адрес е: &f%email' no_email_for_account: '&2Няма добавен имейл адрес към акаунта.' already_used: '&4Имейл адреса вече се използва, опитайте с друг.' @@ -140,3 +141,16 @@ time: hours: 'часа' day: 'ден' days: 'дена' + +# Two-factor authentication +two_factor: + code_created: '&2Кода е %code. Можеш да го провериш оттука: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index 69f5e3954..40c58fc8f 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -63,7 +63,6 @@ misc: logout: '&2Desconectado com sucesso!' reload: '&2Configuração e o banco de dados foram recarregados corretamente!' usage_change_password: '&cUse: /changepassword ' - two_factor_create: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' accounts_owned_self: 'Você tem %count contas:' accounts_owned_other: 'O jogador %name tem %count contas:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&cE-mail velho inválido, tente novamente!' invalid: '&E-mail inválido, tente novamente!' added: '&2Email adicionado com sucesso à sua conta!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cPor favor confirme seu endereço de email!' changed: '&2Troca de email com sucesso.!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2O seu endereço de e-mail atual é: &f%email' no_email_for_account: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' already_used: '&4O endereço de e-mail já está sendo usado' @@ -143,3 +144,16 @@ time: hours: 'horas' day: 'dia' days: 'dias' + +# Two-factor authentication +two_factor: + code_created: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 5ed2ceb41..025b39f7a 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -60,7 +60,6 @@ misc: logout: '&cÚspěšně jsi se odhlásil.' reload: '&cZnovu načtení nastavení AuthMe proběhlo úspěšně.' usage_change_password: '&cPoužij: "/changepassword StaréHeslo NovéHeslo".' - two_factor_create: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' accounts_owned_self: 'Vlastníš tyto účty (%count):' accounts_owned_other: 'Hráč %name vlastní tyto účty (%count):' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Starý email je chybně zadán!' invalid: '[AuthMe] Nesprávný email' added: '[AuthMe] Email přidán!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Potvrď prosím svůj email!' changed: '[AuthMe] Email změněn!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Váš aktuální email je: &f%email' no_email_for_account: '&2K tomuto účtu nemáte přidanou žádnou emailovou adresu.' already_used: '&4Tato emailová adresa je již používána' @@ -140,3 +141,16 @@ time: hours: 'hodin' day: 'dny' days: 'dnu' + +# Two-factor authentication +two_factor: + code_created: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 84679c486..006626234 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -60,7 +60,6 @@ misc: logout: '&2Erfolgreich ausgeloggt' reload: '&2Konfiguration und Datenbank wurden erfolgreich neu geladen.' usage_change_password: '&cBenutze: /changepassword ' - two_factor_create: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' accounts_owned_self: 'Du besitzt %count Accounts:' accounts_owned_other: 'Der Spieler %name hat %count Accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cDie alte E-Mail ist ungültig!' invalid: '&cUngültige E-Mail!' added: '&2E-Mail hinzugefügt!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cBitte bestätige deine E-Mail!' changed: '&2E-Mail aktualisiert!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Deine aktuelle E-Mail-Adresse ist: &f%email' no_email_for_account: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' @@ -140,3 +141,16 @@ time: hours: 'Stunden' day: 'Tag' days: 'Tage' + +# Two-factor authentication +two_factor: + code_created: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 6d9d28798..917d57cd1 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -56,7 +56,6 @@ unregister: misc: accounts_owned_self: 'You own %count accounts:' accounts_owned_other: 'The player %name has %count accounts:' - two_factor_create: '&2Your secret code is %code. You can scan it from here %url' account_not_activated: '&cYour account isn''t activated yet, please check your emails!' password_changed: '&2Password changed successfully!' logout: '&2Logged out successfully!' @@ -98,6 +97,8 @@ email: add_email_request: '&3Please add your email to your account with the command: /email add ' change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + add_not_allowed: '&cAdding email was not allowed' + change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -129,6 +130,18 @@ verification: code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' email_needed: '&3To verify your identity you need to link an email address with your account!!' +two_factor: + code_created: '&2Your secret code is %code. You can scan it from here %url' + confirmation_required: 'Please confirm your code with /2fa confirm ' + code_required: 'Please submit your two-factor authentication code with /2fa code ' + already_enabled: 'Two-factor authentication is already enabled for your account!' + enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + enable_success: 'Successfully enabled two-factor authentication for your account' + enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + removed_success: 'Successfully removed two-factor auth from your account' + invalid_code: 'Invalid code!' + # Time units time: second: 'second' diff --git a/src/main/resources/messages/messages_eo.yml b/src/main/resources/messages/messages_eo.yml index c5783c831..5636ab729 100644 --- a/src/main/resources/messages/messages_eo.yml +++ b/src/main/resources/messages/messages_eo.yml @@ -60,7 +60,6 @@ misc: logout: '&2Elsalutita sukcese!' reload: '&2Agordo kaj datumbazo estis larditaj korekte!' usage_change_password: '&cUzado: /changepassword ' - two_factor_create: '&2Via sekreta kodo estas %code. Vi povas skani ĝin de tie %url' accounts_owned_self: 'Vi posedas %count kontoj:' accounts_owned_other: 'La ludanto %name havas %count kontojn::' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cNevalida malnovaj retpoŝto, provu denove!' invalid: '&cNevalida retadreso, provu denove!' added: '&2Retpoŝtadreso sukcese aldonitaj al via konto!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cBonvolu konfirmi vian retadreson!' changed: '&2Retpoŝtadreso ŝanĝis ĝuste!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Via nuna retadreso estas: &f%email' no_email_for_account: '&2Vi aktuale ne havas retadreson asociita kun ĉi tiu konto.' already_used: '&4La retpoŝto jam estas uzata' @@ -140,3 +141,16 @@ time: hours: 'horoj' day: 'tago' days: 'tagoj' + +# Two-factor authentication +two_factor: + code_created: '&2Via sekreta kodo estas %code. Vi povas skani ĝin de tie %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index 50363ead1..6070411f5 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -61,7 +61,6 @@ misc: logout: '&cDesconectado correctamente.' reload: '&fLa configuración y la base de datos han sido recargados' usage_change_password: '&fUso: /changepw contraseñaActual contraseñaNueva' - two_factor_create: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' accounts_owned_self: 'Eres propietario de %count cuentas:' accounts_owned_other: 'El jugador %name tiene %count cuentas:' @@ -80,7 +79,7 @@ on_join_validation: country_banned: '¡Tu país ha sido baneado de este servidor!' not_owner_error: 'No eres el propietario de esta cuenta. ¡Por favor, elije otro nombre!' invalid_name_case: 'Solo puedes unirte mediante el nombre de usuario %valid, no %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Has usado el comando demasiado rápido! Porfavor, entra al servidor de nuevo y espera un poco antes de usar cualquier comando.' # Email email: @@ -91,8 +90,10 @@ email: old_email_invalid: '[AuthMe] Email anterior inválido!' invalid: '[AuthMe] Email inválido' added: '[AuthMe] Email agregado !' + add_not_allowed: '&cNo se permite añadir un Email' request_confirmation: '[AuthMe] Confirma tu Email !' changed: '[AuthMe] Email cambiado !' + change_not_allowed: '&cNo se permite el cambio de Email' email_show: '&2Tu dirección de E-Mail actual es: &f%email' no_email_for_account: '&2No tienes ningun E-Mail asociado en esta cuenta.' already_used: '&4La dirección Email ya está siendo usada' @@ -141,3 +142,16 @@ time: hours: 'horas' day: 'día' days: 'días' + +# Two-factor authentication +two_factor: + code_created: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_et.yml b/src/main/resources/messages/messages_et.yml index 68ce520f5..3484fae69 100644 --- a/src/main/resources/messages/messages_et.yml +++ b/src/main/resources/messages/messages_et.yml @@ -60,7 +60,6 @@ misc: logout: '&2Edukalt välja logitud!!' reload: '&2Andmebaas uuendatud!' usage_change_password: '&cKasutus: /changepassword ' - two_factor_create: '&2Su salajane kood on %code. Skänni see siin: %url' accounts_owned_self: 'Sa omad %count kontot:' accounts_owned_other: 'Mängijal %name on %count kontot:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cVale vana meiliaadress, proovi uuesti.' invalid: '&cVale meiliaadress, proovi uuesti.' added: '&2Meiliaadress edukalt vahetatud!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cPalun kinnita oma meiliaadress.' changed: '&2Meiliaadress edukalt vahetatud.' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Su meiliaadress on: &f%email' no_email_for_account: '&2Selle kasutajaga pole seotud ühtegi meiliaadressi.' already_used: '&4Meiliaadress juba kasutuses.' @@ -140,3 +141,16 @@ time: hours: 'tundi' day: 'päev' days: 'päeva' + +# Two-factor authentication +two_factor: + code_created: '&2Su salajane kood on %code. Skänni see siin: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 0507d1042..cfa0f78b3 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -60,7 +60,6 @@ misc: logout: '&cAtera zara' reload: '&fConfiguration and database has been reloaded' usage_change_password: '&fErabili: /changepassword pasahitzZaharra pasahitzBerria' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Email zaharra okerra!' invalid: '[AuthMe] Email okerrea' added: '[AuthMe] Emaila gehitu duzu !' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Konfirmatu zure emaila !' changed: '[AuthMe] Emaila aldatua!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index b0b4f3032..00a29ba4e 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -60,7 +60,6 @@ misc: logout: '&cKirjauduit ulos palvelimelta.' reload: '&fAsetukset uudelleenladattu' usage_change_password: '&fKäyttötapa: /changepassword vanhaSalasana uusiSalasana' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Vanha sähköposti on väärä!' invalid: '[AuthMe] Väärä sähköposti' added: '[AuthMe] Sähköposti lisätty!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Vahvistuta sähköposti!' changed: '[AuthMe] Sähköposti vaihdettu!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 52fd4c882..9a492cc72 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -63,7 +63,6 @@ misc: logout: '&cVous avez été déconnecté !' reload: '&aAuthMe a été relancé avec succès.' usage_change_password: '&cPour changer de mot de passe, utilisez "/changepassword "' - two_factor_create: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' accounts_owned_self: 'Vous avez %count comptes:' accounts_owned_other: 'Le joueur %name a %count comptes:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&cAncien email invalide !' invalid: '&cL''email inscrit est invalide !' added: '&aEmail enregistré. En cas de perte de MDP, faites "/email recover "' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cLa confirmation de l''email est manquante ou éronnée.' changed: '&aVotre email a été mis à jour.' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&fL''email enregistré pour votre compte est: %email' no_email_for_account: '&c&oVous n''avez aucun email enregistré sur votre compte.' already_used: '&cCet email est déjà utilisé !' @@ -143,3 +144,16 @@ time: hours: 'heures' day: 'jour' days: 'jours' + +# Two-factor authentication +two_factor: + code_created: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index d22caa7cb..6b6f8707e 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -60,7 +60,6 @@ misc: logout: '&cSesión pechada con éxito' reload: '&fRecargáronse a configuración e a base de datos' usage_change_password: '&fUso: /changepassword ' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] O correo vello non é válido!' invalid: '[AuthMe] Correo non válido' added: '[AuthMe] Correo engadido!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Confirma o teu correo!' changed: '[AuthMe] Cambiouse o correo!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 1d420b746..4f2fe6020 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -60,7 +60,6 @@ misc: logout: '&cSikeresen kijelentkeztél!' reload: 'Beállítások és az adatbázis újratöltve!' usage_change_password: 'Használat: "/changepassword <új jelszó>".' - two_factor_create: '&2A titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' accounts_owned_self: '%count db regisztrációd van:' accounts_owned_other: 'A %name nevű játékosnak, %count db regisztrációja van:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cHibás a régi email cím, próbáld újra!' invalid: '&cHibás az email cím, próbáld újra!' added: '&2Az email címed rögzítése sikeresen megtörtént!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cKérlek, ellenőrízd az email címedet!' changed: '&2Az email cím cseréje sikeresen megtörtént!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2A jelenlegi email-ed a következő: &f%email' no_email_for_account: '&2Ehhez a felhasználóhoz jelenleg még nincs email hozzárendelve.' already_used: '&4Ez az email cím már használatban van!' @@ -140,3 +141,16 @@ time: hours: 'óra' day: 'nap' days: 'nap' + +# Two-factor authentication +two_factor: + code_created: '&2A titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index 65e84cd9c..9e9d61215 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -60,7 +60,6 @@ misc: logout: '&2Berhasil logout!' reload: '&2Konfigurasi dan database telah dimuat ulang!' usage_change_password: '&cUsage: /changepassword ' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail lama tidak valid, coba lagi!' invalid: '&cAlamat email tidak valid, coba lagi!' added: '&2Berhasil menambahkan alamat email ke akunmu!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cSilahkan konfirmasi alamat email kamu!' changed: '&2Alamat email telah diubah dengan benar!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 22354cda8..9e9721b96 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -4,12 +4,10 @@ # %username% - Sostituisce il nome dell'utente che riceve il messaggio. # %displayname% - Sostituisce il nickname (e i colori) dell'utente che riceve il messaggio. -# Registrazione - # Registration registration: disabled: '&cLa registrazione tramite i comandi di gioco è disabilitata.' - name_taken: '&cHai già eseguito la registrazione, non puoi eseguirla nuovamente.' + name_taken: '&cHai già eseguito la registrazione!' register_request: '&3Per favore, esegui la registrazione con il comando: /register ' command_usage: '&cUtilizzo: /register ' reg_only: '&4Puoi giocare in questo server solo dopo aver eseguito la registrazione attraverso il sito web! Per favore, vai su http://esempio.it per procedere!' @@ -41,7 +39,7 @@ error: no_permission: '&4Non hai il permesso di eseguire questa operazione.' unexpected_error: '&4Qualcosa è andato storto, riporta questo errore ad un amministratore!' max_registration: '&cHai raggiunto il numero massimo di registrazioni (%reg_count/%max_acc %reg_names) per questo indirizzo IP!' - logged_in: '&cHai già eseguito l''autenticazione, non è necessario eseguirla nuovamente!' + logged_in: '&cHai già eseguito l''autenticazione!' kick_for_vip: '&3Un utente VIP è entrato mentre il server era pieno e ha preso il tuo posto!' tempban_max_logins: '&cSei stato temporaneamente bandito per aver fallito l''autenticazione troppe volte.' @@ -63,7 +61,6 @@ misc: logout: '&2Disconnessione avvenuta correttamente!' reload: '&2La configurazione e il database sono stati ricaricati correttamente!' usage_change_password: '&cUtilizzo: /changepassword ' - two_factor_create: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' accounts_owned_self: 'Possiedi %count account:' accounts_owned_other: 'Il giocatore %name possiede %count account:' @@ -82,7 +79,7 @@ on_join_validation: country_banned: '&4Il tuo paese è bandito da questo server!' not_owner_error: 'Non sei il proprietario di questo account. Per favore scegli un altro nome!' invalid_name_case: 'Dovresti entrare con questo nome utente "%valid", al posto di "%invalid".' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Hai usato un comando troppo velocemente dal tuo accesso! Per favore, rientra nel server e aspetta un po'' di più prima di usare un qualsiasi comando.' # Email email: @@ -93,8 +90,10 @@ email: old_email_invalid: '&cIl vecchio indirizzo email inserito non è valido, riprova!' invalid: '&cL''indirizzo email inserito non è valido, riprova!' added: '&2Indirizzo email aggiunto correttamente al tuo account!' + add_not_allowed: '&cNon hai il permesso di aggiungere un indirizzo email' request_confirmation: '&cPer favore, conferma il tuo indirizzo email!' changed: '&2Indirizzo email cambiato correttamente!' + change_not_allowed: '&cNon hai il permesso di cambiare l''indirizzo email' email_show: '&2Il tuo indirizzo email al momento è: &f%email' no_email_for_account: '&2Al momento non hai nessun indirizzo email associato al tuo account.' already_used: '&4L''indirizzo email inserito è già in uso' @@ -105,7 +104,7 @@ email: # Password recovery by email recovery: - forgot_password_hint: '&3Hai dimenticato la tua password? Puoi recuperarla eseguendo il comando: /email recovery ' + forgot_password_hint: '&3Hai dimenticato la tua password? Puoi recuperarla usando il comando: /email recovery ' command_usage: '&cUtilizzo: /email recovery ' email_sent: '&2Una email di recupero è stata appena inviata al tuo indirizzo email!' code: @@ -143,3 +142,16 @@ time: hours: 'ore' day: 'giorno' days: 'giorni' + +# Two-factor authentication +two_factor: + code_created: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' + confirmation_required: 'Per favore conferma il tuo codice con: /2fa confirm ' + code_required: 'Per favore inserisci il tuo codice per l''autenticazione a 2 fattori con: /2fa code ' + already_enabled: 'Hai già abilitato l''autenticazione a 2 fattori!' + enable_error_no_code: 'Non hai ancora generato un codice per l''autenticazione a 2 fattori oppure il tuo codice è scaduto. Per favore scrivi: /2fa add' + enable_success: 'Autenticazione a 2 fattori abilitata correttamente' + enable_error_wrong_code: 'Hai inserito un codice sbagliato o scaduto. Per favore scrivi: /2fa add' + not_enabled_error: 'L''autenticazione a 2 fattori non è ancora abilitata per il tuo account. Scrivi: /2fa add' + removed_success: 'Autenticazione a 2 fattori rimossa correttamente' + invalid_code: 'Il codice inserito non è valido, riprova!' diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index 41049b884..32b5ffd43 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -62,7 +62,6 @@ misc: logout: '&2로그아웃 되었습니다!' reload: '&2설정과 데이터 베이스가 새로고침 되었습니다!' usage_change_password: '&c사용법: /changepassword <예전 비밀번호> <새 비밀번호>' - two_factor_create: '&2당신의 비밀 코드는 %code 입니다. %url 에서 스캔할 수 있습니다' accounts_owned_self: '%count 개의 계정을 소유하고 있습니다.' accounts_owned_other: '플레이어 %name 는 %count 개의 계정을 소유하고 있습니다:' @@ -92,8 +91,10 @@ email: old_email_invalid: '&c예전 이메일 주소가 잘못되었습니다. 다시 시도해보세요!' invalid: '&c이메일 주소가 잘못되었습니다. 다시 시도해보세요!' added: '&2계정에 이메일 주소를 추가했습니다!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&c이메일 주소를 확인해주세요!' changed: '&2이메일 주소가 변경되었습니다!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2현재 이메일 주소: &f%email' no_email_for_account: '&2현재 이 계정과 연결된 이메일 주소가 없습니다.' already_used: '&4이메일 주소가 이미 사용 중입니다.' @@ -142,3 +143,16 @@ time: hours: '시간' day: '일' days: '일' + +# Two-factor authentication +two_factor: + code_created: '&2당신의 비밀 코드는 %code 입니다. %url 에서 스캔할 수 있습니다' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index 15eea3048..e58a9fd6e 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -60,7 +60,6 @@ misc: logout: '&aSekmingai atsijungete' reload: '&aNustatymai ir duomenu baze buvo perkrauta.' usage_change_password: '&ePanaudojimas: /changepassword senasSlaptazodis naujasSlaptazodis' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: # TODO old_email_invalid: '&cInvalid old email, try again!' # TODO invalid: '&cInvalid email address, try again!' # TODO added: '&2Email address successfully added to your account!' + # TODO add_not_allowed: '&cAdding email was not allowed' # TODO request_confirmation: '&cPlease confirm your email address!' # TODO changed: '&2Email address changed correctly!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index b8fc40950..00b99161b 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -60,7 +60,6 @@ misc: logout: '&2Je bent succesvol uitgelogd!' reload: '&2De configuratie en database zijn succesvol herladen!' usage_change_password: '&cGebruik: /changepassword ' - two_factor_create: '&2Je geheime code is %code. Je kunt hem scannen op %url' accounts_owned_self: 'Je bezit %count accounts:' accounts_owned_other: 'De speler %name heeft %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cOngeldig oud e-mailadres, probeer het opnieuw!' invalid: '&cOngeldig E-mailadres, probeer het opnieuw!' added: '&2Het e-mailadres is succesvol toegevoegd aan je account!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cVerifiëer je e-mailadres alsjeblieft!' changed: '&2Het e-mailadres is succesvol veranderd!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Jouw huidige e-mailadres is: %email' no_email_for_account: '&2Je hebt nog geen e-mailadres toegevoegd aan dit account.' already_used: '&4Dit e-mailadres wordt al gebruikt' @@ -140,3 +141,16 @@ time: hours: 'uren' day: 'dag' days: 'dagen' + +# Two-factor authentication +two_factor: + code_created: '&2Je geheime code is %code. Je kunt hem scannen op %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 4d9061dfd..78f63a831 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -60,7 +60,6 @@ misc: logout: '&cPomyślnie wylogowany' reload: '&fKonfiguracja bazy danych została przeładowana.' usage_change_password: '&fUżycie: /changepassword ' - two_factor_create: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' accounts_owned_self: '&7Posiadasz %count kont:' accounts_owned_other: '&7Gracz %name posiada %count kont:' @@ -79,7 +78,7 @@ on_join_validation: country_banned: '&4Ten kraj jest zbanowany na tym serwerze' not_owner_error: '&cNie jesteś właścicielem tego konta, wybierz inny nick!' invalid_name_case: '&cPowinieneś dołączyć do serwera z nicku %valid, a nie %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: '&cUżyłeś komendy zbyt szybko! Ponownie dołącz do serwera i poczekaj chwilę, zanim użyjesz dowolnej komendy.' # Email email: @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Stary e-mail niepoprawny!' invalid: '[AuthMe] Nieprawidłowy adres e-mail.' added: '[AuthMe] E-mail został dodany do Twojego konta!' + add_not_allowed: '&cMożliwość dodania adresu e-mail jest wyłączona.' request_confirmation: '[AuthMe] Potwierdź swój adres e-mail!' changed: '[AuthMe] E-mail został zmieniony!' + change_not_allowed: '&cMożliwość zmiany adresu e-mail jest wyłączona.' email_show: '&2Twój aktualny adres e-mail to: &f%email' no_email_for_account: '&2Nie posiadasz adresu e-mail przypisanego do tego konta.' already_used: '&4Ten adres e-mail jest aktualnie używany!' @@ -140,3 +141,16 @@ time: hours: 'godzin' day: 'dzień' days: 'dni' + +# Two-factor authentication +two_factor: + code_created: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' + confirmation_required: 'Musisz potwierdzić swój kod komendą /2fa confirm ' + code_required: 'Wpisz swój kod weryfikacji dwuetapowej przy pomocy komendy /2fa code ' + already_enabled: '&aWeryfikacja dwuetapowa jest już włączona dla Twojego konta.' + enable_error_no_code: '&cKod weryfikacji dwuetapowej nie został dla Ciebie wygenerowany lub wygasł. Wpisz komende /2fa add' + enable_success: '&aWeryfikacja dwuetapowa została włączona dla Twojego konta.' + enable_error_wrong_code: '&cWpisany kod jest nieprawidłowy lub wygasły. Wpisz ponownie /2fa add' + not_enabled_error: 'Weryfikacja dwuetapowa nie jest włączona dla twojego konta. Wpisz komende /2fa add' + removed_success: '&aPomyślnie usunięto weryfikacje dwuetapową z Twojego konta.' + invalid_code: '&cWpisany kod jest nieprawidłowy, spróbuj jeszcze raz.' diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index 2fb0ad2c4..5d5b0ce9b 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -60,7 +60,6 @@ misc: logout: '&cSaida com sucesso' reload: '&fConfiguração e base de dados foram recarregadas' usage_change_password: '&fUse: /changepassword ' - two_factor_create: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' accounts_owned_self: 'Você possui %count contas:' accounts_owned_other: 'O jogador %name possui %count contas:' @@ -79,7 +78,7 @@ on_join_validation: country_banned: 'O seu país está banido deste servidor' not_owner_error: 'Não é o proprietário da conta. Por favor, escolha outro nome!' invalid_name_case: 'Deve se juntar usando nome de usuário %valid, não %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Você usou o comando demasiado rapido por favor re-entre no servidor e aguarde antes de digitar qualquer comando.' # Email email: @@ -99,6 +98,8 @@ email: send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' change_password_expired: 'Você não pode mais alterar a sua password usando este comando.' email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' + add_not_allowed: '&cAdicionar e-mail não é permitido' + change_not_allowed: '&cAlterar e-mail não é permitido' # Password recovery by email recovery: @@ -117,18 +118,18 @@ captcha: usage_captcha: '&cPrecisa digitar um captcha, escreva: /captcha %captcha_code' wrong_captcha: '&cCaptcha errado, por favor escreva: /captcha %captcha_code' valid_captcha: '&cO seu captcha é válido!' - # TODO captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha %captcha_code' - # TODO register_captcha_valid: '&2Valid captcha! You may now register with /register' + captcha_for_registration: 'Para se registar tem de resolver o captcha primeiro, por favor use: /captcha %captcha_code' + register_captcha_valid: '&2Captcha Valido! Agora você pode te registar com /register' # Verification code verification: - # TODO code_required: '&3This command is sensitive and requires an email verification! Check your inbox and follow the email''s instructions.' - # TODO command_usage: '&cUsage: /verification ' - # TODO incorrect_code: '&cIncorrect code, please type "/verification " into the chat, using the code you received by email' - # TODO success: '&2Your identity has been verified! You can now execute all commands within the current session!' - # TODO already_verified: '&2You can already execute every sensitive command within the current session!' - # TODO code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' - # TODO email_needed: '&3To verify your identity you need to link an email address with your account!!' + code_required: '&3Este codigo é sensivel e requer uma verificação por e-mail! Verifique na sua caixa de entrada e siga as instruções do e-mail.' + command_usage: '&cUso: /verification ' + incorrect_code: '&cCodigo incorreto, por favor digite "/verification " no chat, utilizando o codigo que recebeu no e-mail.' + success: '&2Sua identidade foi verificada! Agora você pode executar todos os comandos nesta sessão!' + already_verified: '&2Você já pode digitar todos os comandos sensiveis nesta sessão!' + code_expired: '&3Seu codigo expirou! Execute outro comando sensivel para obter um novo codigo!' + email_needed: '&3Para confirmar a sua identidade necessita de associar um endereço de e-mail!!' # Time units time: @@ -140,3 +141,16 @@ time: hours: 'horas' day: 'dia' days: 'dias' + +# Two-factor authentication +two_factor: + code_created: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' + confirmation_required: 'Por favor confirme seu codigo de 2 etapas com /2fa confirm ' + code_required: 'Por favor submita seu codigo de duas etapas com /2fa code ' + already_enabled: 'Autenticação de duas etapas já se encontra habilitada na sua conta!' + enable_error_no_code: 'Nenhuma chave 2fa foi gerada por você ou expirou. Por favor digite /2fa add' + enable_success: 'Autenticação de duas etapas habilitada com sucesso na sua conta' + enable_error_wrong_code: 'Codigo errado ou o codigo expirou. Por favor digite /2fa add' + not_enabled_error: 'Autenticação de duas etapas não está habilitada na sua conta. Digite /2fa add' + removed_success: 'Autenticação de duas etapas removida com sucesso da sua conta' + invalid_code: 'Codigo invalido!' diff --git a/src/main/resources/messages/messages_ro.yml b/src/main/resources/messages/messages_ro.yml index 6fcacb052..3d9c6cfed 100644 --- a/src/main/resources/messages/messages_ro.yml +++ b/src/main/resources/messages/messages_ro.yml @@ -60,7 +60,6 @@ misc: logout: '&2Te-ai dezautentificat cu succes!' reload: '&2Configuratiile si baza de date sau reincarcat corect!' usage_change_password: '&cFoloseste comanda: /changepassword ' - two_factor_create: '&2Codul tau secret este %code. Il poti scana de aici %url' accounts_owned_self: 'Detii %count conturi:' accounts_owned_other: 'Jucatorul %name are %count conturi:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail-ul vechi este invalid, incearca din nou!' invalid: '&cEmail-ul este invalid, incearca din nou!' added: '&2Email-ul a fost adaugat cu succes la contul tau!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cTe rugam sa confirmi adresa ta de email!' changed: '&2Email-ul a fost schimbat cu succes!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Adresa ta curenta de email este: &f%email' no_email_for_account: '&2Nu ai nici o adresa de email asociata cu acest cont.' already_used: '&4Email-ul acesta este deja folosit de altcineva' @@ -140,3 +141,16 @@ time: hours: 'ore' day: 'zi' days: 'zile' + +# Two-factor authentication +two_factor: + code_created: '&2Codul tau secret este %code. Il poti scana de aici %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index 25a8ca032..c61d85691 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -60,7 +60,6 @@ misc: logout: '&2Вы успешно вышли.' reload: '&6Конфигурация и база данных перезагружены.' usage_change_password: '&cИспользование: /changepassword <пароль> <новый пароль>' - two_factor_create: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' accounts_owned_self: 'У вас %count уч. записей:' accounts_owned_other: 'У игрока %name %count уч. записей:' @@ -79,7 +78,7 @@ on_join_validation: country_banned: '&4Вход с IP-адресов вашей страны запрещён на этом сервере.' not_owner_error: 'Вы не являетесь владельцем данной уч. записи. Выберите себе другое имя!' invalid_name_case: 'Неверное имя! Зайдите под именем %valid, а не %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Вы вводили команды слишком часто! Пожалуйста заходите снова и вводите команды помедленнее.' # Email email: @@ -90,8 +89,10 @@ email: old_email_invalid: '&cНедействительная старая электронная почта!' invalid: '&cНедействительный адрес электронной почты!' added: '&2Электронная почта успешно добавлена!' + add_not_allowed: '&cДобавление электронной почты не было разрешено.' request_confirmation: '&cПодтвердите свою электронную почту!' changed: '&2Адрес электронной почты изменён!' + change_not_allowed: '&cИзменение электронной почты не было разрешено.' email_show: '&2Текущий адрес электронной почты — &f%email' no_email_for_account: '&2К вашей уч. записи не привязана электронная почта.' already_used: '&4Эта электронная почта уже используется.' @@ -117,8 +118,8 @@ captcha: usage_captcha: '&3Необходимо ввести текст с каптчи. Используйте «/captcha %captcha_code»' wrong_captcha: '&cНеверно! Используйте «/captcha %captcha_code».' valid_captcha: '&2Вы успешно решили каптчу!' - # TODO captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha %captcha_code' - # TODO register_captcha_valid: '&2Valid captcha! You may now register with /register' + captcha_for_registration: 'Чтобы зарегистрироваться, решите каптчу используя команду: «/captcha %captcha_code»' + register_captcha_valid: '&2Вы успешно решили каптчу! Теперь вы можете зарегистрироваться командой «/register»' # Verification code verification: @@ -140,3 +141,16 @@ time: hours: 'ч.' day: 'дн.' days: 'дн.' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' + confirmation_required: 'Пожалуйста, подтвердите ваш код с помощью /2fa confirm <код>' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + enable_success: 'Двухфакторная аутентификация для вашего аккаунта успешно подключена' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index 5af6e5030..73ba9f6e1 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -66,7 +66,6 @@ misc: logout: '&cBol si úspešne odhlásený.' reload: '&fZnovu načítanie konfigurácie a databázy bolo úspešné.' usage_change_password: '&fPoužitie: /changepassword ' - two_factor_create: '&2Tvoj tajný kód je %code. Môžeš ho oskenovať tu: %url' accounts_owned_self: 'Vlastníš tieto účty(%count): ' accounts_owned_other: 'Hráč %name vlastní tieto účty(%count): ' @@ -96,8 +95,10 @@ email: old_email_invalid: '&cNeplatný starý email, skús to znovu!' invalid: '&cNeplatná emailová adresa, skús to znovu!' added: '&2Emailová adresa bola úspešne pridaná k tvojmu účtu!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cProsím potvrď svoju emailovú adresu!' changed: '&2Emailová adresa bola úspešne zmenená!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Tvoja súčastná emailová adresa je: &f%email' no_email_for_account: '&2Momentálne nemáš emailovú adresu spojenú s týmto účtom.' already_used: '&4Túto emailovú adresu už niekto používa.' @@ -146,3 +147,16 @@ time: hours: 'hod.' day: 'd.' days: 'd.' + +# Two-factor authentication +two_factor: + code_created: '&2Tvoj tajný kód je %code. Môžeš ho oskenovať tu: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index b9aff0ae9..39a14b865 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -60,7 +60,6 @@ misc: logout: '&2Basariyla cikis yaptin!' reload: '&2Ayarlar ve veritabani yenilendi!' usage_change_password: '&cKullanim: /changepassword ' - two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' accounts_owned_self: 'Sen %count hesaba sahipsin:' accounts_owned_other: 'Oyuncu %name %count hesaba sahip:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cGecersiz eski eposta, tekrar deneyin!' invalid: '&cGecersiz eposta, tekrar deneyin!' added: '&2Eposta basariyla kullaniciniza eklendi!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cLutfen tekrar epostanizi giriniz!' changed: '&2Epostaniz basariyla degistirildi!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Suanki eposta adresin: &f%email' no_email_for_account: '&2Bu hesapla iliskili bir eposta bulunmuyor.' already_used: '&4Eposta adresi zaten kullaniliyor.' @@ -140,3 +141,16 @@ time: hours: 'saat' day: 'gun' days: 'gun' + +# Two-factor authentication +two_factor: + code_created: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index 3ecf52905..034238d3f 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -60,7 +60,6 @@ misc: logout: '&2Ви вийшли зі свого акаунта!' reload: '&2Конфігурації та базу даних було успішно перезавантажено!' usage_change_password: '&cСинтаксис: /changepassword <старийПароль> <новийПароль>' - two_factor_create: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' accounts_owned_self: 'Кількість ваших твінк‒акаунтів: %count:' accounts_owned_other: 'Кількість твінк‒акаунтів гравця %name: %count' @@ -90,12 +89,14 @@ email: old_email_invalid: '&cСтарий e-mail, що прив’язано до вашого акаунта, відрізняється від введеного вами.' invalid: '&cФормат вказаного e-mail’у є некоректним, або його домен внесено до блеклисту.' added: '&2Електронну пошту успішно прив’язано до вашого акаунта.' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cАдреси не співпадають.' changed: '&2E-mail успішно змінено.' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' already_used: '&4До цієї електронної пошти прив’язано забагато акаунтів!' - incomplete_settings: '&4[AuthMe] Error: Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' + incomplete_settings: '&4Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 5e138f4f6..10a813ef6 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -60,7 +60,6 @@ misc: logout: '&2Bạn đã đăng xuất!' reload: '&2Cấu hình và cơ sở dử liệu đã được nạp lại!' usage_change_password: '&cSử dụng: /changepassword ' - two_factor_create: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' accounts_owned_self: 'Bạn sở hữu %count tài khoản:' accounts_owned_other: 'Người chơi %name có %count tài khoản:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail cũ không hợp lệ, vui lòng thử lại!' invalid: '&cĐại chỉ email không hợp lệ, vui lòng thử lại!' added: '&2Địa chỉ email đã thêm vào tài khoản của bạn thành công!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cVui lòng xác nhận địa chỉ email của bạn!' changed: '&2Địa chỉ email đã thay đổi!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Địa chỉ email hiện tại của bạn là: &f%email' no_email_for_account: '&2Hiện tại bạn chưa liên kết bất kỳ email nào với tài khoản này.' already_used: '&4Địa chỉ email đã được sử dụng' @@ -140,3 +141,16 @@ time: hours: 'giờ' day: 'ngày' days: 'ngày' + +# Two-factor authentication +two_factor: + code_created: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index 78c3c89b6..ec7a4b01e 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -60,7 +60,6 @@ misc: logout: '&8[&6玩家系统&8] &c已成功登出!' reload: '&8[&6玩家系统&8] &f配置以及数据已经重新加载完毕' usage_change_password: '&8[&6玩家系统&8] &f正确用法:“/changepassword 旧密码 新密码”' - two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' accounts_owned_self: '您拥有 %count 个账户:' accounts_owned_other: '玩家 %name 拥有 %count 个账户:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&8[&6玩家系统&8] &f旧邮箱无效!' invalid: '&8[&6玩家系统&8] &f无效的邮箱' added: '&8[&6玩家系统&8] &f邮箱已添加 !' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&8[&6玩家系统&8] &f确认你的邮箱 !' changed: '&8[&6玩家系统&8] &f邮箱已改变 !' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2您当前的电子邮件地址为: &f%email' no_email_for_account: '&2您当前并没有任何邮箱与该账号绑定' already_used: '&8[&6玩家系统&8] &4邮箱已被使用' @@ -140,3 +141,16 @@ time: hours: '小时' day: '天' days: '天' + +# Two-factor authentication +two_factor: + code_created: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 9d080748b..996872c86 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -63,7 +63,6 @@ misc: logout: '&8[&6用戶系統&8] &b你成功登出了。' reload: '&8[&6用戶系統&8] &b登入系統設定及資料庫重新載入完畢。' usage_change_password: '&8[&6用戶系統&8] &f用法:《 /changepassword <舊密碼> <新密碼> 》' - two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' accounts_owned_self: '你擁有 %count 個帳戶:' accounts_owned_other: '玩家《%name》擁有 %count 個帳戶:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&8[&6用戶系統&8] &c你所填寫的舊電郵地址並不正確。' invalid: '&8[&6用戶系統&8] &c你所填寫的電郵地址並不正確。' added: '&8[&6用戶系統&8] &a已新增你的電郵地址。' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&8[&6用戶系統&8] &5請重覆輸入你的電郵地址。' changed: '&8[&6用戶系統&8] &a你的電郵地址已更改。' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&8[&6用戶系統&8] &2你所使用的電郵地址為:&f%email' no_email_for_account: '&8[&6用戶系統&8] &2你並未有綁定電郵地址到此帳戶。' already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' @@ -143,3 +144,16 @@ time: hours: '小時' day: '日' days: '日' + +# Two-factor authentication +two_factor: + code_created: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhmc.yml b/src/main/resources/messages/messages_zhmc.yml index a151a8cf8..cf9fe986f 100644 --- a/src/main/resources/messages/messages_zhmc.yml +++ b/src/main/resources/messages/messages_zhmc.yml @@ -60,7 +60,6 @@ misc: logout: '&2已成功註銷!' reload: '&2伺服器已正確地被重新加載配置和數據庫!' usage_change_password: '&c使用方法: "/changepassword [舊密碼] [新密碼]"' - two_factor_create: '&2您的密碼是 %code。您可以從這裡掃描 %url' accounts_owned_self: '您擁有 %count 個帳戶:' accounts_owned_other: '玩家 %name 擁有 %count 個帳戶:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&c舊電子郵件地址無效,請重試!' invalid: '&c電子郵件地址無效,請重試!' added: '&2電子郵件地址已成功添加到您的帳戶!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&c請確認你的電郵地址!' changed: '&2已正確地更改電子郵件地址!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' already_used: '&4此電子郵件地址已被使用' @@ -140,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + code_created: '&2您的密碼是 %code。您可以從這裡掃描 %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index 43a7b5338..f6192193a 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -62,7 +62,6 @@ misc: logout: '&b【AuthMe】&6您已成功登出' reload: '&b【AuthMe】&6已重新讀取設定檔及資料庫' usage_change_password: '&b【AuthMe】&6用法: &c"/changepassword <舊密碼> <新密碼>"' - two_factor_create: '&b【AuthMe - 兩步驗證碼】&b您的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' accounts_owned_self: '&b【AuthMe】&6您擁有 %count 個帳號:' accounts_owned_other: '&b【AuthMe】&6玩家 %name 擁有 %count 個帳號:' @@ -92,8 +91,10 @@ email: old_email_invalid: '&b【AuthMe】&6舊的Email無效!' invalid: '&b【AuthMe】&6無效的Email!' added: '&b【AuthMe】&6已添加Email!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&b【AuthMe】&6請驗證您的Email!' changed: '&b【AuthMe】&6Email已變更!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&b【AuthMe】&2目前的電子郵件: &f%email' no_email_for_account: '&b【AuthMe】&2您目前沒有設置電子郵件.' already_used: '&b【AuthMe】&4這個電郵地址已被使用。' @@ -142,3 +143,16 @@ time: hours: '時' day: '天' days: '天' + +# Two-factor authentication +two_factor: + code_created: '&b【AuthMe - 兩步驗證碼】&b您的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7ddb4b7e0..90e31c921 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -46,6 +46,11 @@ commands: aliases: - changepass - cp + totp: + description: TOTP commands + usage: /totp code|add|confirm|remove + aliases: + - 2fa captcha: description: Captcha command usage: /captcha @@ -156,6 +161,9 @@ permissions: authme.admin.updatemessages: description: Permission to use the update messages command. default: op + authme.allowchatbeforelogin: + description: Permission to send chat messages before being logged in. + default: false authme.allowmultipleaccounts: description: Permission to be able to register multiple accounts. default: op @@ -234,6 +242,8 @@ permissions: authme.player.register: true authme.player.security.verificationcode: true authme.player.seeownaccounts: true + authme.player.totpadd: true + authme.player.totpremove: true authme.player.unregister: true authme.player.canbeforced: description: Permission for users a login can be forced to. @@ -281,6 +291,12 @@ permissions: authme.player.seeownaccounts: description: Permission to use to see own other accounts. default: true + authme.player.totpadd: + description: Permission to enable two-factor authentication. + default: true + authme.player.totpremove: + description: Permission to disable two-factor authentication. + default: true authme.player.unregister: description: Command permission to unregister. default: true diff --git a/src/test/java/fr/xephi/authme/AuthMeMatchers.java b/src/test/java/fr/xephi/authme/AuthMeMatchers.java index 33768883d..653fb04c9 100644 --- a/src/test/java/fr/xephi/authme/AuthMeMatchers.java +++ b/src/test/java/fr/xephi/authme/AuthMeMatchers.java @@ -44,8 +44,8 @@ public final class AuthMeMatchers { }; } - public static Matcher hasAuthBasicData(String name, String realName, - String email, String lastIp) { + public static Matcher hasAuthBasicData(String name, String realName, + String email, String lastIp) { return new TypeSafeMatcher() { @Override public boolean matchesSafely(PlayerAuth item) { diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java index 267f7d7cb..f02b5dc9b 100644 --- a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -5,7 +5,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import fr.xephi.authme.data.captcha.CaptchaCodeStorage; +import fr.xephi.authme.datasource.AbstractSqlDataSource; import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.datasource.columnshandler.DataSourceColumn; +import fr.xephi.authme.datasource.columnshandler.PlayerAuthColumn; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.initialization.HasCleanup; import fr.xephi.authme.process.register.executors.RegistrationMethod; @@ -52,7 +55,7 @@ public class ClassesConsistencyTest { int.class, long.class, float.class, String.class, File.class, Enum.class, collectionsUnmodifiableList(), Charset.class, /* AuthMe */ - Property.class, RegistrationMethod.class, + Property.class, RegistrationMethod.class, DataSourceColumn.class, PlayerAuthColumn.class, /* Guava */ ImmutableMap.class, ImmutableList.class); @@ -60,6 +63,7 @@ public class ClassesConsistencyTest { private static final Set> CLASSES_EXCLUDED_FROM_VISIBILITY_TEST = ImmutableSet.of( Whirlpool.class, // not our implementation, so we don't touch it MySqlExtension.class, // has immutable protected fields used by all children + AbstractSqlDataSource.class, // protected members for inheritance Columns.class // uses non-static String constants, which is safe ); diff --git a/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java index 7eff3a72d..e645922e7 100644 --- a/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java +++ b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java @@ -30,21 +30,9 @@ public class CodeClimateConfigTest { assertThat(excludePaths, not(empty())); removeTestsExclusionOrThrow(excludePaths); for (String path : excludePaths) { - verifySourceFileExists(path); - } - } - - private static void verifySourceFileExists(String path) { - // Note ljacqu 20170323: In the future, we could have legitimate exclusions that don't fulfill these checks, - // in which case this test needs to be adapted accordingly. - if (!path.startsWith(TestHelper.SOURCES_FOLDER)) { - fail("Unexpected path '" + path + "': expected to start with sources folder"); - } else if (!path.endsWith(".java")) { - fail("Expected path '" + path + "' to end with '.java'"); - } - - if (!new File(path).exists()) { - fail("Path '" + path + "' does not exist!"); + if (!new File(path).exists()) { + fail("Path '" + path + "' does not exist!"); + } } } diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 2b8215baa..133d01940 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -44,7 +44,7 @@ public class CommandInitializerTest { // It obviously doesn't make sense to test much of the concrete data // that is being initialized; we just want to guarantee with this test // that data is indeed being initialized and we take a few "probes" - assertThat(commands, hasSize(9)); + assertThat(commands, hasSize(10)); assertThat(commandsIncludeLabel(commands, "authme"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "register"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "help"), equalTo(false)); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java index 07c92d13c..dc3fda40e 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.authme; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; 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; @@ -38,7 +38,7 @@ public class GetEmailCommandTest { public void shouldReportUnknownUser() { // given String user = "myTestUser"; - given(dataSource.getEmail(user)).willReturn(DataSourceResult.unknownPlayer()); + given(dataSource.getEmail(user)).willReturn(DataSourceValueImpl.unknownRow()); CommandSender sender = mock(CommandSender.class); // when @@ -53,7 +53,7 @@ public class GetEmailCommandTest { // given String user = "userToView"; String email = "user.email@example.org"; - given(dataSource.getEmail(user)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(user)).willReturn(DataSourceValueImpl.of(email)); CommandSender sender = mock(CommandSender.class); // when diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java new file mode 100644 index 000000000..94e53ff27 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.command.help.HelpMessagesService; +import fr.xephi.authme.service.HelpTranslationGenerator; +import org.bukkit.command.CommandSender; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link UpdateHelpMessagesCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class UpdateHelpMessagesCommandTest { + + @InjectMocks + private UpdateHelpMessagesCommand command; + + @Mock + private HelpTranslationGenerator helpTranslationGenerator; + @Mock + private HelpMessagesService helpMessagesService; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldUpdateHelpMessage() throws IOException { + // given + File updatedFile = new File("some/path/help_xx.yml"); + given(helpTranslationGenerator.updateHelpFile()).willReturn(updatedFile); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(helpMessagesService).reloadMessagesFile(); + verify(sender).sendMessage("Successfully updated the help file 'help_xx.yml'"); + } + + @Test + public void shouldCatchAndReportException() throws IOException { + // given + given(helpTranslationGenerator.updateHelpFile()).willThrow(new IOException("Couldn't do the thing")); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(sender).sendMessage("Could not update help file: Couldn't do the thing"); + verifyZeroInteractions(helpMessagesService); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java new file mode 100644 index 000000000..8f34bcbf8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java @@ -0,0 +1,106 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * Test for {@link PlayerAuthViewer}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PlayerAuthViewerTest { + + @InjectMocks + private PlayerAuthViewer authViewer; + + @Mock + private DataSource dataSource; + + @Test + public void shouldMakeExample() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + authViewer.execute(sender, Collections.emptyList()); + + // then + verify(sender).sendMessage(argThat(containsString("Example: /authme debug db Bobby"))); + } + + @Test + public void shouldHandleMissingPlayer() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + authViewer.execute(sender, Collections.singletonList("bogus")); + + // then + verify(dataSource).getAuth("bogus"); + verify(sender).sendMessage(argThat(containsString("No record exists for 'bogus'"))); + } + + @Test + public void shouldDisplayAuthInfo() { + // given + CommandSender sender = mock(CommandSender.class); + PlayerAuth auth = PlayerAuth.builder().name("george").realName("George") + .password("abcdefghijkl", "mnopqrst") + .lastIp("127.1.2.7").registrationDate(1111140000000L) + .totpKey("SECRET1321") + .build(); + given(dataSource.getAuth("George")).willReturn(auth); + + // when + authViewer.execute(sender, Collections.singletonList("George")); + + // then + ArgumentCaptor textCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(textCaptor.capture()); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Player george / George"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Registration: 2005-03-18T"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Hash / salt (partial): 'abcdef...' / 'mnop...'"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("TOTP code (partial): 'SEC...'"))); + } + + @Test + public void shouldHandleCornerCases() { + // given + CommandSender sender = mock(CommandSender.class); + PlayerAuth auth = PlayerAuth.builder().name("tar") + .password("abcd", null) + .lastIp("127.1.2.7").registrationDate(0L) + .build(); + given(dataSource.getAuth("Tar")).willReturn(auth); + + // when + authViewer.execute(sender, Collections.singletonList("Tar")); + + // then + ArgumentCaptor textCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(textCaptor.capture()); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Player tar / Player"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Registration: Not available (0)"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Last login: Not available (null)"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Hash / salt (partial): 'ab...' / ''"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("TOTP code (partial): ''"))); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java similarity index 90% rename from src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java rename to src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java index 33acaf3f6..bf7fba782 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java @@ -24,13 +24,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; /** - * Tests for {@link SetPasswordCommand}. + * Tests for {@link EmailSetPasswordCommand}. */ @RunWith(MockitoJUnitRunner.class) -public class SetPasswordCommandTest { +public class EmailSetPasswordCommandTest { @InjectMocks - private SetPasswordCommand command; + private EmailSetPasswordCommand command; @Mock private DataSource dataSource; @@ -70,6 +70,7 @@ public class SetPasswordCommandTest { // then verify(validationService).validatePassword("abc123", name); verify(dataSource).updatePassword(name, hashedPassword); + verify(recoveryService).removeFromSuccessfulRecovery(player); verify(commonService).send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); } @@ -101,7 +102,7 @@ public class SetPasswordCommandTest { command.runCommand(player, Collections.singletonList("abc123")); // then - verifyZeroInteractions(validationService); - verifyZeroInteractions(dataSource); + verifyZeroInteractions(validationService, dataSource); + verify(commonService).send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); } } diff --git a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java index 416649e05..5842ec300 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.command.executable.email; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.TestHelper; 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.security.PasswordSecurity; @@ -118,7 +118,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.unknownPlayer()); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.unknownRow()); // when command.executeCommand(sender, Collections.singletonList("someone@example.com")); @@ -138,7 +138,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(DEFAULT_EMAIL)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(DEFAULT_EMAIL)); // when command.executeCommand(sender, Collections.singletonList(DEFAULT_EMAIL)); @@ -158,7 +158,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of("raptor@example.org")); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of("raptor@example.org")); // when command.executeCommand(sender, Collections.singletonList("wrong-email@example.com")); @@ -180,7 +180,7 @@ public class RecoverEmailCommandTest { given(emailService.sendRecoveryCode(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "v@example.com"; - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(email)); String code = "a94f37"; given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); given(recoveryCodeService.generateCode(name)).willReturn(code); @@ -205,7 +205,7 @@ public class RecoverEmailCommandTest { given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "vulture@example.com"; - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(email)); given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(false); setBukkitServiceToRunTaskAsynchronously(bukkitService); diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 38307f93d..b5ce86a53 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -16,7 +16,6 @@ import java.util.Collections; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -57,7 +56,7 @@ public class LoginCommandTest { command.executeCommand(sender, Collections.singletonList("password")); // then - verify(management).performLogin(eq(sender), eq("password")); + verify(management).performLogin(sender, "password" ); } @Test diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java new file mode 100644 index 000000000..ff8608b37 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java @@ -0,0 +1,93 @@ +package fr.xephi.authme.command.executable.totp; + +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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link AddTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AddTotpCommandTest { + + @InjectMocks + private AddTotpCommand addTotpCommand; + + @Mock + private GenerateTotpService generateTotpService; + @Mock + private PlayerCache playerCache; + @Mock + private Messages messages; + + @Test + public void shouldHandleNonLoggedInUser() { + // given + Player player = mockPlayerWithName("bob"); + given(playerCache.getAuth("bob")).willReturn(null); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.NOT_LOGGED_IN); + verifyZeroInteractions(generateTotpService); + } + + @Test + public void shouldNotAddCodeForAlreadyExistingTotp() { + // given + Player player = mockPlayerWithName("arend"); + PlayerAuth auth = PlayerAuth.builder().name("arend") + .totpKey("TOTP2345").build(); + given(playerCache.getAuth("arend")).willReturn(auth); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + verifyZeroInteractions(generateTotpService); + } + + @Test + public void shouldGenerateTotpCode() { + // given + Player player = mockPlayerWithName("charles"); + PlayerAuth auth = PlayerAuth.builder().name("charles").build(); + given(playerCache.getAuth("charles")).willReturn(auth); + + TotpGenerationResult generationResult = new TotpGenerationResult( + "777Key214", "http://example.org/qr-code/link"); + given(generateTotpService.generateTotpKey(player)).willReturn(generationResult); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.TWO_FACTOR_CREATE, generationResult.getTotpKey(), generationResult.getAuthenticatorQrCodeUrl()); + verify(messages).send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java new file mode 100644 index 000000000..17a011ee8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java @@ -0,0 +1,158 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.TestHelper; +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 org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link ConfirmTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ConfirmTotpCommandTest { + + @InjectMocks + private ConfirmTotpCommand command; + + @Mock + private GenerateTotpService generateTotpService; + @Mock + private DataSource dataSource; + @Mock + private PlayerCache playerCache; + @Mock + private Messages messages; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldAddTotpCodeToUserAfterSuccessfulConfirmation() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(playerCache.getAuth(playerName)).willReturn(auth); + String generatedTotpKey = "totp-key"; + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult(generatedTotpKey, "url-not-relevant")); + String totpCode = "954321"; + given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(true); + given(dataSource.setTotpKey(anyString(), anyString())).willReturn(true); + + // when + command.runCommand(player, Collections.singletonList(totpCode)); + + // then + verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); + verify(generateTotpService).removeGenerateTotpKey(player); + verify(dataSource).setTotpKey(playerName, generatedTotpKey); + verify(playerCache).updatePlayer(auth); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + assertThat(auth.getTotpKey(), equalTo(generatedTotpKey)); + } + + @Test + public void shouldHandleWrongTotpCode() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(playerCache.getAuth(playerName)).willReturn(auth); + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult("totp-key", "url-not-relevant")); + String totpCode = "754321"; + given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(false); + + // when + command.runCommand(player, Collections.singletonList(totpCode)); + + // then + verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); + verify(generateTotpService, never()).removeGenerateTotpKey(any(Player.class)); + verify(playerCache, only()).getAuth(playerName); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldHandleMissingTotpKey() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(playerCache.getAuth(playerName)).willReturn(auth); + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(null); + + // when + command.runCommand(player, Collections.singletonList("871634")); + + // then + verify(generateTotpService, only()).getGeneratedTotpKey(player); + verify(playerCache, only()).getAuth(playerName); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldStopForAlreadyExistingTotpKeyOnAccount() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).totpKey("A987234").build(); + given(playerCache.getAuth(playerName)).willReturn(auth); + + // when + command.runCommand(player, Collections.singletonList("871634")); + + // then + verify(playerCache, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService, dataSource); + verify(messages).send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } + + @Test + public void shouldHandleMissingAuthAccount() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + given(playerCache.getAuth(playerName)).willReturn(null); + + // when + command.runCommand(player, Collections.singletonList("984685")); + + // then + verify(playerCache, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService, dataSource); + verify(messages).send(player, MessageKey.NOT_LOGGED_IN); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java new file mode 100644 index 000000000..18903972d --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java @@ -0,0 +1,149 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.TestHelper; +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 org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link RemoveTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class RemoveTotpCommandTest { + + @InjectMocks + private RemoveTotpCommand command; + + @Mock + private DataSource dataSource; + @Mock + private PlayerCache playerCache; + @Mock + private TotpAuthenticator totpAuthenticator; + @Mock + private Messages messages; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldRemoveTotpKey() { + // given + String name = "aws"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(true); + given(dataSource.removeTotpKey(name)).willReturn(true); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verify(dataSource).removeTotpKey(name); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + verify(playerCache).updatePlayer(auth); + assertThat(auth.getTotpKey(), nullValue()); + } + + @Test + public void shouldHandleDatabaseError() { + // given + String name = "aws"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(true); + given(dataSource.removeTotpKey(name)).willReturn(false); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verify(dataSource).removeTotpKey(name); + verify(messages, only()).send(player, MessageKey.ERROR); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleInvalidCode() { + // given + String name = "cesar"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(false); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_INVALID_CODE); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleUserWithoutTotpKey() { + // given + String name = "cesar"; + PlayerAuth auth = PlayerAuth.builder().name(name).build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "654684"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource, totpAuthenticator); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleNonLoggedInUser() { + // given + String name = "cesar"; + given(playerCache.getAuth(name)).willReturn(null); + String inputCode = "654684"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource, totpAuthenticator); + verify(messages, only()).send(player, MessageKey.NOT_LOGGED_IN); + verify(playerCache, only()).getAuth(name); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java new file mode 100644 index 000000000..0e279f687 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.CommandMapper; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link TotpBaseCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpBaseCommandTest { + + @InjectMocks + private TotpBaseCommand command; + + @Mock + private CommandMapper mapper; + @Mock + private HelpProvider helpProvider; + + @Test + public void shouldOutputHelp() { + // given + CommandSender sender = mock(CommandSender.class); + FoundCommandResult mappingResult = mock(FoundCommandResult.class); + given(mapper.mapPartsToCommand(sender, Collections.singletonList("totp"))).willReturn(mappingResult); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(mapper).mapPartsToCommand(sender, Collections.singletonList("totp")); + verify(helpProvider).outputHelp(sender, mappingResult, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java b/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java index b99d6a373..732b1ad70 100644 --- a/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.data; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; @@ -51,7 +51,7 @@ public class VerificationCodeManagerTest { // given String name1 = "ILoveTests"; Player player1 = mockPlayerWithName(name1); - given(dataSource.getEmail(name1)).willReturn(DataSourceResult.of("ilovetests@test.com")); + given(dataSource.getEmail(name1)).willReturn(DataSourceValueImpl.of("ilovetests@test.com")); given(permissionsManager.hasPermission(player1, PlayerPermission.VERIFICATION_CODE)).willReturn(true); String name2 = "StillLovingTests"; Player player2 = mockPlayerWithName(name2); @@ -106,7 +106,7 @@ public class VerificationCodeManagerTest { // given String player = "ILoveTests"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager2.codeExistOrGenerateNew(player); @@ -125,7 +125,7 @@ public class VerificationCodeManagerTest { // given String player = "ILoveTests"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager2.codeExistOrGenerateNew(player); @@ -145,7 +145,7 @@ public class VerificationCodeManagerTest { String player = "ILoveTests"; String code = "193458"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager1.codeExistOrGenerateNew(player); diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java index f1507a29e..ff0743001 100644 --- a/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java @@ -76,7 +76,7 @@ public class LimboPlayerTaskManagerTest { given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(interval); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then verify(limboPlayer).setMessageTask(any(MessageTask.class)); @@ -94,7 +94,7 @@ public class LimboPlayerTaskManagerTest { given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(0); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, true); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.LOG_IN); // then verifyZeroInteractions(limboPlayer, bukkitService); @@ -113,7 +113,7 @@ public class LimboPlayerTaskManagerTest { given(messages.retrieveSingle(player, MessageKey.REGISTER_MESSAGE)).willReturn("Please register!"); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then assertThat(limboPlayer.getMessageTask(), not(nullValue())); @@ -137,7 +137,7 @@ public class LimboPlayerTaskManagerTest { given(messages.retrieveSingle(player, MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captcha)).willReturn("Need to use captcha"); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then assertThat(limboPlayer.getMessageTask(), not(nullValue())); diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java index 81feaa8ed..8bcfaff79 100644 --- a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java @@ -90,7 +90,7 @@ public class LimboServiceTest { limboService.createLimboPlayer(player, true); // then - verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(true)); + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(LimboMessageType.LOG_IN)); verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); verify(player).setAllowFlight(false); verify(player).setFlySpeed(0.0f); @@ -121,7 +121,7 @@ public class LimboServiceTest { limboService.createLimboPlayer(player, false); // then - verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(false)); + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(LimboMessageType.REGISTER)); verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); verify(permissionsManager, only()).hasGroupSupport(); verify(player).setAllowFlight(false); @@ -209,7 +209,7 @@ public class LimboServiceTest { // then verify(taskManager).registerTimeoutTask(player, limbo); - verify(taskManager).registerMessageTask(player, limbo, true); + verify(taskManager).registerMessageTask(player, limbo, LimboMessageType.LOG_IN); verify(authGroupHandler).setGroup(player, limbo, AuthGroupType.REGISTERED_UNAUTHENTICATED); } diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 530ab56be..58fd59c08 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -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.collect.Lists; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; @@ -61,7 +63,7 @@ public abstract class AbstractDataSourceIntegrationTest { // when HashedPassword bobbyPassword = dataSource.getPassword("bobby"); HashedPassword invalidPassword = dataSource.getPassword("doesNotExist"); - HashedPassword userPassword = dataSource.getPassword("user"); + HashedPassword userPassword = dataSource.getPassword("User"); // then assertThat(bobbyPassword, equalToHash("$SHA$11aa0706173d7272$dbba966")); @@ -103,12 +105,14 @@ public abstract class AbstractDataSourceIntegrationTest { assertThat(bobbyAuth, hasRegistrationInfo("127.0.4.22", 1436778723L)); assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L)); assertThat(bobbyAuth.getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); + assertThat(bobbyAuth.getTotpKey(), equalTo("JBSWY3DPEHPK3PXP")); assertThat(userAuth, hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90")); assertThat(userAuth, hasAuthLocation(124.1, 76.3, -127.8, "nether", 0.23f, 4.88f)); assertThat(userAuth, hasRegistrationInfo(null, 0)); assertThat(userAuth.getLastLogin(), equalTo(1453242857L)); assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); + assertThat(userAuth.getTotpKey(), nullValue()); } @Test @@ -160,7 +164,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword("non-existent-name", new HashedPassword("sd")); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("user"), equalToHash(newHash)); } @@ -175,7 +180,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword("non-existent-name", new HashedPassword("asdfasdf", "a1f34ec")); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("user"), equalToHash("new_hash")); } @@ -191,7 +197,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword(invalidAuth); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("bobby"), equalToHash("tt", "cc")); } @@ -273,7 +280,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updateEmail(invalidAuth); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getAllAuths(), hasItem(hasAuthBasicData("user", "user", email, "34.56.78.90"))); } @@ -328,7 +336,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updateRealName("notExists", "NOTEXISTS"); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getAuth("bobby"), hasAuthBasicData("bobby", "BOBBY", null, "123.45.67.89")); } @@ -415,12 +424,12 @@ public abstract class AbstractDataSourceIntegrationTest { DataSource dataSource = getDataSource(); // when - DataSourceResult email1 = dataSource.getEmail(user1); - DataSourceResult email2 = dataSource.getEmail(user2); + DataSourceValue email1 = dataSource.getEmail(user1); + DataSourceValue email2 = dataSource.getEmail(user2); // then assertThat(email1.getValue(), equalTo("user@example.org")); - assertThat(email2, is(DataSourceResult.unknownPlayer())); + assertThat(email2, is(DataSourceValueImpl.unknownRow())); } @Test @@ -494,4 +503,33 @@ public abstract class AbstractDataSourceIntegrationTest { contains("user24", "user20", "user22", "user29", "user28", "user16", "user18", "user12", "user14", "user11")); } + + @Test + public void shouldSetTotpKey() { + // given + DataSource dataSource = getDataSource(); + String newTotpKey = "My new TOTP key"; + + // when + dataSource.setTotpKey("BObBy", newTotpKey); + dataSource.setTotpKey("does-not-exist", "bogus"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), equalTo(newTotpKey)); + } + + @Test + public void shouldRemoveTotpKey() { + // given + DataSource dataSource = getDataSource(); + + // when + dataSource.removeTotpKey("BoBBy"); + dataSource.removeTotpKey("user"); + dataSource.removeTotpKey("does-not-exist"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), nullValue()); + assertThat(dataSource.getAuth("user").getTotpKey(), nullValue()); + } } diff --git a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java index eab6a4cc4..027608230 100644 --- a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java @@ -55,7 +55,9 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest { HikariConfig config = new HikariConfig(); config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); config.setConnectionTestQuery("VALUES 1"); - config.addDataSourceProperty("URL", "jdbc:h2:mem:test"); + // Note "ignorecase=true": H2 does not support `COLLATE NOCASE` for case-insensitive equals queries. + // MySQL is by default case-insensitive so this is OK to make as an assumption. + config.addDataSourceProperty("URL", "jdbc:h2:mem:test;ignorecase=true"); config.addDataSourceProperty("user", "sa"); config.addDataSourceProperty("password", "sa"); HikariDataSource ds = new HikariDataSource(config); diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java index 2f6121a90..114d42101 100644 --- a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -30,10 +30,10 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -123,7 +123,7 @@ public class OnJoinVerifierTest { List onlinePlayers = Arrays.asList(mock(Player.class), mock(Player.class)); given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); given(permissionsManager.hasPermission(onlinePlayers.get(1), PlayerStatePermission.IS_VIP)).willReturn(false); - returnOnlineListFromBukkitServer(onlinePlayers); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); given(messages.retrieveSingle(player, MessageKey.KICK_FOR_VIP)).willReturn("kick for vip"); @@ -147,7 +147,7 @@ public class OnJoinVerifierTest { given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); List onlinePlayers = Collections.singletonList(mock(Player.class)); given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); - returnOnlineListFromBukkitServer(onlinePlayers); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); given(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER)).willReturn("kick full server"); @@ -501,13 +501,6 @@ public class OnJoinVerifierTest { onJoinVerifier.checkPlayerCountry(joiningPlayer, ip, false); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void returnOnlineListFromBukkitServer(Collection onlineList) { - // Note ljacqu 20160529: The compiler gets lost in generics because Collection is returned - // from getOnlinePlayers(). We need to uncheck onlineList to a simple Collection or it will refuse to compile. - given(bukkitService.getOnlinePlayers()).willReturn((Collection) onlineList); - } - private void expectValidationExceptionWith(MessageKey messageKey, String... args) { expectedException.expect(exceptionWithData(messageKey, args)); } diff --git a/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java b/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java index 42d14e463..59d837a9c 100644 --- a/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java +++ b/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java @@ -6,6 +6,8 @@ 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.process.Management; import fr.xephi.authme.service.AntiBotService; import fr.xephi.authme.service.BukkitService; @@ -60,6 +62,7 @@ import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceTo import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -113,6 +116,8 @@ public class PlayerListenerTest { private JoinMessageService joinMessageService; @Mock private QuickCommandsProtectionManager quickCommandsProtectionManager; + @Mock + private PermissionsManager permissionsManager; /** * #831: If a player is kicked because of "logged in from another location", the kick @@ -289,12 +294,14 @@ public class PlayerListenerTest { given(settings.getProperty(RestrictionSettings.ALLOW_CHAT)).willReturn(false); AsyncPlayerChatEvent event = newAsyncChatEvent(); given(listenerService.shouldCancelEvent(event.getPlayer())).willReturn(true); + given(permissionsManager.hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN)).willReturn(false); // when listener.onPlayerChat(event); // then verify(listenerService).shouldCancelEvent(event.getPlayer()); + verify(permissionsManager).hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); verify(event).setCancelled(true); verify(messages).send(event.getPlayer(), MessageKey.DENIED_CHAT); } @@ -356,6 +363,25 @@ public class PlayerListenerTest { assertThat(event.getRecipients(), empty()); } + @Test + public void shouldAllowChatForBypassPermission() { + // given + given(settings.getProperty(RestrictionSettings.ALLOW_CHAT)).willReturn(false); + AsyncPlayerChatEvent event = newAsyncChatEvent(); + given(listenerService.shouldCancelEvent(event.getPlayer())).willReturn(true); + given(permissionsManager.hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.HIDE_CHAT)).willReturn(false); + + // when + listener.onPlayerChat(event); + + // then + assertThat(event.isCancelled(), equalTo(false)); + verify(listenerService).shouldCancelEvent(event.getPlayer()); + verify(permissionsManager).hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); + assertThat(event.getRecipients(), hasSize(3)); + } + @Test public void shouldAllowUnlimitedMovement() { // given diff --git a/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java b/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java index 9fbd27bdb..b04259b21 100644 --- a/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java +++ b/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.message; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.help.HelpSection; +import fr.xephi.authme.util.ExceptionUtils; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.BeforeClass; @@ -85,7 +86,7 @@ public class YamlTextFileCheckerTest { errors.add("Message for '" + mandatoryKey + "' is empty"); } } catch (Exception e) { - errors.add("Could not load file: " + StringUtils.formatException(e)); + errors.add("Could not load file: " + ExceptionUtils.formatException(e)); } } } diff --git a/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java b/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java index 83f5a5c63..254e0b1a6 100644 --- a/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java +++ b/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java @@ -100,6 +100,23 @@ public class MessageUpdaterTest { equalTo("seconds in plural")); } + @Test + public void shouldPerformNewerMigrations() throws IOException { + // given + File messagesFile = temporaryFolder.newFile(); + Files.copy(TestHelper.getJarFile(TestHelper.PROJECT_ROOT + "message/messages_test2.yml"), messagesFile); + + // when + boolean wasChanged = messageUpdater.migrateAndSave(messagesFile, "messages/messages_en.yml", "messages/messages_en.yml"); + + // then + assertThat(wasChanged, equalTo(true)); + FileConfiguration configuration = YamlConfiguration.loadConfiguration(messagesFile); + assertThat(configuration.getString(MessageKey.TWO_FACTOR_CREATE.getKey()), equalTo("Old 2fa create text")); + assertThat(configuration.getString(MessageKey.WRONG_PASSWORD.getKey()), equalTo("test2 - wrong password")); // from pre-5.5 key + assertThat(configuration.getString(MessageKey.SECOND.getKey()), equalTo("second")); // from messages_en.yml + } + @Test public void shouldHaveAllKeysInConfigurationData() { // given diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java index b945e9f93..6d2fdd222 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java @@ -4,7 +4,9 @@ import fr.xephi.authme.TestHelper; 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.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -15,11 +17,13 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.function.Function; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; /** @@ -49,6 +53,9 @@ public class AsyncAddEmailTest { @Mock private BungeeSender bungeeSender; + @Mock + private BukkitService bukkitService; + @BeforeClass public static void setUp() { TestHelper.setupLogger(); @@ -66,6 +73,8 @@ public class AsyncAddEmailTest { given(dataSource.updateEmail(any(PlayerAuth.class))).willReturn(true); given(validationService.validateEmail(email)).willReturn(true); given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when asyncAddEmail.addEmail(player, email); @@ -89,6 +98,8 @@ public class AsyncAddEmailTest { given(dataSource.updateEmail(any(PlayerAuth.class))).willReturn(false); given(validationService.validateEmail(email)).willReturn(true); given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when asyncAddEmail.addEmail(player, email); @@ -184,4 +195,27 @@ public class AsyncAddEmailTest { verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); } + @Test + public void shouldNotAddOnCancelledEvent() { + // given + String email = "player@mail.tld"; + given(player.getName()).willReturn("TestName"); + given(playerCache.isAuthenticated("testname")).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(auth.getEmail()).willReturn(null); + given(playerCache.getAuth("testname")).willReturn(auth); + given(validationService.validateEmail(email)).willReturn(true); + given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + event.setCancelled(true); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + + // when + asyncAddEmail.addEmail(player, email); + + // then + verify(service).send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED); + verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); + } + } diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java index 4427c45d6..23fb01e69 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java @@ -3,7 +3,9 @@ package fr.xephi.authme.process.email; 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.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -14,10 +16,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.function.Function; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,6 +53,9 @@ public class AsyncChangeEmailTest { @Mock private BungeeSender bungeeSender; + @Mock + private BukkitService bukkitService; + @Test public void shouldChangeEmail() { // given @@ -59,7 +67,9 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(true); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); - + EmailChangedEvent event = spy(new EmailChangedEvent(player, "old@mail.tld", newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + // when process.changeEmail(player, "old@mail.tld", newEmail); @@ -81,6 +91,8 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(true); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, oldEmail, newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when process.changeEmail(player, "old-mail@example.org", newEmail); @@ -102,6 +114,8 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(false); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, "old@mail.tld", newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when process.changeEmail(player, "old@mail.tld", newEmail); @@ -219,6 +233,30 @@ public class AsyncChangeEmailTest { verify(service).send(player, MessageKey.REGISTER_MESSAGE); } + @Test + public void shouldNotChangeOnCancelledEvent() { + // given + String newEmail = "new@example.com"; + String oldEmail = "old@example.com"; + given(player.getName()).willReturn("Username"); + given(playerCache.isAuthenticated("username")).willReturn(true); + PlayerAuth auth = authWithMail(oldEmail); + given(playerCache.getAuth("username")).willReturn(auth); + given(validationService.validateEmail(newEmail)).willReturn(true); + given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, oldEmail, newEmail, false)); + event.setCancelled(true); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + + // when + process.changeEmail(player, oldEmail, newEmail); + + // then + verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); + verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); + verify(service).send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED); + } + private static PlayerAuth authWithMail(String email) { PlayerAuth auth = mock(PlayerAuth.class); when(auth.getEmail()).thenReturn(email); diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java index 9f21b7ba4..2c02e581f 100644 --- a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -21,13 +21,13 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import java.util.Arrays; -import java.util.Collection; +import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -124,7 +124,7 @@ public class AsynchronousLoginTest { public void shouldNotForceLoginUserWithAlreadyOnlineIp() { // given String name = "oscar"; - String ip = "127.0.12.245"; + String ip = "1.1.1.245"; Player player = mockPlayer(name); TestHelper.mockPlayerIp(player, ip); given(playerCache.isAuthenticated(name)).willReturn(false); @@ -147,7 +147,7 @@ public class AsynchronousLoginTest { public void shouldNotForceLoginForCanceledEvent() { // given String name = "oscar"; - String ip = "127.0.12.245"; + String ip = "1.1.1.245"; Player player = mockPlayer(name); TestHelper.mockPlayerIp(player, ip); given(playerCache.isAuthenticated(name)).willReturn(false); @@ -156,12 +156,9 @@ public class AsynchronousLoginTest { given(commonService.getProperty(DatabaseSettings.MYSQL_COL_GROUP)).willReturn(""); given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); doReturn(false).when(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(any(Player.class), anyString()); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - ((AuthMeAsyncPreLoginEvent) invocation.getArgument(0)).setCanLogin(false); - return null; - } + doAnswer((Answer) invocation -> { + ((AuthMeAsyncPreLoginEvent) invocation.getArgument(0)).setCanLogin(false); + return null; }).when(bukkitService).callEvent(any(AuthMeAsyncPreLoginEvent.class)); // when @@ -183,7 +180,7 @@ public class AsynchronousLoginTest { mockOnlinePlayersInBukkitService(); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "1.1.1.1"); // then assertThat(result, equalTo(false)); @@ -198,7 +195,7 @@ public class AsynchronousLoginTest { given(commonService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(0); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "2.2.2.2"); // then assertThat(result, equalTo(false)); @@ -213,7 +210,7 @@ public class AsynchronousLoginTest { given(commonService.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "1.1.1.1"); // then assertThat(result, equalTo(false)); @@ -230,7 +227,7 @@ public class AsynchronousLoginTest { mockOnlinePlayersInBukkitService(); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "2.2.2.2"); // then assertThat(result, equalTo(true)); @@ -244,33 +241,32 @@ public class AsynchronousLoginTest { return player; } - @SuppressWarnings({ "unchecked", "rawtypes" }) private void mockOnlinePlayersInBukkitService() { - // 127.0.0.4: albania (online), brazil (offline) + // 1.1.1.1: albania (online), brazil (offline) Player playerA = mockPlayer("albania"); - TestHelper.mockPlayerIp(playerA, "127.0.0.4"); + TestHelper.mockPlayerIp(playerA, "1.1.1.1"); given(dataSource.isLogged(playerA.getName())).willReturn(true); Player playerB = mockPlayer("brazil"); - TestHelper.mockPlayerIp(playerB, "127.0.0.4"); + TestHelper.mockPlayerIp(playerB, "1.1.1.1"); given(dataSource.isLogged(playerB.getName())).willReturn(false); - // 192.168.0.1: congo (online), denmark (offline), ecuador (online) + // 2.2.2.2: congo (online), denmark (offline), ecuador (online) Player playerC = mockPlayer("congo"); - TestHelper.mockPlayerIp(playerC, "192.168.0.1"); + TestHelper.mockPlayerIp(playerC, "2.2.2.2"); given(dataSource.isLogged(playerC.getName())).willReturn(true); Player playerD = mockPlayer("denmark"); - TestHelper.mockPlayerIp(playerD, "192.168.0.1"); + TestHelper.mockPlayerIp(playerD, "2.2.2.2"); given(dataSource.isLogged(playerD.getName())).willReturn(false); Player playerE = mockPlayer("ecuador"); - TestHelper.mockPlayerIp(playerE, "192.168.0.1"); + TestHelper.mockPlayerIp(playerE, "2.2.2.2"); given(dataSource.isLogged(playerE.getName())).willReturn(true); - // 192.168.0.0: france (offline) + // 3.3.3.3: france (offline) Player playerF = mockPlayer("france"); - TestHelper.mockPlayerIp(playerF, "192.168.0.0"); + TestHelper.mockPlayerIp(playerF, "3.3.3.3"); - Collection onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); - given(bukkitService.getOnlinePlayers()).willReturn(onlinePlayers); + List onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); } } diff --git a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java index 77f91ca88..029ff90ce 100644 --- a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java +++ b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java @@ -4,11 +4,13 @@ import ch.jalu.injector.factory.SingletonStore; import fr.xephi.authme.TestHelper; 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.register.executors.PasswordRegisterParams; import fr.xephi.authme.process.register.executors.RegistrationExecutor; import fr.xephi.authme.process.register.executors.RegistrationMethod; import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -19,6 +21,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.function.Function; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -40,6 +44,8 @@ public class AsyncRegisterTest { @Mock private CommonService commonService; @Mock + private BukkitService bukkitService; + @Mock private DataSource dataSource; @Mock private SingletonStore registrationExecutorStore; @@ -99,6 +105,31 @@ public class AsyncRegisterTest { verifyZeroInteractions(executor); } + @Test + @SuppressWarnings("unchecked") + public void shouldStopForCanceledEvent() { + // given + String name = "edbert"; + Player player = mockPlayerWithName(name); + TestHelper.mockPlayerIp(player, "33.44.55.66"); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); + given(dataSource.isAuthAvailable(name)).willReturn(false); + RegistrationExecutor executor = mock(RegistrationExecutor.class); + TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); + singletonStoreWillReturn(registrationExecutorStore, executor); + + AuthMeAsyncPreRegisterEvent canceledEvent = new AuthMeAsyncPreRegisterEvent(player, true); + canceledEvent.setCanRegister(false); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(canceledEvent); + + // when + asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); + + // then + verify(dataSource, only()).isAuthAvailable(name); + } + @Test @SuppressWarnings("unchecked") public void shouldStopForFailedExecutorCheck() { @@ -115,6 +146,9 @@ public class AsyncRegisterTest { given(executor.isRegistrationAdmitted(params)).willReturn(false); singletonStoreWillReturn(registrationExecutorStore, executor); + given(bukkitService.createAndCallEvent(any(Function.class))) + .willReturn(new AuthMeAsyncPreRegisterEvent(player, false)); + // when asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); diff --git a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java index fc9fd0d66..e59dae647 100644 --- a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.security; import ch.jalu.injector.Injector; import ch.jalu.injector.InjectorBuilder; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.security.crypts.Argon2; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; @@ -40,6 +41,7 @@ public class HashAlgorithmIntegrationTest { given(settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS)).willReturn(10_000); injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create(); injector.register(Settings.class, settings); + TestHelper.setupLogger(); } @Test diff --git a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java index 5c1fda220..440e748a2 100644 --- a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java +++ b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java @@ -123,4 +123,13 @@ public class HashUtilsTest { assertThat(HashUtils.isValidBcryptHash("#2ae5fc78"), equalTo(false)); } + @Test + public void shouldCompareStrings() { + // given / when / then + assertThat(HashUtils.isEqual("test", "test"), equalTo(true)); + assertThat(HashUtils.isEqual("test", "Test"), equalTo(false)); + assertThat(HashUtils.isEqual("1234", "1234."), equalTo(false)); + assertThat(HashUtils.isEqual("ພາສາຫວຽດນາມ", "ພາສາຫວຽດນາມ"), equalTo(true)); + assertThat(HashUtils.isEqual("test", "tëst"), equalTo(false)); + } } diff --git a/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java b/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java new file mode 100644 index 000000000..1a22d26e7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link GenerateTotpService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class GenerateTotpServiceTest { + + @InjectMocks + private GenerateTotpService generateTotpService; + + @Mock + private TotpAuthenticator totpAuthenticator; + + @Test + public void shouldGenerateTotpKey() { + // given + TotpGenerationResult givenGenerationResult = new TotpGenerationResult("1234", "http://example.com/link/to/chart"); + Player player = mockPlayerWithName("Spencer"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + + // when + TotpGenerationResult result = generateTotpService.generateTotpKey(player); + + // then + assertThat(result, equalTo(givenGenerationResult)); + assertThat(generateTotpService.getGeneratedTotpKey(player), equalTo(givenGenerationResult)); + } + + @Test + public void shouldRemoveGeneratedTotpKey() { + // given + TotpGenerationResult givenGenerationResult = new TotpGenerationResult("1234", "http://example.com/link/to/chart"); + Player player = mockPlayerWithName("Hanna"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + generateTotpService.generateTotpKey(player); + + // when + generateTotpService.removeGenerateTotpKey(player); + + // then + assertThat(generateTotpService.getGeneratedTotpKey(player), nullValue()); + } + + @Test + public void shouldCheckGeneratedTotpKey() { + // given + String generatedKey = "ASLO43KDF2J"; + TotpGenerationResult givenGenerationResult = new TotpGenerationResult(generatedKey, "url"); + Player player = mockPlayerWithName("Aria"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + generateTotpService.generateTotpKey(player); + String validCode = "928374"; + given(totpAuthenticator.checkCode(generatedKey, validCode)).willReturn(true); + + // when + boolean invalidCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, "000000"); + boolean validCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, validCode); + boolean unknownPlayerResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(mockPlayerWithName("other"), "299874"); + + // then + assertThat(invalidCodeResult, equalTo(false)); + assertThat(validCodeResult, equalTo(true)); + assertThat(unknownPlayerResult, equalTo(false)); + verify(totpAuthenticator).checkCode(generatedKey, "000000"); + verify(totpAuthenticator).checkCode(generatedKey, validCode); + } + + @Test + public void shouldRemoveExpiredEntries() throws InterruptedException { + // given + TotpGenerationResult generationResult = new TotpGenerationResult("key", "url"); + ExpiringMap generatedKeys = + ReflectionTestUtils.getFieldValue(GenerateTotpService.class, generateTotpService, "totpKeys"); + generatedKeys.setExpiration(1, TimeUnit.MILLISECONDS); + generatedKeys.put("ghost", generationResult); + generatedKeys.setExpiration(5, TimeUnit.MINUTES); + generatedKeys.put("ezra", generationResult); + + // when + Thread.sleep(2L); + generateTotpService.performCleanup(); + + // then + assertThat(generateTotpService.getGeneratedTotpKey(mockPlayerWithName("Ezra")), equalTo(generationResult)); + assertThat(generateTotpService.getGeneratedTotpKey(mockPlayerWithName("ghost")), nullValue()); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } +} diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java new file mode 100644 index 000000000..3afc81817 --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java @@ -0,0 +1,120 @@ +package fr.xephi.authme.security.totp; + +import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static fr.xephi.authme.AuthMeMatchers.stringWithLength; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link TotpAuthenticator}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpAuthenticatorTest { + + private TotpAuthenticator totpAuthenticator; + + @Mock + private BukkitService bukkitService; + + @Mock + private IGoogleAuthenticator googleAuthenticator; + + @Before + public void initializeTotpAuthenticator() { + totpAuthenticator = new TotpAuthenticatorTestImpl(bukkitService); + } + + @Test + public void shouldGenerateTotpKey() { + // given + // Use the GoogleAuthenticator instance the TotpAuthenticator normally creates to test its parameters + totpAuthenticator = new TotpAuthenticator(bukkitService); + + Player player = mock(Player.class); + given(player.getName()).willReturn("Bobby"); + given(bukkitService.getIp()).willReturn("127.48.44.4"); + + // when + TotpGenerationResult key1 = totpAuthenticator.generateTotpKey(player); + TotpGenerationResult key2 = totpAuthenticator.generateTotpKey(player); + + // then + assertThat(key1.getTotpKey(), stringWithLength(16)); + assertThat(key2.getTotpKey(), stringWithLength(16)); + assertThat(key1.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key2.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key1.getTotpKey(), not(equalTo(key2.getTotpKey()))); + } + + @Test + public void shouldCheckCode() { + // given + String secret = "the_secret"; + int code = 21398; + given(googleAuthenticator.authorize(secret, code)).willReturn(true); + + // when + boolean result = totpAuthenticator.checkCode(secret, Integer.toString(code)); + + // then + assertThat(result, equalTo(true)); + verify(googleAuthenticator).authorize(secret, code); + } + + @Test + public void shouldHandleInvalidNumberInput() { + // given / when + boolean result = totpAuthenticator.checkCode("Some_Secret", "123ZZ"); + + // then + assertThat(result, equalTo(false)); + verifyZeroInteractions(googleAuthenticator); + } + + @Test + public void shouldVerifyCode() { + // given + String totpKey = "ASLO43KDF2J"; + PlayerAuth auth = PlayerAuth.builder() + .name("Maya") + .totpKey(totpKey) + .build(); + String inputCode = "408435"; + given(totpAuthenticator.checkCode(totpKey, inputCode)).willReturn(true); + + // when + boolean result = totpAuthenticator.checkCode(auth, inputCode); + + // then + assertThat(result, equalTo(true)); + verify(googleAuthenticator).authorize(totpKey, 408435); + } + + private final class TotpAuthenticatorTestImpl extends TotpAuthenticator { + + TotpAuthenticatorTestImpl(BukkitService bukkitService) { + super(bukkitService); + } + + @Override + protected IGoogleAuthenticator createGoogleAuthenticator() { + return googleAuthenticator; + } + } +} diff --git a/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java b/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java index 62dda8e50..f992e2554 100644 --- a/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java @@ -19,6 +19,7 @@ import org.mockito.Mock; import java.util.Arrays; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToScheduleSyncDelayedTaskWithDelay; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -154,11 +155,10 @@ public class AntiBotServiceTest { } @Test - @SuppressWarnings({"unchecked", "rawtypes"}) public void shouldInformPlayersOnActivation() { // given - listening antibot List players = Arrays.asList(mock(Player.class), mock(Player.class)); - given(bukkitService.getOnlinePlayers()).willReturn((List) players); + returnGivenOnlinePlayers(bukkitService, players); given(permissionsManager.hasPermission(players.get(0), AdminPermission.ANTIBOT_MESSAGES)).willReturn(false); given(permissionsManager.hasPermission(players.get(1), AdminPermission.ANTIBOT_MESSAGES)).willReturn(true); diff --git a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java index 232d11ce2..e6787c109 100644 --- a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java @@ -332,6 +332,19 @@ public class BukkitServiceTest { assertThat(event.getPlayer(), equalTo(player)); } + @Test + public void shouldReturnServerIp() { + // given + String ip = "99.99.99.99"; + given(server.getIp()).willReturn(ip); + + // when + String result = bukkitService.getIp(); + + // then + assertThat(result, equalTo(ip)); + } + // Note: This method is used through reflections public static Player[] onlinePlayersImpl() { return new Player[]{ diff --git a/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java b/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java index 9807e4f5b..7d57869dd 100644 --- a/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java +++ b/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java @@ -1,7 +1,12 @@ package fr.xephi.authme.service; +import org.bukkit.entity.Player; + +import java.util.Collection; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doAnswer; /** @@ -81,4 +86,17 @@ public final class BukkitServiceTestHelper { return null; }).when(bukkitService).scheduleSyncDelayedTask(any(Runnable.class), anyLong()); } + + /** + * Sets a BukkitService mock to return the given players when its method + * {@link BukkitService#getOnlinePlayers()} is invoked. + * + * @param bukkitService the mock to set behavior on + * @param players the players to return + */ + @SuppressWarnings("unchecked") + public static void returnGivenOnlinePlayers(BukkitService bukkitService, Collection players) { + // The compiler gets lost in generics because Collection is returned from getOnlinePlayers() + given(bukkitService.getOnlinePlayers()).willReturn((Collection) players); + } } diff --git a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java index 48973bbfa..f0fc9b87c 100644 --- a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java @@ -3,6 +3,7 @@ package fr.xephi.authme.service; import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; @@ -14,6 +15,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -70,4 +73,50 @@ public class PasswordRecoveryServiceTest { verify(emailService).sendRecoveryCode(name, email, code); verify(commonService).send(player, MessageKey.RECOVERY_CODE_SENT); } + + @Test + public void shouldKeepTrackOfSuccessfulRecoversByIp() { + // given + Player bobby = mock(Player.class); + TestHelper.mockPlayerIp(bobby, "192.168.8.8"); + given(bobby.getName()).willReturn("bobby"); + + Player bobby2 = mock(Player.class); + TestHelper.mockPlayerIp(bobby2, "127.0.0.1"); + given(bobby2.getName()).willReturn("bobby"); + + Player other = mock(Player.class); + TestHelper.mockPlayerIp(other, "192.168.8.8"); + given(other.getName()).willReturn("other"); + + // when + recoveryService.addSuccessfulRecovery(bobby); + + // then + assertThat(recoveryService.canChangePassword(bobby), equalTo(true)); + assertThat(recoveryService.canChangePassword(bobby2), equalTo(false)); + assertThat(recoveryService.canChangePassword(other), equalTo(false)); + } + + @Test + public void shouldRemovePlayerFromSuccessfulRecovers() { + // given + Player bobby = mock(Player.class); + TestHelper.mockPlayerIp(bobby, "192.168.8.8"); + given(bobby.getName()).willReturn("bobby"); + recoveryService.addSuccessfulRecovery(bobby); + + Player other = mock(Player.class); + TestHelper.mockPlayerIp(other, "8.8.8.8"); + given(other.getName()).willReturn("other"); + recoveryService.addSuccessfulRecovery(other); + + // when + recoveryService.removeFromSuccessfulRecovery(other); + + + // then + assertThat(recoveryService.canChangePassword(bobby), equalTo(true)); + assertThat(recoveryService.canChangePassword(other), equalTo(false)); + } } diff --git a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java index de53ec169..51cef1936 100644 --- a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java +++ b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import static fr.xephi.authme.TestHelper.getJarFile; +import static fr.xephi.authme.settings.properties.DatabaseSettings.MYSQL_COL_SALT; import static fr.xephi.authme.settings.properties.PluginSettings.ENABLE_PERMISSION_CHECK; import static fr.xephi.authme.settings.properties.PluginSettings.LOG_LEVEL; import static fr.xephi.authme.settings.properties.PluginSettings.REGISTERED_GROUP; @@ -128,6 +129,7 @@ public class SettingsMigrationServiceTest { assertThat(settings.getProperty(UNREGISTERED_GROUP), equalTo("")); assertThat(settings.getProperty(PASSWORD_HASH), equalTo(HashAlgorithm.SHA256)); assertThat(settings.getProperty(LEGACY_HASHES), contains(HashAlgorithm.PBKDF2, HashAlgorithm.WORDPRESS, HashAlgorithm.SHA512)); + assertThat(settings.getProperty(MYSQL_COL_SALT), equalTo("salt_col_name")); // Check migration of old setting to email.html assertThat(Files.readLines(new File(dataFolder, "email.html"), StandardCharsets.UTF_8), diff --git a/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java index c650dd6f5..2109edbd9 100644 --- a/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java +++ b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -68,7 +69,7 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldLoadWelcomeMessage() throws IOException { + public void shouldLoadWelcomeMessage() { // given String welcomeMessage = "This is my welcome message for testing\nBye!"; setWelcomeMessageAndReload(welcomeMessage); @@ -84,7 +85,7 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldReplaceNameAndIpAndCountry() throws IOException { + public void shouldReplaceNameAndIpAndCountry() { // given String welcomeMessage = "Hello {PLAYER}, your IP is {IP}\nYour country is {COUNTRY}.\nWelcome to {SERVER}!"; setWelcomeMessageAndReload(welcomeMessage); @@ -108,11 +109,11 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldApplyOtherReplacements() throws IOException { + public void shouldApplyOtherReplacements() { // given String welcomeMessage = "{ONLINE}/{MAXPLAYERS} online\n{LOGINS} logged in\nYour world is {WORLD}\nServer: {VERSION}"; setWelcomeMessageAndReload(welcomeMessage); - given(bukkitService.getOnlinePlayers()).willReturn((List) Arrays.asList(mock(Player.class), mock(Player.class))); + returnGivenOnlinePlayers(bukkitService, Arrays.asList(mock(Player.class), mock(Player.class))); given(server.getMaxPlayers()).willReturn(20); given(playerCache.getLogged()).willReturn(1); given(server.getBukkitVersion()).willReturn("Bukkit-456.77.8"); diff --git a/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java b/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java index 93538c858..2ecf6a13a 100644 --- a/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java +++ b/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java @@ -216,18 +216,16 @@ public class PurgeTaskTest { private void setPermissionsBehavior() { given(permissionsManager.hasPermissionOffline(any(OfflinePlayer.class), eq(BYPASS_NODE))) - .willAnswer(new Answer() { - @Override - public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { - OfflinePlayer player = invocationOnMock.getArgument(0); - Boolean hasPermission = playerBypassAssignments.get(player); - if (hasPermission == null) { - throw new IllegalStateException("Unexpected check of '" + BYPASS_NODE - + "' with player = " + player); - } - return hasPermission; + .willAnswer((Answer) invocationOnMock -> { + OfflinePlayer player = invocationOnMock.getArgument(0); + Boolean hasPermission = playerBypassAssignments.get(player); + if (hasPermission == null) { + throw new IllegalStateException("Unexpected check of '" + BYPASS_NODE + + "' with player = " + player); } + return hasPermission; }); + given(permissionsManager.loadUserData(any(OfflinePlayer.class))).willReturn(true); } private void assertRanPurgeWithPlayers(OfflinePlayer... players) { diff --git a/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java index 8685d7f33..9f60c53af 100644 --- a/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java @@ -4,8 +4,10 @@ import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import org.junit.Test; +import java.net.MalformedURLException; import java.util.ConcurrentModificationException; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; @@ -58,4 +60,16 @@ public class ExceptionUtilsTest { // given / when / then TestHelper.validateHasOnlyPrivateEmptyConstructor(ExceptionUtils.class); } + + @Test + public void shouldFormatException() { + // given + MalformedURLException ex = new MalformedURLException("Unrecognized URL format"); + + // when + String result = ExceptionUtils.formatException(ex); + + // then + assertThat(result, equalTo("[MalformedURLException]: Unrecognized URL format")); + } } diff --git a/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java b/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java index d45c0a578..02d8872a2 100644 --- a/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java @@ -3,8 +3,8 @@ package fr.xephi.authme.util; import fr.xephi.authme.TestHelper; import org.junit.Test; -import static org.junit.Assert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; /** * Test for {@link InternetProtocolUtils} @@ -13,14 +13,44 @@ public class InternetProtocolUtilsTest { @Test public void shouldCheckLocalAddress() { + // loopback + assertThat(InternetProtocolUtils.isLocalAddress("localhost"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("127.0.0.1"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("::1"), equalTo(true)); + + // site local assertThat(InternetProtocolUtils.isLocalAddress("10.0.0.1"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("172.0.0.1"), equalTo(false)); assertThat(InternetProtocolUtils.isLocalAddress("172.16.0.1"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("192.168.0.1"), equalTo(true)); + + // deprecated site-local + // ref: https://en.wikipedia.org/wiki/IPv6_address#Default_address_selection + assertThat(InternetProtocolUtils.isLocalAddress("fec0::"), equalTo(true)); + + // unique site-local (not deprecated!) + // ref: https://en.wikipedia.org/wiki/Unique_local_address + assertThat(InternetProtocolUtils.isLocalAddress("fde4:8dba:82e1::"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fc00::"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fe00::"), equalTo(false)); + + // link local + assertThat(InternetProtocolUtils.isLocalAddress("169.254.0.64"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("FE80:0000:0000:0000:C800:0EFF:FE74:0008"), equalTo(true)); + + // public assertThat(InternetProtocolUtils.isLocalAddress("94.32.34.5"), equalTo(false)); } + @Test + public void testIsLoopback() { + // loopback + assertThat(InternetProtocolUtils.isLoopbackAddress("localhost"), equalTo(true)); + assertThat(InternetProtocolUtils.isLoopbackAddress("127.0.0.1"), equalTo(true)); + assertThat(InternetProtocolUtils.isLoopbackAddress("::1"), equalTo(true)); + } + @Test public void shouldHavePrivateConstructor() { // given / when / then diff --git a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java index 7111f81b8..76e7ae754 100644 --- a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java @@ -3,8 +3,6 @@ package fr.xephi.authme.util; import fr.xephi.authme.TestHelper; import org.junit.Test; -import java.net.MalformedURLException; - import static java.util.Arrays.asList; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -63,18 +61,6 @@ public class StringUtilsTest { assertFalse(StringUtils.isEmpty(" test")); } - @Test - public void shouldFormatException() { - // given - MalformedURLException ex = new MalformedURLException("Unrecognized URL format"); - - // when - String result = StringUtils.formatException(ex); - - // then - assertThat(result, equalTo("[MalformedURLException]: Unrecognized URL format")); - } - @Test public void shouldGetDifferenceWithNullString() { // given/when/then diff --git a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql index 306df4769..f8153d0d2 100644 --- a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -4,6 +4,7 @@ CREATE TABLE authme ( id INTEGER AUTO_INCREMENT, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, + totp VARCHAR(16), ip VARCHAR(40), lastlogin BIGINT, regdate BIGINT NOT NULL, @@ -22,7 +23,7 @@ CREATE TABLE authme ( CONSTRAINT table_const_prim PRIMARY KEY (id) ); -INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip) -VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22'); +INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip, totp) +VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22','JBSWY3DPEHPK3PXP'); INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate) VALUES (NULL,'user','b28c32f624a4eb161d6adc9acb5bfc5b','34.56.78.90',1453242857,124.1,76.3,-127.8,'nether',0.23,4.88,'user@example.org',0,'user','f750ba32',0); diff --git a/src/test/resources/fr/xephi/authme/message/messages_test2.yml b/src/test/resources/fr/xephi/authme/message/messages_test2.yml index e4a607239..f2871ddbb 100644 --- a/src/test/resources/fr/xephi/authme/message/messages_test2.yml +++ b/src/test/resources/fr/xephi/authme/message/messages_test2.yml @@ -4,3 +4,5 @@ unknown_user: 'Message from test2' login: 'test2 - login' not_logged_in: 'test2 - not logged in' wrong_pwd: 'test2 - wrong password' +misc: + two_factor_create: 'Old 2fa create text' diff --git a/src/test/resources/fr/xephi/authme/settings/config-old.yml b/src/test/resources/fr/xephi/authme/settings/config-old.yml index 8c1dc7f3e..2e34180cb 100644 --- a/src/test/resources/fr/xephi/authme/settings/config-old.yml +++ b/src/test/resources/fr/xephi/authme/settings/config-old.yml @@ -275,7 +275,7 @@ settings: applyBlindEffect: false ExternalBoardOptions: # MySQL column for the salt , needed for some forum/cms support - mySQLColumnSalt: '' + mySQLColumnSalt: 'salt_col_name' # MySQL column for the group, needed for some forum/cms support mySQLColumnGroup: '' # -1 mean disabled. If u want that only