From c2deb9d0b5c9fa4c57249ee9afc0a7922686ce0a Mon Sep 17 00:00:00 2001 From: Xephi Date: Thu, 31 Dec 2015 16:35:38 +0100 Subject: [PATCH 01/26] Start rework of Settings Please move to another branch :3 (cherry picked from commit 0c5d835) --- .../settings/custom/ConverterSettings.java | 46 ++++++ .../authme/settings/custom/CustomSetting.java | 156 ++++++++++++++++++ .../settings/custom/DatabaseSettings.java | 121 ++++++++++++++ .../authme/settings/custom/EmailSettings.java | 83 ++++++++++ .../authme/settings/custom/HooksSettings.java | 54 ++++++ .../settings/custom/ProtectionSettings.java | 63 +++++++ .../authme/settings/custom/PurgeSettings.java | 62 +++++++ .../settings/custom/SecuritySettings.java | 60 +++++++ .../settings/custom/annotations/Comment.java | 20 +++ .../settings/custom/annotations/Type.java | 43 +++++ 10 files changed, 708 insertions(+) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java new file mode 100644 index 000000000..7cee6a521 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class ConverterSettings extends CustomSetting { + + @Comment("Rakamak file name") + @Type(SettingType.String) + public String rakamakFileName = "users.rak"; + + @Comment("Rakamak use Ip ?") + @Type(SettingType.Boolean) + public boolean rakamakeUseIP = false; + + @Comment("Rakamak IP file name") + @Type(SettingType.String) + public String rakamakIPFileName = "UsersIp.rak"; + + @Comment("CrazyLogin database file name") + @Type(SettingType.String) + public String crazyLoginFileName = "accounts.db"; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "converter.yml"); + + private ConverterSettings instance; + + public ConverterSettings() + { + super(configFile); + instance = this; + } + + public ConverterSettings getInstance() { + return instance; + } + + public void setInstance(ConverterSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java b/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java new file mode 100644 index 000000000..27792efd9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java @@ -0,0 +1,156 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.settings.CustomConfiguration; +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; + +public class CustomSetting extends CustomConfiguration { + + private File configFile; + public boolean isFirstLaunch = false; + + public CustomSetting(File file) { + super(file); + this.configFile = file; + try { + if (!configFile.exists()) + { + isFirstLaunch = true; + configFile.createNewFile(); + } + else + { + load(); + loadValues(); + } + save(); + } catch (IOException e) + { + ConsoleLogger.writeStackTrace(e); + } + } + + @Override + public boolean reLoad() + { + boolean out = true; + if (!configFile.exists()) { + try { + configFile.createNewFile(); + save(); + } catch (IOException e) { + out = false; + } + } + if (out) + { + load(); + loadValues(); + } + return out; + } + + public void loadValues() + { + for (Field f : this.getClass().getDeclaredFields()) + { + if (!f.isAnnotationPresent(Type.class)) + continue; + f.setAccessible(true); + try { + switch (f.getAnnotation(Type.class).value()) + { + case Boolean: + f.setBoolean(this, this.getBoolean(f.getName())); + break; + case Double: + f.setDouble(this, this.getDouble(f.getName())); + break; + case Int: + f.setInt(this, this.getInt(f.getName())); + break; + case Long: + f.setLong(this, this.getLong(f.getName())); + break; + case String: + f.set(this, this.getString(f.getName())); + break; + case StringList: + f.set(this, this.getStringList(f.getName())); + break; + default: + break; + } + } catch (Exception e) + { + ConsoleLogger.writeStackTrace(e); + } + } + } + + @Override + public void save() + { + FileWriter writer = null; + try { + writer = new FileWriter(configFile); + writer.write(""); + for (Field f : this.getClass().getDeclaredFields()) + { + if (!f.isAnnotationPresent(Comment.class)) + continue; + if (!f.isAnnotationPresent(Type.class)) + continue; + for (String s : f.getAnnotation(Comment.class).value()) + { + writer.append("# " + s + "\n"); + } + writer.append(f.getName() + ": "); + switch (f.getAnnotation(Type.class).value()) + { + case Boolean: + writer.append(f.getBoolean(this) ? "true" : "false"); + break; + case Double: + writer.append("" + f.getDouble(this)); + break; + case Int: + writer.append("" + f.getInt(this)); + break; + case String: + writer.append("'" + f.get(this).toString() + "'"); + break; + case StringList: + @SuppressWarnings("unchecked") + List list = (List) f.get(this); + writer.append("\n"); + if (list.isEmpty()) + writer.write("[]"); + else + for (String s : list) + writer.append(" - '" + s + "'\n"); + break; + case Long: + writer.append("" + f.getLong(this)); + break; + default: + break; + + } + writer.append("\n"); + writer.flush(); + } + writer.close(); + } catch (Exception e) { + ConsoleLogger.writeStackTrace(e); + } + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java new file mode 100644 index 000000000..c3c20c6bd --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class DatabaseSettings extends CustomSetting { + + @Comment({"What type of database do you want to use?", + "Valid values: sqlite, mysql"}) + @Type(SettingType.String) + public String backend = "sqlite"; + + @Comment("Enable database caching, should improve database performance") + @Type(SettingType.Boolean) + public boolean caching = true; + + @Comment("Database host address") + @Type(SettingType.String) + public String mySQLHost = "127.0.0.1"; + + @Comment("Database port") + @Type(SettingType.String) + public String mySQLPort = "3306"; + + @Comment("Username about Database Connection Infos") + @Type(SettingType.String) + public String mySQLUsername = "authme"; + + @Comment("Password about Database Connection Infos") + @Type(SettingType.String) + public String mySQLPassword = "12345"; + + @Comment("Database Name, use with converters or as SQLITE database name") + @Type(SettingType.String) + public String mySQLDatabase = "authme"; + + @Comment("Table of the database") + @Type(SettingType.String) + public String mySQLTablename = "authme"; + + @Comment("Column of IDs to sort data") + @Type(SettingType.String) + public String mySQLColumnId = "id"; + + @Comment("Column for storing or checking players nickname") + @Type(SettingType.String) + public String mySQLColumnName = "username"; + + @Comment("Column for storing or checking players RealName ") + @Type(SettingType.String) + public String mySQLColumnRealName = "realname"; + + @Comment("Column for storing players passwords") + @Type(SettingType.String) + public String mySQLColumnPassword = "password"; + + @Comment("Column for storing players passwords salts") + @Type(SettingType.String) + public String mySQLColumnSalt = ""; + + @Comment("Column for storing players emails") + @Type(SettingType.String) + public String mySQLColumnEmail = "email"; + + @Comment("Column for storing if a player is logged in or not") + @Type(SettingType.String) + public String mySQLColumnLogged = "isLogged"; + + @Comment("Column for storing players ips") + @Type(SettingType.String) + public String mySQLColumnIp = "ip"; + + @Comment("Column for storing players lastlogins") + @Type(SettingType.String) + public String mySQLColumnLastLogin = "lastlogin"; + + @Comment("Column for storing player LastLocation - X") + @Type(SettingType.String) + public String mySQLColumnLastLocX = "x"; + + @Comment("Column for storing player LastLocation - Y") + @Type(SettingType.String) + public String mySQLColumnLastLocY = "y"; + + @Comment("Column for storing player LastLocation - Z") + @Type(SettingType.String) + public String mySQLColumnLastLocZ = "z"; + + @Comment("Column for storing player LastLocation - World Name") + @Type(SettingType.String) + public String mySQLColumnLastLocWorld = "world"; + + @Comment("Column for storing players groups") + @Type(SettingType.String) + public String mySQLColumnGroup = ""; + + @Comment("Enable this when you allow registration through a website") + @Type(SettingType.Boolean) + public boolean mySQLWebsite = false; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "database.yml"); + + private DatabaseSettings instance; + + public DatabaseSettings() + { + super(configFile); + instance = this; + } + + public DatabaseSettings getInstance() { + return instance; + } + + public void setInstance(DatabaseSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java new file mode 100644 index 000000000..d6cc36753 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -0,0 +1,83 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class EmailSettings extends CustomSetting { + + @Comment("Email SMTP server host") + @Type(SettingType.String) + public String mailSMTP = "smtp.gmail.com"; + + @Comment("Email SMTP server port") + @Type(SettingType.Int) + public int mailPort = 465; + + @Comment("Email account whose send the mail") + @Type(SettingType.String) + public String mailAccount = ""; + + @Comment("Email account password") + @Type(SettingType.String) + public String mailPassword = ""; + + @Comment("Random password length") + @Type(SettingType.Int) + public int recoveryPasswordLength = 8; + + @Comment("Mail Subject") + @Type(SettingType.String) + public String mailSubject = "Your new AuthMe password"; + + @Comment("Like maxRegPerIP but with email") + @Type(SettingType.Int) + public int maxRegPerEmail = 1; + + @Comment("Recall players to add an email ?") + @Type(SettingType.Boolean) + public boolean recallPlayers = false; + + @Comment("Delay in minute for the recall scheduler") + @Type(SettingType.Int) + public int delayRecall = 5; + + @Comment("Blacklist these domains for emails") + @Type(SettingType.StringList) + public List emailBlackListed = new ArrayList(); + + @Comment("Whitelist ONLY these domains for emails") + @Type(SettingType.StringList) + public List emailWhiteListed = new ArrayList(); + + @Comment("Do we need to send new password draw in an image ?") + @Type(SettingType.Boolean) + public boolean generateImage = false; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "emails.yml"); + + private EmailSettings instance; + + public EmailSettings() + { + super(configFile); + instance = this; + if (this.isFirstLaunch) + { + this.emailBlackListed.add("10minutemail.com"); + save(); + } + } + + public EmailSettings getInstance() { + return instance; + } + + public void setInstance(EmailSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java new file mode 100644 index 000000000..307c79062 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class HooksSettings extends CustomSetting { + + @Comment("Do we need to hook with multiverse for spawn checking?") + @Type(SettingType.Boolean) + public boolean multiverse = true; + + @Comment("Do we need to hook with BungeeCord ?") + @Type(SettingType.Boolean) + public boolean bungeecord = false; + + @Comment("Send player to this BungeeCord server after register/login") + @Type(SettingType.String) + public String sendPlayerTo = ""; + + @Comment("Do we need to disable Essentials SocialSpy on join?") + @Type(SettingType.Boolean) + public boolean disableSocialSpy = false; + + @Comment("Do we need to force /motd Essentials command on join?") + @Type(SettingType.Boolean) + public boolean useEssentialsMotd = false; + + @Comment("Do we need to cache custom Attributes?") + @Type(SettingType.Boolean) + public boolean customAttributes = false; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "hooks.yml"); + + private HooksSettings instance; + + public HooksSettings() + { + super(configFile); + instance = this; + } + + public HooksSettings getInstance() { + return instance; + } + + public void setInstance(HooksSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java new file mode 100644 index 000000000..e8e2d059c --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -0,0 +1,63 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class ProtectionSettings extends CustomSetting { + + @Comment("Enable some servers protection ( country based login, antibot )") + @Type(SettingType.Boolean) + public boolean enableProtection = false; + + @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", + "PLEASE USE QUOTES !"}) + @Type(SettingType.StringList) + public List countriesWhitelist = new ArrayList(); + + @Comment({"Countries not allowed to join the server and register", + "PLEASE USE QUOTES !"}) + @Type(SettingType.StringList) + public List countriesBlacklist = new ArrayList(); + + @Comment("Do we need to enable automatic antibot system?") + @Type(SettingType.Boolean) + public boolean enableAntiBot = false; + + @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") + @Type(SettingType.Int) + public int antiBotSensibility = 5; + + @Comment("Duration in minutes of the antibot automatic system") + @Type(SettingType.Int) + public int antiBotDuration = 10; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "protection.yml"); + + private ProtectionSettings instance; + + public ProtectionSettings() + { + super(configFile); + instance = this; + if (this.isFirstLaunch) + { + this.countriesWhitelist.add("US"); + this.countriesWhitelist.add("GB"); + this.countriesBlacklist.add("A1"); + save(); + } + } + + public ProtectionSettings getInstance() { + return instance; + } + + public void setInstance(ProtectionSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java new file mode 100644 index 000000000..3b85e2fb0 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -0,0 +1,62 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class PurgeSettings extends CustomSetting { + + @Comment("If enabled, AuthMe automatically purges old, unused accounts") + @Type(SettingType.Boolean) + public boolean useAutoPurge = false; + + @Comment("Number of Days an account become Unused") + @Type(SettingType.Int) + public int daysBeforeRemovePlayer = 60; + + @Comment("Do we need to remove the player.dat file during purge process?") + @Type(SettingType.Boolean) + public boolean removePlayerDat = false; + + @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") + @Type(SettingType.Boolean) + public boolean removeEssentialsFiles = false; + + @Comment("World where are players.dat stores") + @Type(SettingType.String) + public String defaultWorld = "world"; + + @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") + @Type(SettingType.Boolean) + public boolean removeLimiteCreativeInventories = false; + + @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") + @Type(SettingType.Boolean) + public boolean removeAntiXRayFile = false; + + @Comment("Do we need to remove permissions?") + @Type(SettingType.Boolean) + public boolean removePermissions = false; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "purge.yml"); + + private PurgeSettings instance; + + public PurgeSettings() + { + super(configFile); + instance = this; + } + + public PurgeSettings getInstance() { + return instance; + } + + public void setInstance(PurgeSettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java new file mode 100644 index 000000000..3166cbe26 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -0,0 +1,60 @@ +package fr.xephi.authme.settings.custom; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import fr.xephi.authme.settings.custom.annotations.Comment; +import fr.xephi.authme.settings.custom.annotations.Type; +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +public class SecuritySettings extends CustomSetting { + + @Comment({"Stop the server if we can't contact the sql database", + "Take care with this, if you set that to false,", + "AuthMe automatically disable and the server is not protected!"}) + @Type(SettingType.Boolean) + public boolean stopServerOnProblem = true; + + @Comment("/reload support") + @Type(SettingType.Boolean) + public boolean useReloadCommandSupport = true; + + @Comment("Remove Spam from Console ?") + @Type(SettingType.Boolean) + public boolean removeSpamFromConsole = false; + + @Comment("Remove Password from Console ?") + @Type(SettingType.Boolean) + public boolean removePasswordFromConsole = true; + + @Comment("Player need to put a captcha when he fails too lot the password") + @Type(SettingType.Boolean) + public boolean useCaptcha = false; + + @Comment("Max allowed tries before request a captcha") + @Type(SettingType.Int) + public int maxLoginTryBeforeCaptcha = 5; + + @Comment("Captcha length ") + @Type(SettingType.Int) + public int captchaLength = 5; + + private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "security.yml"); + + private SecuritySettings instance; + + public SecuritySettings() + { + super(configFile); + instance = this; + } + + public SecuritySettings getInstance() { + return instance; + } + + public void setInstance(SecuritySettings instance) { + this.instance = instance; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java b/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java new file mode 100644 index 000000000..d711a1943 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.settings.custom.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * Add a comment to a field value + * + * @author xephi59 + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Comment { + + public String[] value() default ""; +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java b/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java new file mode 100644 index 000000000..aae0af52b --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.settings.custom.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import fr.xephi.authme.settings.custom.annotations.Type.SettingType; + +/** +* +* Set the type of a field value +* +* @author xephi59 +* +*/ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Type { + + public enum SettingType { + String(0), + Int(1), + Boolean(2), + Double(3), + StringList(4), + Long(5); + + private int type; + + SettingType(int type) + { + this.type = type; + } + + public int getType() + { + return this.type; + } + } + + public SettingType value() default SettingType.String; +} From 7d41ccbc9ca92feb23c71dfe2ff2b76ce4c0d14f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 3 Jan 2016 15:22:32 +0100 Subject: [PATCH 02/26] Settings: use class constants for properties, create custom writer - Create Property class for defining config properties - Create logic for typed retrival of properties from YAML file - Add custom save method - Retain comments from Comment annotations in the classes - Write in a sorted order: first discovered properties are first written to config.yml - Adjust properties to reflect the current config.yml - Add sample tests for the retrieval and writing of properties with the new setup --- .../settings/custom/ConverterSettings.java | 50 ++--- .../authme/settings/custom/CustomSetting.java | 156 ---------------- .../settings/custom/DatabaseSettings.java | 120 ++++++------ .../authme/settings/custom/EmailSettings.java | 97 +++++----- .../authme/settings/custom/HooksSettings.java | 57 +++--- .../authme/settings/custom/NewSetting.java | 148 +++++++++++++++ .../settings/custom/ProtectionSettings.java | 71 +++----- .../authme/settings/custom/PurgeSettings.java | 65 +++---- .../settings/custom/SecuritySettings.java | 73 ++++---- .../settings/custom/annotations/Type.java | 43 ----- .../{annotations => domain}/Comment.java | 11 +- .../settings/custom/domain/Property.java | 100 ++++++++++ .../settings/custom/domain/PropertyType.java | 146 +++++++++++++++ .../settings/custom/domain/SettingsClass.java | 7 + .../settings/custom/propertymap/Node.java | 121 ++++++++++++ .../custom/propertymap/PropertyMap.java | 48 +++++ .../propertymap/PropertyMapComparator.java | 39 ++++ .../fr/xephi/authme/util/CollectionUtils.java | 14 ++ .../fr/xephi/authme/util/StringUtils.java | 8 + .../settings/custom/NewSettingTest.java | 111 +++++++++++ .../settings/custom/NewSettingsWriteTest.java | 42 +++++ src/test/resources/437-config-test.yml | 44 +++++ src/test/resources/437-write-test.yml | 172 ++++++++++++++++++ 23 files changed, 1225 insertions(+), 518 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/NewSetting.java delete mode 100644 src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java rename src/main/java/fr/xephi/authme/settings/custom/{annotations => domain}/Comment.java (62%) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/Property.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java create mode 100644 src/test/resources/437-config-test.yml create mode 100644 src/test/resources/437-write-test.yml diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java index 7cee6a521..c9f1ba99e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -1,46 +1,32 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class ConverterSettings extends CustomSetting { +public class ConverterSettings implements SettingsClass { @Comment("Rakamak file name") - @Type(SettingType.String) - public String rakamakFileName = "users.rak"; + public static final Property RAKAMAK_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.fileName", "users.rak"); - @Comment("Rakamak use Ip ?") - @Type(SettingType.Boolean) - public boolean rakamakeUseIP = false; + @Comment("Rakamak use IP?") + public static final Property RAKAMAK_USE_IP = + newProperty(BOOLEAN, "Converter.Rakamak.useIP", false); @Comment("Rakamak IP file name") - @Type(SettingType.String) - public String rakamakIPFileName = "UsersIp.rak"; + public static final Property RAKAMAK_IP_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.ipFileName", "UsersIp.rak"); @Comment("CrazyLogin database file name") - @Type(SettingType.String) - public String crazyLoginFileName = "accounts.db"; + public static final Property CRAZYLOGIN_FILE_NAME = + newProperty(STRING, "Converter.CrazyLogin.fileName", "accounts.db"); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "converter.yml"); + private ConverterSettings() { + } - private ConverterSettings instance; - - public ConverterSettings() - { - super(configFile); - instance = this; - } - - public ConverterSettings getInstance() { - return instance; - } - - public void setInstance(ConverterSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java b/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java deleted file mode 100644 index 27792efd9..000000000 --- a/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java +++ /dev/null @@ -1,156 +0,0 @@ -package fr.xephi.authme.settings.custom; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.List; - -import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.settings.CustomConfiguration; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; - -public class CustomSetting extends CustomConfiguration { - - private File configFile; - public boolean isFirstLaunch = false; - - public CustomSetting(File file) { - super(file); - this.configFile = file; - try { - if (!configFile.exists()) - { - isFirstLaunch = true; - configFile.createNewFile(); - } - else - { - load(); - loadValues(); - } - save(); - } catch (IOException e) - { - ConsoleLogger.writeStackTrace(e); - } - } - - @Override - public boolean reLoad() - { - boolean out = true; - if (!configFile.exists()) { - try { - configFile.createNewFile(); - save(); - } catch (IOException e) { - out = false; - } - } - if (out) - { - load(); - loadValues(); - } - return out; - } - - public void loadValues() - { - for (Field f : this.getClass().getDeclaredFields()) - { - if (!f.isAnnotationPresent(Type.class)) - continue; - f.setAccessible(true); - try { - switch (f.getAnnotation(Type.class).value()) - { - case Boolean: - f.setBoolean(this, this.getBoolean(f.getName())); - break; - case Double: - f.setDouble(this, this.getDouble(f.getName())); - break; - case Int: - f.setInt(this, this.getInt(f.getName())); - break; - case Long: - f.setLong(this, this.getLong(f.getName())); - break; - case String: - f.set(this, this.getString(f.getName())); - break; - case StringList: - f.set(this, this.getStringList(f.getName())); - break; - default: - break; - } - } catch (Exception e) - { - ConsoleLogger.writeStackTrace(e); - } - } - } - - @Override - public void save() - { - FileWriter writer = null; - try { - writer = new FileWriter(configFile); - writer.write(""); - for (Field f : this.getClass().getDeclaredFields()) - { - if (!f.isAnnotationPresent(Comment.class)) - continue; - if (!f.isAnnotationPresent(Type.class)) - continue; - for (String s : f.getAnnotation(Comment.class).value()) - { - writer.append("# " + s + "\n"); - } - writer.append(f.getName() + ": "); - switch (f.getAnnotation(Type.class).value()) - { - case Boolean: - writer.append(f.getBoolean(this) ? "true" : "false"); - break; - case Double: - writer.append("" + f.getDouble(this)); - break; - case Int: - writer.append("" + f.getInt(this)); - break; - case String: - writer.append("'" + f.get(this).toString() + "'"); - break; - case StringList: - @SuppressWarnings("unchecked") - List list = (List) f.get(this); - writer.append("\n"); - if (list.isEmpty()) - writer.write("[]"); - else - for (String s : list) - writer.append(" - '" + s + "'\n"); - break; - case Long: - writer.append("" + f.getLong(this)); - break; - default: - break; - - } - writer.append("\n"); - writer.flush(); - } - writer.close(); - } catch (Exception e) { - ConsoleLogger.writeStackTrace(e); - } - } - -} diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index c3c20c6bd..26e36310f 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -1,121 +1,109 @@ package fr.xephi.authme.settings.custom; -import java.io.File; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class DatabaseSettings extends CustomSetting { +public class DatabaseSettings implements SettingsClass { @Comment({"What type of database do you want to use?", "Valid values: sqlite, mysql"}) - @Type(SettingType.String) - public String backend = "sqlite"; + public static final Property BACKEND = + newProperty(STRING, "DataSource.backend", "sqlite"); @Comment("Enable database caching, should improve database performance") - @Type(SettingType.Boolean) - public boolean caching = true; + public static final Property USE_CACHING = + newProperty(BOOLEAN, "DataSource.caching", true); @Comment("Database host address") - @Type(SettingType.String) - public String mySQLHost = "127.0.0.1"; + public static final Property MYSQL_HOST = + newProperty(STRING, "DataSource.mySQLHost", "127.0.0.1"); @Comment("Database port") - @Type(SettingType.String) - public String mySQLPort = "3306"; + public static final Property MYSQL_PORT = + newProperty(STRING, "DataSource.mySQLPort", "3306"); @Comment("Username about Database Connection Infos") - @Type(SettingType.String) - public String mySQLUsername = "authme"; + public static final Property MYSQL_USERNAME = + newProperty(STRING, "DataSource.mySQLUsername", "authme"); @Comment("Password about Database Connection Infos") - @Type(SettingType.String) - public String mySQLPassword = "12345"; + public static final Property MYSQL_PASSWORD = + newProperty(STRING, "DataSource.mySQLPassword", "123456"); @Comment("Database Name, use with converters or as SQLITE database name") - @Type(SettingType.String) - public String mySQLDatabase = "authme"; + public static final Property MYSQL_DATABASE = + newProperty(STRING, "DataSource.mySQLDatabase", "authme"); @Comment("Table of the database") - @Type(SettingType.String) - public String mySQLTablename = "authme"; + public static final Property MYSQL_TABLE = + newProperty(STRING, "DataSource.mySQLTablename", "authme"); @Comment("Column of IDs to sort data") - @Type(SettingType.String) - public String mySQLColumnId = "id"; + public static final Property MYSQL_COL_ID = + newProperty(STRING, "DataSource.mySQLColumnId", "id"); @Comment("Column for storing or checking players nickname") - @Type(SettingType.String) - public String mySQLColumnName = "username"; + public static final Property MYSQL_COL_NAME = + newProperty(STRING, "DataSource.mySQLColumnName", "username"); @Comment("Column for storing or checking players RealName ") - @Type(SettingType.String) - public String mySQLColumnRealName = "realname"; + public static final Property MYSQL_COL_REALNAME = + newProperty(STRING, "DataSource.mySQLRealName", "realname"); @Comment("Column for storing players passwords") - @Type(SettingType.String) - public String mySQLColumnPassword = "password"; + public static final Property MYSQL_COL_PASSWORD = + newProperty(STRING, "DataSource.mySQLColumnPassword", "password"); @Comment("Column for storing players passwords salts") - @Type(SettingType.String) - public String mySQLColumnSalt = ""; + public static final Property MYSQL_COL_SALT = + newProperty(STRING, "ExternalBoardOptions.mySQLColumnSalt", ""); @Comment("Column for storing players emails") - @Type(SettingType.String) - public String mySQLColumnEmail = "email"; + public static final Property MYSQL_COL_EMAIL = + newProperty(STRING, "DataSource.mySQLColumnEmail", "email"); @Comment("Column for storing if a player is logged in or not") - @Type(SettingType.String) - public String mySQLColumnLogged = "isLogged"; + public static final Property MYSQL_COL_ISLOGGED = + newProperty(STRING, "DataSource.mySQLColumnLogged", "isLogged"); @Comment("Column for storing players ips") - @Type(SettingType.String) - public String mySQLColumnIp = "ip"; + public static final Property MYSQL_COL_IP = + newProperty(STRING, "DataSource.mySQLColumnIp", "ip"); @Comment("Column for storing players lastlogins") - @Type(SettingType.String) - public String mySQLColumnLastLogin = "lastlogin"; + public static final Property MYSQL_COL_LASTLOGIN = + newProperty(STRING, "DataSource.mySQLColumnLastLogin", "lastlogin"); @Comment("Column for storing player LastLocation - X") - @Type(SettingType.String) - public String mySQLColumnLastLocX = "x"; + public static final Property MYSQL_COL_LASTLOC_X = + newProperty(STRING, "DataSource.mySQLlastlocX", "x"); @Comment("Column for storing player LastLocation - Y") - @Type(SettingType.String) - public String mySQLColumnLastLocY = "y"; + public static final Property MYSQL_COL_LASTLOC_Y = + newProperty(STRING, "DataSource.mySQLlastlocY", "y"); @Comment("Column for storing player LastLocation - Z") - @Type(SettingType.String) - public String mySQLColumnLastLocZ = "z"; + public static final Property MYSQL_COL_LASTLOC_Z = + newProperty(STRING, "DataSource.mySQLlastlocZ", "z"); @Comment("Column for storing player LastLocation - World Name") - @Type(SettingType.String) - public String mySQLColumnLastLocWorld = "world"; + public static final Property MYSQL_COL_LASTLOC_WORLD = + newProperty(STRING, "DataSource.mySQLlastlocWorld", "world"); @Comment("Column for storing players groups") - @Type(SettingType.String) - public String mySQLColumnGroup = ""; + public static final Property MYSQL_COL_GROUP = + newProperty(STRING, "ExternalBoardOptions.mySQLColumnGroup", ""); @Comment("Enable this when you allow registration through a website") - @Type(SettingType.Boolean) - public boolean mySQLWebsite = false; + public static final Property MYSQL_WEBSITE = + newProperty(BOOLEAN, "DataSource.mySQLWebsite", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "database.yml"); - - private DatabaseSettings instance; - - public DatabaseSettings() - { - super(configFile); - instance = this; + private DatabaseSettings() { } - public DatabaseSettings getInstance() { - return instance; - } - - public void setInstance(DatabaseSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java index d6cc36753..f53d9549d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -1,83 +1,72 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; + import java.util.List; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; -public class EmailSettings extends CustomSetting { +public class EmailSettings implements SettingsClass { @Comment("Email SMTP server host") - @Type(SettingType.String) - public String mailSMTP = "smtp.gmail.com"; + public static final Property SMTP_HOST = + newProperty(STRING, "Email.mailSMTP", "smtp.gmail.com"); @Comment("Email SMTP server port") - @Type(SettingType.Int) - public int mailPort = 465; + public static final Property SMTP_PORT = + newProperty(INTEGER, "Email.mailPort", 465); - @Comment("Email account whose send the mail") - @Type(SettingType.String) - public String mailAccount = ""; + @Comment("Email account which sends the mails") + public static final Property MAIL_ACCOUNT = + newProperty(STRING, "Email.mailAccount", ""); @Comment("Email account password") - @Type(SettingType.String) - public String mailPassword = ""; + public static final Property MAIL_PASSWORD = + newProperty(STRING, "Email.mailPassword", ""); - @Comment("Random password length") - @Type(SettingType.Int) - public int recoveryPasswordLength = 8; + @Comment("Recovery password length") + public static final Property RECOVERY_PASSWORD_LENGTH = + newProperty(INTEGER, "Email.RecoveryPasswordLength", 8); @Comment("Mail Subject") - @Type(SettingType.String) - public String mailSubject = "Your new AuthMe password"; + public static final Property RECOVERY_MAIL_SUBJECT = + newProperty(STRING, "Email.mailSubject", "Your new AuthMe password"); @Comment("Like maxRegPerIP but with email") - @Type(SettingType.Int) - public int maxRegPerEmail = 1; + public static final Property MAX_REG_PER_EMAIL = + newProperty(INTEGER, "Email.maxRegPerEmail", 1); - @Comment("Recall players to add an email ?") - @Type(SettingType.Boolean) - public boolean recallPlayers = false; + @Comment("Recall players to add an email?") + public static final Property RECALL_PLAYERS = + newProperty(BOOLEAN, "Email.recallPlayers", false); @Comment("Delay in minute for the recall scheduler") - @Type(SettingType.Int) - public int delayRecall = 5; + public static final Property DELAY_RECALL = + newProperty(INTEGER, "Email.delayRecall", 5); @Comment("Blacklist these domains for emails") - @Type(SettingType.StringList) - public List emailBlackListed = new ArrayList(); + public static final Property> DOMAIN_BLACKLIST = + newProperty(STRING_LIST, "Email.emailBlacklisted", "10minutemail.com"); @Comment("Whitelist ONLY these domains for emails") - @Type(SettingType.StringList) - public List emailWhiteListed = new ArrayList(); + public static final Property> DOMAIN_WHITELIST = + newProperty(STRING_LIST, "Email.emailWhitelisted"); - @Comment("Do we need to send new password draw in an image ?") - @Type(SettingType.Boolean) - public boolean generateImage = false; + @Comment("Send the new password drawn in an image?") + public static final Property PASSWORD_AS_IMAGE = + newProperty(BOOLEAN, "Email.generateImage", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "emails.yml"); + @Comment("The OAuth2 token") + public static final Property OAUTH2_TOKEN = + newProperty(STRING, "Email.emailOauth2Token", ""); - private EmailSettings instance; + private EmailSettings() { + } - public EmailSettings() - { - super(configFile); - instance = this; - if (this.isFirstLaunch) - { - this.emailBlackListed.add("10minutemail.com"); - save(); - } - } - - public EmailSettings getInstance() { - return instance; - } - - public void setInstance(EmailSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index 307c79062..6f75eb247 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -1,54 +1,39 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.PropertyType; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -public class HooksSettings extends CustomSetting { +public class HooksSettings implements SettingsClass { @Comment("Do we need to hook with multiverse for spawn checking?") - @Type(SettingType.Boolean) - public boolean multiverse = true; + public static final Property MULTIVERSE = + newProperty(PropertyType.BOOLEAN, "Hooks.multiverse", true); - @Comment("Do we need to hook with BungeeCord ?") - @Type(SettingType.Boolean) - public boolean bungeecord = false; + @Comment("Do we need to hook with BungeeCord?") + public static final Property BUNGEECORD = + newProperty(PropertyType.BOOLEAN, "Hooks.bungeecord", false); @Comment("Send player to this BungeeCord server after register/login") - @Type(SettingType.String) - public String sendPlayerTo = ""; + public static final Property BUNGEECORD_SERVER = + newProperty(PropertyType.STRING, "bungeecord.server", ""); @Comment("Do we need to disable Essentials SocialSpy on join?") - @Type(SettingType.Boolean) - public boolean disableSocialSpy = false; + public static final Property DISABLE_SOCIAL_SPY = + newProperty(PropertyType.BOOLEAN, "Hooks.disableSocialSpy", false); @Comment("Do we need to force /motd Essentials command on join?") - @Type(SettingType.Boolean) - public boolean useEssentialsMotd = false; + public static final Property USE_ESSENTIALS_MOTD = + newProperty(PropertyType.BOOLEAN, "Hooks.useEssentialsMotd", false); @Comment("Do we need to cache custom Attributes?") - @Type(SettingType.Boolean) - public boolean customAttributes = false; + public static final Property CACHE_CUSTOM_ATTRIBUTES = + newProperty(PropertyType.BOOLEAN, "Hooks.customAttributes", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "hooks.yml"); + private HooksSettings() { + } - private HooksSettings instance; - - public HooksSettings() - { - super(configFile); - instance = this; - } - - public HooksSettings getInstance() { - return instance; - } - - public void setInstance(HooksSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java new file mode 100644 index 000000000..b64d2f2ec --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -0,0 +1,148 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.custom.propertymap.PropertyMap; +import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * The new settings manager. + */ +public class NewSetting { + + private static final List> CONFIGURATION_CLASSES = Arrays.asList( + ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); + + private static final int YAML_INDENTATION = 4; + + private File file; + private YamlConfiguration configuration; + + public NewSetting(File file) { + this.configuration = YamlConfiguration.loadConfiguration(file); + this.file = file; + } + + // TODO: No way of passing just a YamlConfiguration object (later on for mocking purposes) ? + // If not, best is probably to keep this constructor as package-private with @VisibleForTesting + // but it's not a satisfying solution + public NewSetting(YamlConfiguration yamlConfiguration, String file) { + this.configuration = yamlConfiguration; + this.file = new File(file); + } + + public T getOption(Property property) { + return property.getFromFile(configuration); + } + + public void save() { + PropertyMap properties = getAllPropertyFields(); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(""); + + // Contains all but the last node of the setting, e.g. [DataSource, mysql] for "DataSource.mysql.username" + List currentPath = new ArrayList<>(); + for (Map.Entry entry : properties.entrySet()) { + Property property = entry.getKey(); + + // Handle properties + List propertyPath = Arrays.asList(property.getPath().split("\\.")); + List commonPathParts = CollectionUtils.filterCommonStart( + currentPath, propertyPath.subList(0, propertyPath.size() - 1)); + List newPathParts = CollectionUtils.getRange(propertyPath, commonPathParts.size()); + + if (commonPathParts.isEmpty()) { + writer.append("\n"); + } + + int indentationLevel = commonPathParts.size(); + if (newPathParts.size() > 1) { + for (String path : newPathParts.subList(0, newPathParts.size() - 1)) { + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(path) + .append(": "); + ++indentationLevel; + } + } + for (String comment : entry.getValue()) { + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append("# ") + .append(comment); + } + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(CollectionUtils.getRange(newPathParts, newPathParts.size() - 1).get(0)) + .append(": "); + + List yamlLines = property.formatValueAsYaml(configuration); + String delim = ""; + for (String yamlLine : yamlLines) { + writer.append(delim).append(yamlLine); + delim = "\n" + StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION); + } + + currentPath = propertyPath.subList(0, propertyPath.size() - 1); + } + writer.flush(); + writer.close(); + } catch (IOException e) { + ConsoleLogger.showError("Could not save config file - " + StringUtils.formatException(e)); + ConsoleLogger.writeStackTrace(e); + } + } + + private static PropertyMap getAllPropertyFields() { + PropertyMap properties = new PropertyMap(); + for (Class clazz : CONFIGURATION_CLASSES) { + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + Property property = getFieldIfRelevant(field); + if (property != null) { + properties.put(property, getCommentsForField(field)); + } + } + } + return properties; + } + + private static String[] getCommentsForField(Field field) { + if (field.isAnnotationPresent(Comment.class)) { + return field.getAnnotation(Comment.class).value(); + } + return new String[0]; + } + + private static Property getFieldIfRelevant(Field field) { + field.setAccessible(true); + if (field.isAccessible() && Property.class.equals(field.getType()) && Modifier.isStatic(field.getModifiers())) { + try { + return (Property) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not fetch field '" + field.getName() + "' from class '" + + field.getDeclaringClass().getSimpleName() + "': " + StringUtils.formatException(e)); + } + } + return null; + } + + + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java index e8e2d059c..9f5daf952 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -1,63 +1,46 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; + import java.util.List; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; -public class ProtectionSettings extends CustomSetting { - @Comment("Enable some servers protection ( country based login, antibot )") - @Type(SettingType.Boolean) - public boolean enableProtection = false; +public class ProtectionSettings implements SettingsClass { + + @Comment("Enable some servers protection (country based login, antibot)") + public static final Property ENABLE_PROTECTION = + newProperty(BOOLEAN, "Protection.enableProtection", false); @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", - "PLEASE USE QUOTES !"}) - @Type(SettingType.StringList) - public List countriesWhitelist = new ArrayList(); + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_WHITELIST = + newProperty(STRING_LIST, "Protection.countries", "US", "GB", "A1"); @Comment({"Countries not allowed to join the server and register", - "PLEASE USE QUOTES !"}) - @Type(SettingType.StringList) - public List countriesBlacklist = new ArrayList(); + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_BLACKLIST = + newProperty(STRING_LIST, "Protection.countriesBlacklist"); @Comment("Do we need to enable automatic antibot system?") - @Type(SettingType.Boolean) - public boolean enableAntiBot = false; + public static final Property ENABLE_ANTIBOT = + newProperty(BOOLEAN, "Protection.enableAntiBot", false); @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") - @Type(SettingType.Int) - public int antiBotSensibility = 5; + public static final Property ANTIBOT_SENSIBILITY = + newProperty(INTEGER, "Protection.antiBotSensibility", 5); @Comment("Duration in minutes of the antibot automatic system") - @Type(SettingType.Int) - public int antiBotDuration = 10; + public static final Property ANTIBOT_DURATION = + newProperty(INTEGER, "Protection.antiBotDuration", 10); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "protection.yml"); + private ProtectionSettings() { + } - private ProtectionSettings instance; - - public ProtectionSettings() - { - super(configFile); - instance = this; - if (this.isFirstLaunch) - { - this.countriesWhitelist.add("US"); - this.countriesWhitelist.add("GB"); - this.countriesBlacklist.add("A1"); - save(); - } - } - - public ProtectionSettings getInstance() { - return instance; - } - - public void setInstance(ProtectionSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java index 3b85e2fb0..c46e86482 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -1,62 +1,49 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class PurgeSettings extends CustomSetting { +public class PurgeSettings implements SettingsClass { @Comment("If enabled, AuthMe automatically purges old, unused accounts") - @Type(SettingType.Boolean) - public boolean useAutoPurge = false; + public static final Property USE_AUTO_PURGE = + newProperty(BOOLEAN, "Purge.useAutoPurge", false); @Comment("Number of Days an account become Unused") - @Type(SettingType.Int) - public int daysBeforeRemovePlayer = 60; + public static final Property DAYS_BEFORE_REMOVE_PLAYER = + newProperty(INTEGER, "Purge.daysBeforeRemovePlayer", 60); @Comment("Do we need to remove the player.dat file during purge process?") - @Type(SettingType.Boolean) - public boolean removePlayerDat = false; + public static final Property REMOVE_PLAYER_DAT = + newProperty(BOOLEAN, "Purge.removePlayerDat", false); @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") - @Type(SettingType.Boolean) - public boolean removeEssentialsFiles = false; + public static final Property REMOVE_ESSENTIALS_FILES = + newProperty(BOOLEAN, "Purge.removeEssentialsFiles", false); @Comment("World where are players.dat stores") - @Type(SettingType.String) - public String defaultWorld = "world"; + public static final Property DEFAULT_WORLD = + newProperty(STRING, "Purge.defaultWorld", "world"); @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") - @Type(SettingType.Boolean) - public boolean removeLimiteCreativeInventories = false; + public static final Property REMOVE_LIMITED_CREATIVE_INVENTORIES = + newProperty(BOOLEAN, "Purge.removeLimitedCreativesInventories", false); @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") - @Type(SettingType.Boolean) - public boolean removeAntiXRayFile = false; + public static final Property REMOVE_ANTI_XRAY_FILE = + newProperty(BOOLEAN, "Purge.removeAntiXRayFile", false); @Comment("Do we need to remove permissions?") - @Type(SettingType.Boolean) - public boolean removePermissions = false; + public static final Property REMOVE_PERMISSIONS = + newProperty(BOOLEAN, "Purge.removePermissions", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "purge.yml"); + private PurgeSettings() { + } - private PurgeSettings instance; - - public PurgeSettings() - { - super(configFile); - instance = this; - } - - public PurgeSettings getInstance() { - return instance; - } - - public void setInstance(PurgeSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index 3166cbe26..4b4638b3e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -1,60 +1,51 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; -public class SecuritySettings extends CustomSetting { +public class SecuritySettings implements SettingsClass { @Comment({"Stop the server if we can't contact the sql database", - "Take care with this, if you set that to false,", - "AuthMe automatically disable and the server is not protected!"}) - @Type(SettingType.Boolean) - public boolean stopServerOnProblem = true; + "Take care with this, if you set this to false,", + "AuthMe will automatically disable and the server won't be protected!"}) + public static final Property STOP_SERVER_ON_PROBLEM = + newProperty(BOOLEAN, "Security.SQLProblem.stopServer", true); @Comment("/reload support") - @Type(SettingType.Boolean) - public boolean useReloadCommandSupport = true; + public static final Property USE_RELOAD_COMMAND_SUPPORT = + newProperty(BOOLEAN, "Security.ReloadCommand.useReloadCommandSupport", true); - @Comment("Remove Spam from Console ?") - @Type(SettingType.Boolean) - public boolean removeSpamFromConsole = false; + @Comment("Remove spam from console?") + public static final Property REMOVE_SPAM_FROM_CONSOLE = + newProperty(BOOLEAN, "Security.console.noConsoleSpam", false); - @Comment("Remove Password from Console ?") - @Type(SettingType.Boolean) - public boolean removePasswordFromConsole = true; + @Comment("Remove passwords from console?") + public static final Property REMOVE_PASSWORD_FROM_CONSOLE = + newProperty(BOOLEAN, "Security.console.removePassword", true); @Comment("Player need to put a captcha when he fails too lot the password") - @Type(SettingType.Boolean) - public boolean useCaptcha = false; + public static final Property USE_CAPTCHA = + newProperty(BOOLEAN, "Security.captcha.useCaptcha", false); @Comment("Max allowed tries before request a captcha") - @Type(SettingType.Int) - public int maxLoginTryBeforeCaptcha = 5; + public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = + newProperty(INTEGER, "Security.captcha.maxLoginTry", 5); - @Comment("Captcha length ") - @Type(SettingType.Int) - public int captchaLength = 5; + @Comment("Captcha length") + public static final Property CAPTCHA_LENGTH = + newProperty(INTEGER, "Security.captcha.captchaLength", 5); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "security.yml"); + @Comment({"Kick players before stopping the server, that allow us to save position of players", + "and all needed information correctly without any corruption."}) + public static final Property KICK_PLAYERS_BEFORE_STOPPING = + newProperty(BOOLEAN, "Security.stop.kickPlayersBeforeStopping", true); - private SecuritySettings instance; + private SecuritySettings() { + } - public SecuritySettings() - { - super(configFile); - instance = this; - } - - public SecuritySettings getInstance() { - return instance; - } - - public void setInstance(SecuritySettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java b/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java deleted file mode 100644 index aae0af52b..000000000 --- a/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java +++ /dev/null @@ -1,43 +0,0 @@ -package fr.xephi.authme.settings.custom.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; - -/** -* -* Set the type of a field value -* -* @author xephi59 -* -*/ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Type { - - public enum SettingType { - String(0), - Int(1), - Boolean(2), - Double(3), - StringList(4), - Long(5); - - private int type; - - SettingType(int type) - { - this.type = type; - } - - public int getType() - { - return this.type; - } - } - - public SettingType value() default SettingType.String; -} diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java b/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java similarity index 62% rename from src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java rename to src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java index d711a1943..f664e424b 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.annotations; +package fr.xephi.authme.settings.custom.domain; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -6,15 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * - * Add a comment to a field value - * - * @author xephi59 - * + * Comment for properties which are also included in the YAML file upon saving. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment { - public String[] value() default ""; + String[] value() default ""; + } diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java new file mode 100644 index 000000000..e777f6ec9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java @@ -0,0 +1,100 @@ +package fr.xephi.authme.settings.custom.domain; + +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Properties (i.e. a setting that is read from the config.yml file). + */ +public class Property { + + private final PropertyType type; + private final String path; + private final T defaultValue; + + private Property(PropertyType type, String path, T defaultValue) { + Objects.requireNonNull(defaultValue); + this.type = type; + this.path = path; + this.defaultValue = defaultValue; + } + + public static Property newProperty(PropertyType type, String path, T defaultValue) { + return new Property<>(type, path, defaultValue); + } + + @SafeVarargs + public static Property> newProperty(PropertyType> type, String path, U... defaultValues) { + return new Property<>(type, path, Arrays.asList(defaultValues)); + } + + // ----- + // Overloaded convenience methods for specific types + // ----- + public static Property newProperty(String path, boolean defaultValue) { + return new Property<>(PropertyType.BOOLEAN, path, defaultValue); + } + + public static Property newProperty(String path, int defaultValue) { + return new Property<>(PropertyType.INTEGER, path, defaultValue); + } + + public static Property newProperty(String path, String defaultValue) { + return new Property<>(PropertyType.STRING, path, defaultValue); + } + + // ----- + // Hooks to the PropertyType methods + // ----- + + /** + * Get the property value from the given configuration. + * + * @param configuration The configuration to read the value from + * @return The value, or default if not present + */ + public T getFromFile(YamlConfiguration configuration) { + return type.getFromFile(this, configuration); + } + + /** + * Format the property value as YAML. + * + * @param configuration The configuration to read the value from + * @return The property value as YAML + */ + public List formatValueAsYaml(YamlConfiguration configuration) { + return type.asYaml(this, configuration); + } + + // ----- + // Trivial getters + // ----- + + /** + * Return the default value of the property. + * + * @return The default value + */ + public T getDefaultValue() { + return defaultValue; + } + + /** + * Return the property path (i.e. the node at which this property is located in the YAML file). + * + * @return The path + */ + public String getPath() { + return path; + } + + @Override + public String toString() { + return "Property '" + path + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java new file mode 100644 index 000000000..3680de3f9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java @@ -0,0 +1,146 @@ +package fr.xephi.authme.settings.custom.domain; + +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * Handles a certain property type and provides type-specific functionality. + * + * @param The value of the property + * @see Property + */ +public abstract class PropertyType { + + public static final PropertyType BOOLEAN = new BooleanProperty(); + public static final PropertyType DOUBLE = new DoubleProperty(); + public static final PropertyType INTEGER = new IntegerProperty(); + public static final PropertyType STRING = new StringProperty(); + public static final PropertyType> STRING_LIST = new StringListProperty(); + + /** + * Get the property's value from the given YAML configuration. + * + * @param property The property to retrieve + * @param configuration The YAML configuration to read from + * @return The read value, or the default value if absent + */ + public abstract T getFromFile(Property property, YamlConfiguration configuration); + + /** + * Return the property's value (or its default) as YAML. + * + * @param property The property to transform + * @param configuration The YAML configuration to read from + * @return The read value or its default in YAML format + */ + public List asYaml(Property property, YamlConfiguration configuration) { + return asYaml(getFromFile(property, configuration)); + } + + /** + * Transform the given value to YAML. + * + * @param value The value to transform + * @return The value as YAML + */ + protected abstract List asYaml(T value); + + + /** + * Boolean property. + */ + private static final class BooleanProperty extends PropertyType { + @Override + public Boolean getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getBoolean(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Boolean value) { + return asList(value ? "true" : "false"); + } + } + + /** + * Double property. + */ + private static final class DoubleProperty extends PropertyType { + @Override + public Double getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getDouble(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Double value) { + return asList(String.valueOf(value)); + } + } + + /** + * Integer property. + */ + private static final class IntegerProperty extends PropertyType { + @Override + public Integer getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getInt(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Integer value) { + return asList(String.valueOf(value)); + } + } + + /** + * String property. + */ + private static final class StringProperty extends PropertyType { + @Override + public String getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getString(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(String value) { + return asList(toYamlLiteral(value)); + } + + public static String toYamlLiteral(String str) { + // TODO: Need to handle new lines properly + return "'" + str.replace("'", "''") + "'"; + } + } + + /** + * String list property. + */ + private static final class StringListProperty extends PropertyType> { + @Override + public List getFromFile(Property> property, YamlConfiguration configuration) { + if (!configuration.isList(property.getPath())) { + return property.getDefaultValue(); + } + return configuration.getStringList(property.getPath()); + } + + @Override + protected List asYaml(List value) { + if (value.isEmpty()) { + return asList("[]"); + } + + List resultLines = new ArrayList<>(); + resultLines.add(""); // add + for (String entry : value) { + // TODO: StringProperty#toYamlLiteral will return List... + resultLines.add(" - " + StringProperty.toYamlLiteral(entry)); + } + return resultLines; + } + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java b/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java new file mode 100644 index 000000000..6820e12e1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java @@ -0,0 +1,7 @@ +package fr.xephi.authme.settings.custom.domain; + +/** + * Marker for classes that define {@link Property} fields. + */ +public interface SettingsClass { +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java new file mode 100644 index 000000000..4e621617e --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import java.util.ArrayList; +import java.util.List; + +/** + * Node class for building a tree from supplied String paths, ordered by insertion. + *

+ * For instance, consider a tree to which the following paths are inserted (in the given order): + * "animal.bird.duck", "color.yellow", "animal.rodent.rat", "animal.rodent.rabbit", "color.red". + * For such a tree:

    + *
  • "animal" (or any of its children) is sorted before "color" (or any of its children)
  • + *
  • "animal.bird" or any child thereof is sorted before "animal.rodent"
  • + *
  • "animal.rodent.rat" comes before "animal.rodent.rabbit"
  • + *
+ * + * @see PropertyMapComparator + */ +final class Node { + + private final String name; + private final List children; + + private Node(String name) { + this.name = name; + this.children = new ArrayList<>(); + } + + /** + * Create a root node, i.e. the starting node for a new tree. Call this method to create + * a new tree and always pass this root to other methods. + * + * @return The generated root node. + */ + public static Node createRoot() { + return new Node(null); + } + + /** + * Add a node to the root, creating any intermediary children that don't exist. + * + * @param root The root to add the path to + * @param fullPath The entire path of the node to add, separate by periods + */ + public static void addNode(Node root, String fullPath) { + String[] pathParts = fullPath.split("\\."); + Node parent = root; + for (String part : pathParts) { + Node child = parent.getChild(part); + if (child == null) { + child = new Node(part); + parent.children.add(child); + } + parent = child; + } + } + + /** + * Compare two nodes by this class' sorting behavior (insertion order). + * Note that this method assumes that both supplied paths exist in the tree. + * + * @param root The root of the tree + * @param fullPath1 The full path to the first node + * @param fullPath2 The full path to the second node + * @return The comparison result, in the same format as {@link Comparable#compareTo} + */ + public static int compare(Node root, String fullPath1, String fullPath2) { + String[] path1 = fullPath1.split("\\."); + String[] path2 = fullPath2.split("\\."); + + int commonCount = 0; + Node commonNode = root; + while (commonCount < path1.length && commonCount < path2.length + && path1[commonCount].equals(path2[commonCount]) && commonNode != null) { + commonNode = commonNode.getChild(path1[commonCount]); + ++commonCount; + } + + if (commonNode == null) { + System.err.println("Could not find common node for '" + fullPath1 + "' at index " + commonCount); + return fullPath1.compareTo(fullPath2); // fallback + } else if (commonCount >= path1.length || commonCount >= path2.length) { + return Integer.compare(path1.length, path2.length); + } + int child1Index = commonNode.getChildIndex(path1[commonCount]); + int child2Index = commonNode.getChildIndex(path2[commonCount]); + return Integer.compare(child1Index, child2Index); + } + + private Node getChild(String name) { + for (Node child : children) { + if (child.name.equals(name)) { + return child; + } + } + return null; + } + + /** + * Return the child's index, i.e. the position at which it was inserted to its parent. + * + * @param name The name of the node + * @return The insertion index + */ + private int getChildIndex(String name) { + int i = 0; + for (Node child : children) { + if (child.name.equals(name)) { + return i; + } + ++i; + } + return -1; + } + + @Override + public String toString() { + return "Node '" + name + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java new file mode 100644 index 000000000..e539db7ab --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import fr.xephi.authme.settings.custom.domain.Property; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Class wrapping a {@code Map} for storing properties and their associated + * comments with custom ordering. + * + * @see PropertyMapComparator for details about the map's order + */ +public class PropertyMap { + + private Map propertyMap; + private PropertyMapComparator comparator; + + /** + * Create a new property map. + */ + public PropertyMap() { + comparator = new PropertyMapComparator(); + propertyMap = new TreeMap<>(comparator); + } + + /** + * Add a new property to the map. + * + * @param property The property to add + * @param comments The comments associated to the property + */ + public void put(Property property, String[] comments) { + comparator.add(property); + propertyMap.put(property, comments); + } + + /** + * Return the entry set of the map. + * + * @return The entry set + */ + public Set> entrySet() { + return propertyMap.entrySet(); + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java new file mode 100644 index 000000000..9b07070bc --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import fr.xephi.authme.settings.custom.domain.Property; + +import java.util.Comparator; + +/** + * Custom comparator for {@link PropertyMap}. It guarantees that the map's entries: + *
    + *
  • are grouped by path, e.g. all "DataSource.mysql" properties are together, and "DataSource.mysql" properties + * are within the broader "DataSource" group.
  • + *
  • are ordered by insertion, e.g. if the first "DataSource" property is inserted before the first "security" + * property, then "DataSource" properties will come before the "security" ones.
  • + *
+ */ +final class PropertyMapComparator implements Comparator { + + private Node parent = Node.createRoot(); + + /** + * Method to call when adding a new property to the map (as to retain its insertion time). + * + * @param property The property that is being added + */ + public void add(Property property) { + Node.addNode(parent, property.getPath()); + } + + @Override + public int compare(Property p1, Property p2) { + return Node.compare(parent, p1.getPath(), p2.getPath()); + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + +} diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java index 5c3eaa7f7..519951655 100644 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ b/src/main/java/fr/xephi/authme/util/CollectionUtils.java @@ -3,6 +3,7 @@ package fr.xephi.authme.util; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; /** * Utils class for collections. @@ -58,4 +59,17 @@ public final class CollectionUtils { public static boolean isEmpty(Collection coll) { return coll == null || coll.isEmpty(); } + + public static List filterCommonStart(List coll1, List coll2) { + List commonStart = new ArrayList<>(); + int minSize = Math.min(coll1.size(), coll2.size()); + for (int i = 0; i < minSize; ++i) { + if (Objects.equals(coll1.get(i), coll2.get(i))) { + commonStart.add(coll1.get(i)); + } else { + break; + } + } + return commonStart; + } } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index 9ae0ec0fd..2ee243819 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -117,4 +117,12 @@ public final class StringUtils { return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); } + public static String repeat(String str, int times) { + StringBuilder sb = new StringBuilder(str.length() * times); + for (int i = 0; i < times; ++i) { + sb.append(str); + } + return sb.toString(); + } + } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java new file mode 100644 index 000000000..39a5062c8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -0,0 +1,111 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.PropertyType; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.net.URL; +import java.util.List; + +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; + +public class NewSettingTest { + + private static final String CONFIG_FILE = "437-config-test.yml"; + + @Test + public void shouldReturnIntegerFromFile() { + // given + YamlConfiguration file = mock(YamlConfiguration.class); + Property config = TestConfiguration.DURATION_IN_SECONDS; + given(file.getInt("test.duration", 4)).willReturn(18); + NewSetting settings = new NewSetting(file, "conf.txt"); + + // when + int retrieve = settings.getOption(config); + + // then + assertThat(retrieve, equalTo(18)); + } + + @Test + public void shouldLoadAllConfigs() { + // given + YamlConfiguration file = mock(YamlConfiguration.class); + + given(file.getString(anyString(), anyString())).willAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (String) invocation.getArguments()[1]; + } + }); + + given(file.getInt(eq(EmailSettings.RECOVERY_PASSWORD_LENGTH.getPath()), anyInt())) + .willReturn(20); + given(file.getBoolean(eq(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE.getPath()), anyBoolean())) + .willReturn(false); + + // when + NewSetting settings = new NewSetting(file, "conf.txt"); + + // then + // Expect the value we told the YAML mock to return: + assertThat(settings.getOption(EmailSettings.RECOVERY_PASSWORD_LENGTH), equalTo(20)); + // Expect the default: + assertThat(settings.getOption(EmailSettings.SMTP_HOST), equalTo(EmailSettings.SMTP_HOST.getDefaultValue())); + // Expect the value we told the YAML mock to return: + assertThat(settings.getOption(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE), equalTo(false)); + } + + @Test + public void executeIntegrationTest() { + // given + YamlConfiguration yamlFile = YamlConfiguration.loadConfiguration(getConfigFile()); + NewSetting settings = new NewSetting(yamlFile, "conf.txt"); + + // when + int result = settings.getOption(TestConfiguration.DURATION_IN_SECONDS); + String systemName = settings.getOption(TestConfiguration.SYSTEM_NAME); + String helpHeader = settings.getOption(newProperty("settings.helpHeader", "")); + List unsafePasswords = settings.getOption( + newProperty(PropertyType.STRING_LIST, "Security.unsafePasswords")); + + // then + assertThat(result, equalTo(22)); + assertThat(systemName, equalTo(TestConfiguration.SYSTEM_NAME.getDefaultValue())); + assertThat(helpHeader, equalTo("AuthMeReloaded")); + assertThat(unsafePasswords, contains("123456", "qwerty", "54321")); + } + + private File getConfigFile() { + URL url = getClass().getClassLoader().getResource(CONFIG_FILE); + if (url == null) { + throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); + } + return new File(url.getFile()); + } + + private static class TestConfiguration { + + public static final Property DURATION_IN_SECONDS = + newProperty("test.duration", 4); + + public static final Property SYSTEM_NAME = + newProperty("test.systemName", "[TestDefaultValue]"); + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java new file mode 100644 index 000000000..1dadf75e6 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java @@ -0,0 +1,42 @@ +package fr.xephi.authme.settings.custom; + +import org.junit.Test; + +import java.io.File; +import java.net.URL; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test for the save() function of new settings + */ +public class NewSettingsWriteTest { + + private static final String CONFIG_FILE = "437-write-test.yml"; + + @Test + public void shouldWriteProperties() { + File file = getConfigFile(); + NewSetting setting = new NewSetting(file); + setting.save(); + + // assert that we can load the file again -- i.e. that it's valid YAML! + NewSetting newSetting = new NewSetting(file); + assertThat(newSetting.getOption(SecuritySettings.CAPTCHA_LENGTH), + equalTo(SecuritySettings.CAPTCHA_LENGTH.getDefaultValue())); + assertThat(newSetting.getOption(ProtectionSettings.COUNTRIES_BLACKLIST), + equalTo(ProtectionSettings.COUNTRIES_BLACKLIST.getDefaultValue())); + } + + + + private File getConfigFile() { + URL url = getClass().getClassLoader().getResource(CONFIG_FILE); + if (url == null) { + throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); + } + return new File(url.getFile()); + } + +} diff --git a/src/test/resources/437-config-test.yml b/src/test/resources/437-config-test.yml new file mode 100644 index 000000000..f0d9f11b9 --- /dev/null +++ b/src/test/resources/437-config-test.yml @@ -0,0 +1,44 @@ +test: + duration: 22 +DataSource: + # What type of database do you want to use? + # Valid values: sqlite, mysql + backend: sqlite + # Enable database caching, should improve database performance + caching: true +settings: + # The name shown in the help messages. + helpHeader: AuthMeReloaded + GameMode: + # ForceSurvivalMode to player when join ? + ForceSurvivalMode: false +Security: + SQLProblem: + # Stop the server if we can't contact the sql database + # Take care with this, if you set that to false, + # AuthMe automatically disable and the server is not protected! + stopServer: true + ReloadCommand: + # /reload support + useReloadCommandSupport: true + console: + # Remove spam console + noConsoleSpam: false + # Replace passwords in the console when player type a command like /login + removePassword: true + captcha: + # Player need to put a captcha when he fails too lot the password + useCaptcha: false + # Max allowed tries before request a captcha + maxLoginTry: 5 + # Captcha length + captchaLength: 5 + unsafePasswords: + - '123456' + - 'qwerty' + - '54321' +Email: + # Email SMTP server host + mailSMTP: smtp.gmail.com + # Email SMTP server port + mailPort: 465 diff --git a/src/test/resources/437-write-test.yml b/src/test/resources/437-write-test.yml new file mode 100644 index 000000000..de2b762f8 --- /dev/null +++ b/src/test/resources/437-write-test.yml @@ -0,0 +1,172 @@ + + +Converter: + Rakamak: + # Rakamak file name + fileName: 'users.rak' + # Rakamak use IP? + useIP: false + # Rakamak IP file name + ipFileName: 'UsersIp.rak' + CrazyLogin: + # CrazyLogin database file name + fileName: 'accounts.db' + +DataSource: + # What type of database do you want to use? + # Valid values: sqlite, mysql + backend: 'sqlite' + # Enable database caching, should improve database performance + caching: true + # Database host address + mySQLHost: '127.0.0.1' + # Database port + mySQLPort: '3306' + # Username about Database Connection Infos + mySQLUsername: 'authme' + # Password about Database Connection Infos + mySQLPassword: '123456' + # Database Name, use with converters or as SQLITE database name + mySQLDatabase: 'authme' + # Table of the database + mySQLTablename: 'authme' + # Column of IDs to sort data + mySQLColumnId: 'id' + # Column for storing or checking players nickname + mySQLColumnName: 'username' + # Column for storing or checking players RealName + mySQLRealName: 'realname' + # Column for storing players passwords + mySQLColumnPassword: 'password' + # Column for storing players emails + mySQLColumnEmail: 'email' + # Column for storing if a player is logged in or not + mySQLColumnLogged: 'email' + # Column for storing players ips + mySQLColumnIp: 'ip' + # Column for storing players lastlogins + mySQLColumnLastLogin: 'lastlogin' + # Column for storing player LastLocation - X + mySQLlastlocX: 'x' + # Column for storing player LastLocation - Y + mySQLlastlocY: 'y' + # Column for storing player LastLocation - Z + mySQLlastlocZ: 'z' + # Column for storing player LastLocation - World Name + mySQLlastlocWorld: 'world' + # Enable this when you allow registration through a website + mySQLWebsite: false + +ExternalBoardOptions: + # Column for storing players passwords salts + mySQLColumnSalt: '' + # Column for storing players groups + mySQLColumnGroup: '' + +Email: + # Email SMTP server host + mailSMTP: 'smtp.gmail.com' + # Email SMTP server port + mailPort: 465 + # Email account which sends the mails + mailAccount: '' + # Email account password + mailPassword: '' + # Recovery password length + RecoveryPasswordLength: 8 + # Mail Subject + mailSubject: 'Your new AuthMe password' + # Like maxRegPerIP but with email + maxRegPerEmail: 1 + # Recall players to add an email? + recallPlayers: false + # Delay in minute for the recall scheduler + delayRecall: 5 + # Blacklist these domains for emails + emailBlacklisted: + - '10minutemail.com' + # Whitelist ONLY these domains for emails + emailWhitelisted: [] + # Send the new password drawn in an image? + generateImage: false + # The OAuth2 token + emailOauth2Token: '' + +Hooks: + # Do we need to hook with multiverse for spawn checking? + multiverse: true + # Do we need to hook with BungeeCord? + bungeecord: false + # Do we need to disable Essentials SocialSpy on join? + disableSocialSpy: false + # Do we need to force /motd Essentials command on join? + useEssentialsMotd: false + # Do we need to cache custom Attributes? + customAttributes: false + +bungeecord: + # Send player to this BungeeCord server after register/login + server: '' + +Protection: + # Enable some servers protection (country based login, antibot) + enableProtection: false + # Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes + # PLEASE USE QUOTES! + countries: + - 'US' + - 'GB' + - 'A1' + # Countries not allowed to join the server and register + # PLEASE USE QUOTES! + countriesBlacklist: [] + # Do we need to enable automatic antibot system? + enableAntiBot: false + # Max number of player allowed to login in 5 secs before enable AntiBot system automatically + antiBotSensibility: 5 + # Duration in minutes of the antibot automatic system + antiBotDuration: 10 + +Purge: + # If enabled, AuthMe automatically purges old, unused accounts + useAutoPurge: false + # Number of Days an account become Unused + daysBeforeRemovePlayer: 60 + # Do we need to remove the player.dat file during purge process? + removePlayerDat: false + # Do we need to remove the Essentials/users/player.yml file during purge process? + removeEssentialsFiles: false + # World where are players.dat stores + defaultWorld: 'world' + # Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ? + removeLimitedCreativesInventories: false + # Do we need to remove the AntiXRayData/PlayerData/player file during purge process? + removeAntiXRayFile: false + # Do we need to remove permissions? + removePermissions: false + +Security: + SQLProblem: + # Stop the server if we can't contact the sql database + # Take care with this, if you set this to false, + # AuthMe will automatically disable and the server won't be protected! + stopServer: true + ReloadCommand: + # /reload support + useReloadCommandSupport: true + console: + # Remove spam from console? + noConsoleSpam: false + # Remove passwords from console? + removePassword: true + captcha: + # Player need to put a captcha when he fails too lot the password + useCaptcha: false + # Max allowed tries before request a captcha + maxLoginTry: 5 + # Captcha length + captchaLength: 5 + stop: + # Kick players before stopping the server, that allow us to save position of players + # and all needed information correctly without any corruption. + kickPlayersBeforeStopping: true From cb07b3df3db39f5bc7faddd92633ee1ca17dba1c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 4 Jan 2016 20:33:31 +0100 Subject: [PATCH 03/26] Create enum property type, trivial code simplification --- .../settings/custom/DatabaseSettings.java | 5 +-- .../authme/settings/custom/NewSetting.java | 19 ++++++---- .../settings/custom/domain/Property.java | 4 +++ .../settings/custom/domain/PropertyType.java | 36 +++++++++++++++++++ .../fr/xephi/authme/util/CollectionUtils.java | 14 ++++---- .../fr/xephi/authme/util/StringUtils.java | 8 ----- .../settings/custom/NewSettingTest.java | 3 ++ src/test/resources/437-config-test.yml | 2 +- 8 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index 26e36310f..d8c5d9912 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -1,5 +1,6 @@ package fr.xephi.authme.settings.custom; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.settings.custom.domain.Comment; import fr.xephi.authme.settings.custom.domain.Property; import fr.xephi.authme.settings.custom.domain.SettingsClass; @@ -12,8 +13,8 @@ public class DatabaseSettings implements SettingsClass { @Comment({"What type of database do you want to use?", "Valid values: sqlite, mysql"}) - public static final Property BACKEND = - newProperty(STRING, "DataSource.backend", "sqlite"); + public static final Property BACKEND = + newProperty(DataSource.DataSourceType.class, "DataSource.backend", DataSource.DataSourceType.SQLITE); @Comment("Enable database caching, should improve database performance") public static final Property USE_CACHING = diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java index b64d2f2ec..1b56cd0fd 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -28,8 +28,6 @@ public class NewSetting { ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); - private static final int YAML_INDENTATION = 4; - private File file; private YamlConfiguration configuration; @@ -75,7 +73,7 @@ public class NewSetting { if (newPathParts.size() > 1) { for (String path : newPathParts.subList(0, newPathParts.size() - 1)) { writer.append("\n") - .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(indent(indentationLevel)) .append(path) .append(": "); ++indentationLevel; @@ -83,12 +81,12 @@ public class NewSetting { } for (String comment : entry.getValue()) { writer.append("\n") - .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(indent(indentationLevel)) .append("# ") .append(comment); } writer.append("\n") - .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(indent(indentationLevel)) .append(CollectionUtils.getRange(newPathParts, newPathParts.size() - 1).get(0)) .append(": "); @@ -96,7 +94,7 @@ public class NewSetting { String delim = ""; for (String yamlLine : yamlLines) { writer.append(delim).append(yamlLine); - delim = "\n" + StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION); + delim = "\n" + indent(indentationLevel); } currentPath = propertyPath.subList(0, propertyPath.size() - 1); @@ -109,6 +107,15 @@ public class NewSetting { } } + private static String indent(int level) { + // YAML uses indentation of 4 spaces + StringBuilder sb = new StringBuilder(level * 4); + for (int i = 0; i < level; ++i) { + sb.append(" "); + } + return sb.toString(); + } + private static PropertyMap getAllPropertyFields() { PropertyMap properties = new PropertyMap(); for (Class clazz : CONFIGURATION_CLASSES) { diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java index e777f6ec9..89b9f9420 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java @@ -31,6 +31,10 @@ public class Property { return new Property<>(type, path, Arrays.asList(defaultValues)); } + public static > Property newProperty(Class clazz, String path, E defaultValue) { + return new Property<>(new PropertyType.EnumProperty<>(clazz), path, defaultValue); + } + // ----- // Overloaded convenience methods for specific types // ----- diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java index 3680de3f9..ac6d3d692 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java @@ -143,4 +143,40 @@ public abstract class PropertyType { } } + /** + * Enum property. + * @param The enum class + */ + static final class EnumProperty> extends PropertyType { + private Class clazz; + + public EnumProperty(Class clazz) { + this.clazz = clazz; + } + + @Override + public E getFromFile(Property property, YamlConfiguration configuration) { + String textValue = configuration.getString(property.getPath()); + if (textValue == null) { + return property.getDefaultValue(); + } + E mappedValue = mapToEnum(textValue); + return mappedValue != null ? mappedValue : property.getDefaultValue(); + } + + @Override + protected List asYaml(E value) { + return asList("'" + value + "'"); + } + + private E mapToEnum(String value) { + for (E entry : clazz.getEnumConstants()) { + if (entry.name().equalsIgnoreCase(value)) { + return entry; + } + } + return null; + } + } + } diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java index 519951655..13077547c 100644 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ b/src/main/java/fr/xephi/authme/util/CollectionUtils.java @@ -60,15 +60,13 @@ public final class CollectionUtils { return coll == null || coll.isEmpty(); } - public static List filterCommonStart(List coll1, List coll2) { + public static List filterCommonStart(List list1, List list2) { List commonStart = new ArrayList<>(); - int minSize = Math.min(coll1.size(), coll2.size()); - for (int i = 0; i < minSize; ++i) { - if (Objects.equals(coll1.get(i), coll2.get(i))) { - commonStart.add(coll1.get(i)); - } else { - break; - } + int minSize = Math.min(list1.size(), list2.size()); + int i = 0; + while (i < minSize && Objects.equals(list1.get(i), list2.get(i))) { + commonStart.add(list1.get(i)); + ++i; } return commonStart; } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index 2ee243819..9ae0ec0fd 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -117,12 +117,4 @@ public final class StringUtils { return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); } - public static String repeat(String str, int times) { - StringBuilder sb = new StringBuilder(str.length() * times); - for (int i = 0; i < times; ++i) { - sb.append(str); - } - return sb.toString(); - } - } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java index 39a5062c8..585f128ec 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.settings.custom; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.settings.custom.domain.Property; import fr.xephi.authme.settings.custom.domain.PropertyType; import org.bukkit.configuration.file.YamlConfiguration; @@ -83,12 +84,14 @@ public class NewSettingTest { String helpHeader = settings.getOption(newProperty("settings.helpHeader", "")); List unsafePasswords = settings.getOption( newProperty(PropertyType.STRING_LIST, "Security.unsafePasswords")); + DataSource.DataSourceType dataSourceType = settings.getOption(DatabaseSettings.BACKEND); // then assertThat(result, equalTo(22)); assertThat(systemName, equalTo(TestConfiguration.SYSTEM_NAME.getDefaultValue())); assertThat(helpHeader, equalTo("AuthMeReloaded")); assertThat(unsafePasswords, contains("123456", "qwerty", "54321")); + assertThat(dataSourceType, equalTo(DataSource.DataSourceType.MYSQL)); } private File getConfigFile() { diff --git a/src/test/resources/437-config-test.yml b/src/test/resources/437-config-test.yml index f0d9f11b9..5057bea92 100644 --- a/src/test/resources/437-config-test.yml +++ b/src/test/resources/437-config-test.yml @@ -3,7 +3,7 @@ test: DataSource: # What type of database do you want to use? # Valid values: sqlite, mysql - backend: sqlite + backend: mysql # Enable database caching, should improve database performance caching: true settings: From 204a564a9ab9397a4872dda6bc5d18de68f8f82d Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 5 Jan 2016 21:59:08 +0100 Subject: [PATCH 04/26] Move new configuration packages and add test for PropertyMap --- .../settings/custom/ConverterSettings.java | 12 ++--- .../settings/custom/DatabaseSettings.java | 12 ++--- .../authme/settings/custom/EmailSettings.java | 16 +++--- .../authme/settings/custom/HooksSettings.java | 10 ++-- .../authme/settings/custom/NewSetting.java | 8 +-- .../settings/custom/ProtectionSettings.java | 14 ++--- .../authme/settings/custom/PurgeSettings.java | 14 ++--- .../settings/custom/SecuritySettings.java | 12 ++--- .../settings/{custom => }/domain/Comment.java | 2 +- .../{custom => }/domain/Property.java | 2 +- .../{custom => }/domain/PropertyType.java | 2 +- .../{custom => }/domain/SettingsClass.java | 2 +- .../{custom => }/propertymap/Node.java | 2 +- .../{custom => }/propertymap/PropertyMap.java | 4 +- .../propertymap/PropertyMapComparator.java | 4 +- .../settings/custom/NewSettingTest.java | 6 +-- .../settings/propertymap/PropertyMapTest.java | 53 +++++++++++++++++++ 17 files changed, 114 insertions(+), 61 deletions(-) rename src/main/java/fr/xephi/authme/settings/{custom => }/domain/Comment.java (88%) rename src/main/java/fr/xephi/authme/settings/{custom => }/domain/Property.java (98%) rename src/main/java/fr/xephi/authme/settings/{custom => }/domain/PropertyType.java (99%) rename src/main/java/fr/xephi/authme/settings/{custom => }/domain/SettingsClass.java (68%) rename src/main/java/fr/xephi/authme/settings/{custom => }/propertymap/Node.java (98%) rename src/main/java/fr/xephi/authme/settings/{custom => }/propertymap/PropertyMap.java (90%) rename src/main/java/fr/xephi/authme/settings/{custom => }/propertymap/PropertyMapComparator.java (91%) create mode 100644 src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java index c9f1ba99e..f208727b7 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -1,12 +1,12 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class ConverterSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index d8c5d9912..d638fd8f0 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -1,13 +1,13 @@ package fr.xephi.authme.settings.custom; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class DatabaseSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java index f53d9549d..dcde50191 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -1,16 +1,16 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; import java.util.List; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; public class EmailSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index 6f75eb247..6e23c7398 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -1,11 +1,11 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.PropertyType; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.Property.newProperty; public class HooksSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java index 1b56cd0fd..138c37d3d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -1,10 +1,10 @@ package fr.xephi.authme.settings.custom; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.propertymap.PropertyMap; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; +import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java index 9f5daf952..b71a9589b 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -1,15 +1,15 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; import java.util.List; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; public class ProtectionSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java index c46e86482..e2e1c0290 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -1,13 +1,13 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; -import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class PurgeSettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index 4b4638b3e..c18e7e7ca 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -1,12 +1,12 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.settings.custom.domain.Comment; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; public class SecuritySettings implements SettingsClass { diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java b/src/main/java/fr/xephi/authme/settings/domain/Comment.java similarity index 88% rename from src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java rename to src/main/java/fr/xephi/authme/settings/domain/Comment.java index f664e424b..92713aeec 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Comment.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.domain; +package fr.xephi.authme.settings.domain; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java similarity index 98% rename from src/main/java/fr/xephi/authme/settings/custom/domain/Property.java rename to src/main/java/fr/xephi/authme/settings/domain/Property.java index 89b9f9420..71e111648 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.domain; +package fr.xephi.authme.settings.domain; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java similarity index 99% rename from src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java rename to src/main/java/fr/xephi/authme/settings/domain/PropertyType.java index ac6d3d692..dba0e9712 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.domain; +package fr.xephi.authme.settings.domain; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java b/src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java similarity index 68% rename from src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java rename to src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java index 6820e12e1..796505add 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java +++ b/src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.domain; +package fr.xephi.authme.settings.domain; /** * Marker for classes that define {@link Property} fields. diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java b/src/main/java/fr/xephi/authme/settings/propertymap/Node.java similarity index 98% rename from src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java rename to src/main/java/fr/xephi/authme/settings/propertymap/Node.java index 4e621617e..70d8ce239 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java +++ b/src/main/java/fr/xephi/authme/settings/propertymap/Node.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.propertymap; +package fr.xephi.authme.settings.propertymap; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java similarity index 90% rename from src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java rename to src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java index e539db7ab..934245a07 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java @@ -1,6 +1,6 @@ -package fr.xephi.authme.settings.custom.propertymap; +package fr.xephi.authme.settings.propertymap; -import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.domain.Property; import java.util.Map; import java.util.Set; diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java similarity index 91% rename from src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java rename to src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java index 9b07070bc..e646fb61d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java @@ -1,6 +1,6 @@ -package fr.xephi.authme.settings.custom.propertymap; +package fr.xephi.authme.settings.propertymap; -import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.domain.Property; import java.util.Comparator; diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java index 585f128ec..419d5917c 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -1,8 +1,8 @@ package fr.xephi.authme.settings.custom; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.settings.custom.domain.Property; -import fr.xephi.authme.settings.custom.domain.PropertyType; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -12,7 +12,7 @@ import java.io.File; import java.net.URL; import java.util.List; -import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.Property.newProperty; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java new file mode 100644 index 000000000..895b12e1c --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.settings.propertymap; + +import fr.xephi.authme.settings.domain.Property; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link PropertyMap}. + */ +public class PropertyMapTest { + + @Test + public void shouldKeepEntriesByInsertionAndGroup() { + // given + List paths = Arrays.asList("japan", "indonesia.jakarta", "japan.tokyo", "china.shanghai", "egypt.cairo", + "china.shenzhen", "china", "indonesia.jakarta.tugu", "egypt", "japan.nagoya", "japan.tokyo.taito"); + PropertyMap map = new PropertyMap(); + + // when + for (String path : paths) { + Property property = createPropertyWithPath(path); + map.put(property, new String[0]); + } + + // then + Set> entrySet = map.entrySet(); + List resultPaths = new ArrayList<>(entrySet.size()); + for (Map.Entry entry : entrySet) { + resultPaths.add(entry.getKey().getPath()); + } + + Assert.assertThat(resultPaths, contains("japan", "japan.tokyo", "japan.tokyo.taito", "japan.nagoya", + "indonesia.jakarta", "indonesia.jakarta.tugu", "china", "china.shanghai", "china.shenzhen", + "egypt", "egypt.cairo")); + } + + private static Property createPropertyWithPath(String path) { + Property property = mock(Property.class); + when(property.getPath()).thenReturn(path); + return property; + } +} From acda03bb40f4d6fd281f738b5a785703fd65750b Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 7 Jan 2016 21:38:03 +0100 Subject: [PATCH 05/26] #347 Create enum property + consistency tests --- .../settings/domain/EnumPropertyType.java | 44 ++++++++ .../authme/settings/domain/Property.java | 2 +- .../authme/settings/domain/PropertyType.java | 36 ------ .../custom/SettingsClassConsistencyTest.java | 106 ++++++++++++++++++ .../settings/domain/EnumPropertyTypeTest.java | 71 ++++++++++++ 5 files changed, 222 insertions(+), 37 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java create mode 100644 src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java diff --git a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java new file mode 100644 index 000000000..8455a893a --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * Enum property type. + * @param The enum class + */ +class EnumPropertyType> extends PropertyType { + + private Class clazz; + + public EnumPropertyType(Class clazz) { + this.clazz = clazz; + } + + @Override + public E getFromFile(Property property, YamlConfiguration configuration) { + String textValue = configuration.getString(property.getPath()); + if (textValue == null) { + return property.getDefaultValue(); + } + E mappedValue = mapToEnum(textValue); + return mappedValue != null ? mappedValue : property.getDefaultValue(); + } + + @Override + protected List asYaml(E value) { + return asList("'" + value + "'"); + } + + private E mapToEnum(String value) { + for (E entry : clazz.getEnumConstants()) { + if (entry.name().equalsIgnoreCase(value)) { + return entry; + } + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java index 71e111648..809f7a987 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -32,7 +32,7 @@ public class Property { } public static > Property newProperty(Class clazz, String path, E defaultValue) { - return new Property<>(new PropertyType.EnumProperty<>(clazz), path, defaultValue); + return new Property<>(new EnumPropertyType<>(clazz), path, defaultValue); } // ----- diff --git a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java index dba0e9712..e41965640 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -143,40 +143,4 @@ public abstract class PropertyType { } } - /** - * Enum property. - * @param The enum class - */ - static final class EnumProperty> extends PropertyType { - private Class clazz; - - public EnumProperty(Class clazz) { - this.clazz = clazz; - } - - @Override - public E getFromFile(Property property, YamlConfiguration configuration) { - String textValue = configuration.getString(property.getPath()); - if (textValue == null) { - return property.getDefaultValue(); - } - E mappedValue = mapToEnum(textValue); - return mappedValue != null ? mappedValue : property.getDefaultValue(); - } - - @Override - protected List asYaml(E value) { - return asList("'" + value + "'"); - } - - private E mapToEnum(String value) { - for (E entry : clazz.getEnumConstants()) { - if (entry.name().equalsIgnoreCase(value)) { - return entry; - } - } - return null; - } - } - } diff --git a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java new file mode 100644 index 000000000..c06ed11b1 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java @@ -0,0 +1,106 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Test for {@link SettingsClass} implementations. + */ +public class SettingsClassConsistencyTest { + + private static final String SETTINGS_FOLDER = "src/main/java/fr/xephi/authme/settings/custom"; + private static List> classes; + + @BeforeClass + public static void scanForSettingsClasses() { + File settingsFolder = new File(SETTINGS_FOLDER); + File[] filesInFolder = settingsFolder.listFiles(); + if (filesInFolder == null || filesInFolder.length == 0) { + throw new IllegalStateException("Could not read folder '" + SETTINGS_FOLDER + "'. Is it correct?"); + } + + classes = new ArrayList<>(); + for (File file : filesInFolder) { + Class clazz = getSettingsClassFromFile(file); + if (clazz != null) { + classes.add(clazz); + } + } + System.out.println("Found " + classes.size() + " SettingsClass implementations"); + } + + /** + * Make sure that all {@link Property} instances we define are in public, static, final fields. + */ + @Test + public void shouldHavePublicStaticFinalFields() { + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (Property.class.isAssignableFrom(field.getType())) { + String fieldName = "Field " + clazz.getSimpleName() + "#" + field.getName(); + assertThat(fieldName + "should be public, static, and final", + isValidConstantField(field), equalTo(true)); + } + } + } + } + + /** + * Make sure that no properties use the same path. + */ + @Test + public void shouldHaveUniquePaths() { + Set paths = new HashSet<>(); + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (Property.class.isAssignableFrom(field.getType())) { + Property property = + (Property) ReflectionTestUtils.getFieldValue(clazz, null, field.getName()); + if (paths.contains(property.getPath())) { + fail("Path '" + property.getPath() + "' should be used by only one constant"); + } + paths.add(property.getPath()); + } + } + } + } + + private static boolean isValidConstantField(Field field) { + int modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); + } + + private static Class getSettingsClassFromFile(File file) { + String fileName = file.getPath(); + String className = fileName + .substring("src/main/java/".length(), fileName.length() - ".java".length()) + .replace(File.separator, "."); + try { + Class clazz = SettingsClassConsistencyTest.class.getClassLoader().loadClass(className); + if (SettingsClass.class.isAssignableFrom(clazz)) { + return (Class) clazz; + } + return null; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not load class '" + className + "'", e); + } + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java new file mode 100644 index 000000000..1b9d39091 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java @@ -0,0 +1,71 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link EnumPropertyType}. + */ +public class EnumPropertyTypeTest { + + @Test + public void shouldReturnCorrectEnumValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn("Entry_B"); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_B)); + } + + @Test + public void shouldFallBackToDefaultForInvalidValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn("Bogus"); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_C)); + } + + @Test + public void shouldFallBackToDefaultForNonExistentValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn(null); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_C)); + } + + + private enum TestEnum { + + ENTRY_A, + + ENTRY_B, + + ENTRY_C + + } +} From 30db03837a32f3f365d8dbd57cbf6447a190117d Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 7 Jan 2016 21:58:28 +0100 Subject: [PATCH 06/26] #347 Add 'contains' method to PropertyType --- .../settings/domain/EnumPropertyType.java | 6 +++ .../authme/settings/domain/PropertyType.java | 4 ++ .../settings/domain/EnumPropertyTypeTest.java | 47 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java index 8455a893a..357067d8e 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java @@ -33,6 +33,12 @@ class EnumPropertyType> extends PropertyType { return asList("'" + value + "'"); } + @Override + public boolean contains(Property property, YamlConfiguration configuration) { + return super.contains(property, configuration) + && mapToEnum(configuration.getString(property.getPath())) != null; + } + private E mapToEnum(String value) { for (E entry : clazz.getEnumConstants()) { if (entry.name().equalsIgnoreCase(value)) { diff --git a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java index e41965640..b243ef657 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -49,6 +49,10 @@ public abstract class PropertyType { */ protected abstract List asYaml(T value); + protected boolean contains(Property property, YamlConfiguration configuration) { + return configuration.contains(property.getPath()); + } + /** * Boolean property. diff --git a/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java index 1b9d39091..461314e8d 100644 --- a/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java +++ b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java @@ -58,6 +58,53 @@ public class EnumPropertyTypeTest { assertThat(result, equalTo(TestEnum.ENTRY_C)); } + @Test + public void shouldReturnTrueForContainsCheck() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(true); + given(configuration.getString(property.getPath())).willReturn("ENTRY_B"); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void shouldReturnFalseForFileWithoutConfig() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(false); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldReturnFalseForUnknownValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(true); + given(configuration.getString(property.getPath())).willReturn("wrong value"); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(false)); + } + private enum TestEnum { From 69c225c8506ed18d2126a000cce68c0238d10ed0 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 8 Jan 2016 21:22:26 +0100 Subject: [PATCH 07/26] #347 Create tests and add check for missing settings in NewSetting --- .../authme/settings/custom/NewSetting.java | 71 ++----- .../custom/SettingsFieldRetriever.java | 67 +++++++ .../authme/settings/domain/Property.java | 10 + .../authme/settings/domain/PropertyType.java | 20 +- .../settings/propertymap/PropertyMap.java | 13 +- .../settings/custom/NewSettingsWriteTest.java | 1 - .../settings/domain/PropertyTypeTest.java | 182 ++++++++++++++++++ .../settings/propertymap/PropertyMapTest.java | 4 +- 8 files changed, 309 insertions(+), 59 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java create mode 100644 src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java index 138c37d3d..86a15b0d5 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -1,9 +1,8 @@ package fr.xephi.authme.settings.custom; +import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.settings.domain.Comment; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.settings.domain.SettingsClass; import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; @@ -12,8 +11,6 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -24,22 +21,21 @@ import java.util.Map; */ public class NewSetting { - private static final List> CONFIGURATION_CLASSES = Arrays.asList( - ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, - ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); - private File file; private YamlConfiguration configuration; public NewSetting(File file) { this.configuration = YamlConfiguration.loadConfiguration(file); this.file = file; + + PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); + if (!containsAllSettings(propertyMap)) { + save(propertyMap); + } } - // TODO: No way of passing just a YamlConfiguration object (later on for mocking purposes) ? - // If not, best is probably to keep this constructor as package-private with @VisibleForTesting - // but it's not a satisfying solution - public NewSetting(YamlConfiguration yamlConfiguration, String file) { + @VisibleForTesting + NewSetting(YamlConfiguration yamlConfiguration, String file) { this.configuration = yamlConfiguration; this.file = new File(file); } @@ -49,14 +45,16 @@ public class NewSetting { } public void save() { - PropertyMap properties = getAllPropertyFields(); + save(SettingsFieldRetriever.getAllPropertyFields()); + } + public void save(PropertyMap propertyMap) { try (FileWriter writer = new FileWriter(file)) { writer.write(""); // Contains all but the last node of the setting, e.g. [DataSource, mysql] for "DataSource.mysql.username" List currentPath = new ArrayList<>(); - for (Map.Entry entry : properties.entrySet()) { + for (Map.Entry, String[]> entry : propertyMap.entrySet()) { Property property = entry.getKey(); // Handle properties @@ -107,6 +105,15 @@ public class NewSetting { } } + private boolean containsAllSettings(PropertyMap propertyMap) { + for (Property property : propertyMap.keySet()) { + if (!property.isPresent(configuration)) { + return false; + } + } + return true; + } + private static String indent(int level) { // YAML uses indentation of 4 spaces StringBuilder sb = new StringBuilder(level * 4); @@ -116,40 +123,4 @@ public class NewSetting { return sb.toString(); } - private static PropertyMap getAllPropertyFields() { - PropertyMap properties = new PropertyMap(); - for (Class clazz : CONFIGURATION_CLASSES) { - Field[] declaredFields = clazz.getDeclaredFields(); - for (Field field : declaredFields) { - Property property = getFieldIfRelevant(field); - if (property != null) { - properties.put(property, getCommentsForField(field)); - } - } - } - return properties; - } - - private static String[] getCommentsForField(Field field) { - if (field.isAnnotationPresent(Comment.class)) { - return field.getAnnotation(Comment.class).value(); - } - return new String[0]; - } - - private static Property getFieldIfRelevant(Field field) { - field.setAccessible(true); - if (field.isAccessible() && Property.class.equals(field.getType()) && Modifier.isStatic(field.getModifiers())) { - try { - return (Property) field.get(null); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Could not fetch field '" + field.getName() + "' from class '" - + field.getDeclaringClass().getSimpleName() + "': " + StringUtils.formatException(e)); - } - } - return null; - } - - - } diff --git a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java new file mode 100644 index 000000000..9727ffac7 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import fr.xephi.authme.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +/** + * Utility class responsible for the retrieval of all {@link Property} fields via reflections. + */ +final class SettingsFieldRetriever { + + /** The classes to scan for properties. */ + private static final List> CONFIGURATION_CLASSES = Arrays.asList( + ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); + + private SettingsFieldRetriever() { + } + + /** + * Scan all given classes for their properties and return the generated {@link PropertyMap}. + * + * @return PropertyMap containing all found properties and their associated comments + * @see #CONFIGURATION_CLASSES + */ + public static PropertyMap getAllPropertyFields() { + PropertyMap properties = new PropertyMap(); + for (Class clazz : CONFIGURATION_CLASSES) { + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + Property property = getFieldIfRelevant(field); + if (property != null) { + properties.put(property, getCommentsForField(field)); + } + } + } + return properties; + } + + private static String[] getCommentsForField(Field field) { + if (field.isAnnotationPresent(Comment.class)) { + return field.getAnnotation(Comment.class).value(); + } + return new String[0]; + } + + private static Property getFieldIfRelevant(Field field) { + field.setAccessible(true); + if (field.isAccessible() && Property.class.equals(field.getType()) && Modifier.isStatic(field.getModifiers())) { + try { + return (Property) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not fetch field '" + field.getName() + "' from class '" + + field.getDeclaringClass().getSimpleName() + "': " + StringUtils.formatException(e)); + } + } + return null; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java index 809f7a987..c0fd2bfe0 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -74,6 +74,16 @@ public class Property { return type.asYaml(this, configuration); } + /** + * Return whether or not the given configuration file contains the property. + * + * @param configuration The configuration file to verify + * @return True if the property is present, false otherwise + */ + public boolean isPresent(YamlConfiguration configuration) { + return type.contains(this, configuration); + } + // ----- // Trivial getters // ----- diff --git a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java index b243ef657..5338da7af 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -41,6 +41,17 @@ public abstract class PropertyType { return asYaml(getFromFile(property, configuration)); } + /** + * Return whether the property is present in the given configuration. + * + * @param property The property to search for + * @param configuration The configuration to verify + * @return True if the property is present, false otherwise + */ + public boolean contains(Property property, YamlConfiguration configuration) { + return configuration.contains(property.getPath()); + } + /** * Transform the given value to YAML. * @@ -49,10 +60,6 @@ public abstract class PropertyType { */ protected abstract List asYaml(T value); - protected boolean contains(Property property, YamlConfiguration configuration) { - return configuration.contains(property.getPath()); - } - /** * Boolean property. @@ -145,6 +152,11 @@ public abstract class PropertyType { } return resultLines; } + + @Override + public boolean contains(Property> property, YamlConfiguration configuration) { + return configuration.contains(property.getPath()) && configuration.isList(property.getPath()); + } } } diff --git a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java index 934245a07..07cfc7144 100644 --- a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java @@ -14,7 +14,7 @@ import java.util.TreeMap; */ public class PropertyMap { - private Map propertyMap; + private Map, String[]> propertyMap; private PropertyMapComparator comparator; /** @@ -41,8 +41,17 @@ public class PropertyMap { * * @return The entry set */ - public Set> entrySet() { + public Set, String[]>> entrySet() { return propertyMap.entrySet(); } + /** + * Return the key set of the map, i.e. all property objects it holds. + * + * @return The key set + */ + public Set> keySet() { + return propertyMap.keySet(); + } + } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java index 1dadf75e6..ab7e763db 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java @@ -30,7 +30,6 @@ public class NewSettingsWriteTest { } - private File getConfigFile() { URL url = getClass().getClassLoader().getResource(CONFIG_FILE); if (url == null) { diff --git a/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java b/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java new file mode 100644 index 000000000..6dce2ad3e --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java @@ -0,0 +1,182 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link PropertyType} and the contained subtypes. + */ +public class PropertyTypeTest { + + private static YamlConfiguration configuration; + + @BeforeClass + public static void setUpYamlConfigurationMock() { + configuration = mock(YamlConfiguration.class); + + when(configuration.getBoolean(eq("bool.path.test"), anyBoolean())).thenReturn(true); + when(configuration.getBoolean(eq("bool.path.wrong"), anyBoolean())).thenAnswer(secondParameter()); + when(configuration.getDouble(eq("double.path.test"), anyDouble())).thenReturn(-6.4); + when(configuration.getDouble(eq("double.path.wrong"), anyDouble())).thenAnswer(secondParameter()); + when(configuration.getInt(eq("int.path.test"), anyInt())).thenReturn(27); + when(configuration.getInt(eq("int.path.wrong"), anyInt())).thenAnswer(secondParameter()); + when(configuration.getString(eq("str.path.test"), anyString())).thenReturn("Test value"); + when(configuration.getString(eq("str.path.wrong"), anyString())).thenAnswer(secondParameter()); + when(configuration.isList("list.path.test")).thenReturn(true); + when(configuration.getStringList("list.path.test")).thenReturn(Arrays.asList("test1", "Test2", "3rd test")); + when(configuration.isList("list.path.wrong")).thenReturn(false); + } + + /* Boolean */ + @Test + public void shouldGetBoolValue() { + // given + Property property = Property.newProperty("bool.path.test", false); + + // when + boolean result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void shouldGetBoolDefault() { + // given + Property property = Property.newProperty("bool.path.wrong", true); + + // when + boolean result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(true)); + } + + /* Double */ + @Test + public void shouldGetDoubleValue() { + // given + Property property = Property.newProperty(PropertyType.DOUBLE, "double.path.test", 3.8); + + // when + double result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(-6.4)); + } + + @Test + public void shouldGetDoubleDefault() { + // given + Property property = Property.newProperty(PropertyType.DOUBLE, "double.path.wrong", 12.0); + + // when + double result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(12.0)); + } + + /* Integer */ + @Test + public void shouldGetIntValue() { + // given + Property property = Property.newProperty("int.path.test", 3); + + // when + int result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(27)); + } + + @Test + public void shouldGetIntDefault() { + // given + Property property = Property.newProperty("int.path.wrong", -10); + + // when + int result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(-10)); + } + + /* String */ + @Test + public void shouldGetStringValue() { + // given + Property property = Property.newProperty("str.path.test", "unused default"); + + // when + String result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo("Test value")); + } + + @Test + public void shouldGetStringDefault() { + // given + Property property = Property.newProperty("str.path.wrong", "given default value"); + + // when + String result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo("given default value")); + } + + /* String list */ + @Test + public void shouldGetStringListValue() { + // given + Property> property = Property.newProperty(PropertyType.STRING_LIST, "list.path.test", "1", "b"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("test1", "Test2", "3rd test")); + } + + @Test + public void shouldGetStringListDefault() { + // given + Property> property = + Property.newProperty(PropertyType.STRING_LIST, "list.path.wrong", "default", "list", "elements"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("default", "list", "elements")); + } + + private static Answer secondParameter() { + return new Answer() { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (T) invocation.getArguments()[1]; + } + }; + } +} diff --git a/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java index 895b12e1c..40ea14b74 100644 --- a/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java +++ b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java @@ -34,9 +34,9 @@ public class PropertyMapTest { } // then - Set> entrySet = map.entrySet(); + Set, String[]>> entrySet = map.entrySet(); List resultPaths = new ArrayList<>(entrySet.size()); - for (Map.Entry entry : entrySet) { + for (Map.Entry, String[]> entry : entrySet) { resultPaths.add(entry.getKey().getPath()); } From d0b7d0ff069be63834b432c61e637ee42ab17dda Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 8 Jan 2016 23:20:42 +0100 Subject: [PATCH 08/26] #347 Create consistency and integration tests for NewSetting - Ensure that the project's config.yml contains all settings that NewSetting is aware of - Add extensive, more generic integration tests --- .../authme/settings/custom/HooksSettings.java | 2 +- .../authme/settings/custom/NewSetting.java | 34 +++- .../authme/settings/custom/PurgeSettings.java | 2 +- src/main/resources/config.yml | 2 + .../custom/ConfigFileConsistencyTest.java | 35 ++++ .../custom/NewSettingIntegrationTest.java | 113 ++++++++++++ .../settings/custom/NewSettingTest.java | 122 +++++-------- .../settings/custom/NewSettingsWriteTest.java | 41 ----- .../settings/custom/TestConfiguration.java | 46 +++++ src/test/resources/437-config-test.yml | 44 ----- src/test/resources/437-write-test.yml | 172 ------------------ .../resources/config-incomplete-sample.yml | 27 +++ src/test/resources/config-sample-values.yml | 27 +++ 13 files changed, 329 insertions(+), 338 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java delete mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java delete mode 100644 src/test/resources/437-config-test.yml delete mode 100644 src/test/resources/437-write-test.yml create mode 100644 src/test/resources/config-incomplete-sample.yml create mode 100644 src/test/resources/config-sample-values.yml diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index 6e23c7398..707821e05 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -19,7 +19,7 @@ public class HooksSettings implements SettingsClass { @Comment("Send player to this BungeeCord server after register/login") public static final Property BUNGEECORD_SERVER = - newProperty(PropertyType.STRING, "bungeecord.server", ""); + newProperty(PropertyType.STRING, "Hooks.sendPlayerTo", ""); @Comment("Do we need to disable Essentials SocialSpy on join?") public static final Property DISABLE_SOCIAL_SPY = diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java index 86a15b0d5..00aa9220e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -24,6 +24,12 @@ public class NewSetting { private File file; private YamlConfiguration configuration; + /** + * Constructor. + * Loads the file as YAML and checks its integrity. + * + * @param file The configuration file + */ public NewSetting(File file) { this.configuration = YamlConfiguration.loadConfiguration(file); this.file = file; @@ -34,12 +40,33 @@ public class NewSetting { } } + /** + * Simple constructor for testing purposes. Does not check for all properties and + * never saves to the file. + * + * @param yamlConfiguration The YamlConfiguration object to use + * @param file The file to write to + * @param propertyMap The property map whose properties should be verified for presence, or null to skip this + */ @VisibleForTesting - NewSetting(YamlConfiguration yamlConfiguration, String file) { + NewSetting(YamlConfiguration yamlConfiguration, File file, PropertyMap propertyMap) { this.configuration = yamlConfiguration; - this.file = new File(file); + this.file = file; + + if (propertyMap != null) { + if (!containsAllSettings(propertyMap)) { + save(propertyMap); + } + } } + /** + * Get the given property from the configuration. + * + * @param property The property to retrieve + * @param The property's type + * @return The property's value + */ public T getOption(Property property) { return property.getFromFile(configuration); } @@ -105,7 +132,8 @@ public class NewSetting { } } - private boolean containsAllSettings(PropertyMap propertyMap) { + @VisibleForTesting + boolean containsAllSettings(PropertyMap propertyMap) { for (Property property : propertyMap.keySet()) { if (!property.isPresent(configuration)) { return false; diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java index e2e1c0290..cff4a7c98 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -25,7 +25,7 @@ public class PurgeSettings implements SettingsClass { @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") public static final Property REMOVE_ESSENTIALS_FILES = - newProperty(BOOLEAN, "Purge.removeEssentialsFiles", false); + newProperty(BOOLEAN, "Purge.removeEssentialsFile", false); @Comment("World where are players.dat stores") public static final Property DEFAULT_WORLD = diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a98d97393..1e0b6491a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -355,6 +355,8 @@ Email: emailWhitelisted: [] # Do we need to send new password draw in an image? generateImage: false + # The email OAuth 2 token (leave empty if not used) + emailOauth2Token: '' Hooks: # Do we need to hook with multiverse for spawn checking? multiverse: true diff --git a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java new file mode 100644 index 000000000..b5bdc4df9 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.settings.custom; + +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +/** + * Test for {@link NewSetting} and the project's config.yml, + * verifying that no settings are missing from the file. + */ +public class ConfigFileConsistencyTest { + + @Test + public void shouldHaveAllConfigs() throws IOException { + URL url = this.getClass().getResource("/config.yml"); + File configFile = new File(url.getFile()); + + // given + assumeThat(configFile.exists(), equalTo(true)); + NewSetting settings = new NewSetting(configFile); + + // when + boolean result = settings.containsAllSettings(SettingsFieldRetriever.getAllPropertyFields()); + + // then + assertThat(result, equalTo(true)); + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java new file mode 100644 index 000000000..8e6801fe6 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.settings.custom; + +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +/** + * Integration test for {@link NewSetting}. + */ +public class NewSettingIntegrationTest { + + /** File name of the sample config including all {@link TestConfiguration} values. */ + private static final String COMPLETE_FILE = "config-sample-values.yml"; + /** File name of the sample config missing certain {@link TestConfiguration} values. */ + private static final String INCOMPLETE_FILE = "config-incomplete-sample.yml"; + + private static PropertyMap propertyMap; + + @BeforeClass + public static void generatePropertyMap() { + propertyMap = new PropertyMap(); + for (Field field : TestConfiguration.class.getDeclaredFields()) { + Property property = + (Property) ReflectionTestUtils.getFieldValue(TestConfiguration.class, null, field.getName()); + String[] comments = new String[]{"Comment for '" + property.getPath() + "'"}; + propertyMap.put(property, comments); + } + } + + @Test + public void shouldLoadAndReadAllProperties() { + // given + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(getConfigFile(COMPLETE_FILE)); + File file = new File("unused"); + assumeThat(file.exists(), equalTo(false)); + + // when / then + NewSetting settings = new NewSetting(configuration, file, propertyMap); + Map, Object> expectedValues = ImmutableMap., Object>builder() + .put(TestConfiguration.DURATION_IN_SECONDS, 22) + .put(TestConfiguration.SYSTEM_NAME, "Custom sys name") + .put(TestConfiguration.RATIO_LIMIT, -4.1) + .put(TestConfiguration.RATIO_FIELDS, Arrays.asList("Australia", "Burundi", "Colombia")) + .put(TestConfiguration.VERSION_NUMBER, 2492) + .put(TestConfiguration.SKIP_BORING_FEATURES, false) + .put(TestConfiguration.BORING_COLORS, Arrays.asList("beige", "gray")) + .put(TestConfiguration.DUST_LEVEL, 0.81) + .put(TestConfiguration.USE_COOL_FEATURES, true) + .put(TestConfiguration.COOL_OPTIONS, Arrays.asList("Dinosaurs", "Explosions", "Big trucks")) + .build(); + for (Map.Entry, Object> entry : expectedValues.entrySet()) { + assertThat("Property '" + entry.getKey().getPath() + "' has expected value", + settings.getOption(entry.getKey()), equalTo(entry.getValue())); + } + assertThat(file.exists(), equalTo(false)); + } + + @Test + public void shouldWriteMissingProperties() { + // given/when + File file = getConfigFile(INCOMPLETE_FILE); + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); + assumeThat(configuration.contains(TestConfiguration.BORING_COLORS.getPath()), equalTo(false)); + // Expectation: File is rewritten to since it does not have all configurations + new NewSetting(configuration, file, propertyMap); + + // Load the settings again -> checks that what we wrote can be loaded again + configuration = YamlConfiguration.loadConfiguration(file); + + // then + NewSetting settings = new NewSetting(configuration, file, propertyMap); + Map, Object> expectedValues = ImmutableMap., Object>builder() + .put(TestConfiguration.DURATION_IN_SECONDS, 22) + .put(TestConfiguration.SYSTEM_NAME, "[TestDefaultValue]") + .put(TestConfiguration.RATIO_LIMIT, 3.0) + .put(TestConfiguration.RATIO_FIELDS, Arrays.asList("Australia", "Burundi", "Colombia")) + .put(TestConfiguration.VERSION_NUMBER, 32046) + .put(TestConfiguration.SKIP_BORING_FEATURES, false) + .put(TestConfiguration.BORING_COLORS, Collections.EMPTY_LIST) + .put(TestConfiguration.DUST_LEVEL, 0.2) + .put(TestConfiguration.USE_COOL_FEATURES, false) + .put(TestConfiguration.COOL_OPTIONS, Arrays.asList("Dinosaurs", "Explosions", "Big trucks")) + .build(); + for (Map.Entry, Object> entry : expectedValues.entrySet()) { + assertThat("Property '" + entry.getKey().getPath() + "' has expected value", + settings.getOption(entry.getKey()), equalTo(entry.getValue())); + } + } + + private File getConfigFile(String file) { + URL url = getClass().getClassLoader().getResource(file); + if (url == null) { + throw new IllegalStateException("File '" + file + "' could not be loaded"); + } + return new File(url.getFile()); + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java index 419d5917c..080e1d0d0 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -1,114 +1,84 @@ package fr.xephi.authme.settings.custom; -import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.settings.domain.PropertyType; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.File; -import java.net.URL; -import java.util.List; -import static fr.xephi.authme.settings.domain.Property.newProperty; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyDouble; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +/** + * Test for {@link NewSetting}. + */ public class NewSettingTest { - private static final String CONFIG_FILE = "437-config-test.yml"; - - @Test - public void shouldReturnIntegerFromFile() { - // given - YamlConfiguration file = mock(YamlConfiguration.class); - Property config = TestConfiguration.DURATION_IN_SECONDS; - given(file.getInt("test.duration", 4)).willReturn(18); - NewSetting settings = new NewSetting(file, "conf.txt"); - - // when - int retrieve = settings.getOption(config); - - // then - assertThat(retrieve, equalTo(18)); - } - @Test public void shouldLoadAllConfigs() { // given YamlConfiguration file = mock(YamlConfiguration.class); + given(file.getString(anyString(), anyString())).willAnswer(withDefaultArgument()); + given(file.getBoolean(anyString(), anyBoolean())).willAnswer(withDefaultArgument()); + given(file.getDouble(anyString(), anyDouble())).willAnswer(withDefaultArgument()); + given(file.getInt(anyString(), anyInt())).willAnswer(withDefaultArgument()); - given(file.getString(anyString(), anyString())).willAnswer(new Answer() { - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - // Return the second parameter -> the default - return (String) invocation.getArguments()[1]; - } - }); + setReturnValue(file, TestConfiguration.VERSION_NUMBER, 20); + setReturnValue(file, TestConfiguration.SKIP_BORING_FEATURES, true); + setReturnValue(file, TestConfiguration.RATIO_LIMIT, 4.25); + setReturnValue(file, TestConfiguration.SYSTEM_NAME, "myTestSys"); - given(file.getInt(eq(EmailSettings.RECOVERY_PASSWORD_LENGTH.getPath()), anyInt())) - .willReturn(20); - given(file.getBoolean(eq(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE.getPath()), anyBoolean())) - .willReturn(false); + // when / then + NewSetting settings = new NewSetting(file, new File("conf.txt"), null); - // when - NewSetting settings = new NewSetting(file, "conf.txt"); + assertThat(settings.getOption(TestConfiguration.VERSION_NUMBER), equalTo(20)); + assertThat(settings.getOption(TestConfiguration.SKIP_BORING_FEATURES), equalTo(true)); + assertThat(settings.getOption(TestConfiguration.RATIO_LIMIT), equalTo(4.25)); + assertThat(settings.getOption(TestConfiguration.SYSTEM_NAME), equalTo("myTestSys")); - // then - // Expect the value we told the YAML mock to return: - assertThat(settings.getOption(EmailSettings.RECOVERY_PASSWORD_LENGTH), equalTo(20)); - // Expect the default: - assertThat(settings.getOption(EmailSettings.SMTP_HOST), equalTo(EmailSettings.SMTP_HOST.getDefaultValue())); - // Expect the value we told the YAML mock to return: - assertThat(settings.getOption(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE), equalTo(false)); + assertDefaultValue(TestConfiguration.DURATION_IN_SECONDS, settings); + assertDefaultValue(TestConfiguration.DUST_LEVEL, settings); + assertDefaultValue(TestConfiguration.COOL_OPTIONS, settings); } - @Test - public void executeIntegrationTest() { - // given - YamlConfiguration yamlFile = YamlConfiguration.loadConfiguration(getConfigFile()); - NewSetting settings = new NewSetting(yamlFile, "conf.txt"); - - // when - int result = settings.getOption(TestConfiguration.DURATION_IN_SECONDS); - String systemName = settings.getOption(TestConfiguration.SYSTEM_NAME); - String helpHeader = settings.getOption(newProperty("settings.helpHeader", "")); - List unsafePasswords = settings.getOption( - newProperty(PropertyType.STRING_LIST, "Security.unsafePasswords")); - DataSource.DataSourceType dataSourceType = settings.getOption(DatabaseSettings.BACKEND); - - // then - assertThat(result, equalTo(22)); - assertThat(systemName, equalTo(TestConfiguration.SYSTEM_NAME.getDefaultValue())); - assertThat(helpHeader, equalTo("AuthMeReloaded")); - assertThat(unsafePasswords, contains("123456", "qwerty", "54321")); - assertThat(dataSourceType, equalTo(DataSource.DataSourceType.MYSQL)); - } - - private File getConfigFile() { - URL url = getClass().getClassLoader().getResource(CONFIG_FILE); - if (url == null) { - throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); + private static void setReturnValue(YamlConfiguration config, Property property, T value) { + if (value instanceof String) { + when(config.getString(eq(property.getPath()), anyString())).thenReturn((String) value); + } else if (value instanceof Integer) { + when(config.getInt(eq(property.getPath()), anyInt())).thenReturn((Integer) value); + } else if (value instanceof Boolean) { + when(config.getBoolean(eq(property.getPath()), anyBoolean())).thenReturn((Boolean) value); + } else if (value instanceof Double) { + when(config.getDouble(eq(property.getPath()), anyDouble())).thenReturn((Double) value); + } else { + throw new UnsupportedOperationException("Value has unsupported type '" + + (value == null ? "null" : value.getClass().getSimpleName()) + "'"); } - return new File(url.getFile()); } - private static class TestConfiguration { + private static void assertDefaultValue(Property property, NewSetting setting) { + assertThat(property.getPath() + " has default value", + setting.getOption(property).equals(property.getDefaultValue()), equalTo(true)); + } - public static final Property DURATION_IN_SECONDS = - newProperty("test.duration", 4); - - public static final Property SYSTEM_NAME = - newProperty("test.systemName", "[TestDefaultValue]"); + private static Answer withDefaultArgument() { + return new Answer() { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (T) invocation.getArguments()[1]; + } + }; } } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java deleted file mode 100644 index ab7e763db..000000000 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.xephi.authme.settings.custom; - -import org.junit.Test; - -import java.io.File; -import java.net.URL; - -import static org.junit.Assert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -/** - * Test for the save() function of new settings - */ -public class NewSettingsWriteTest { - - private static final String CONFIG_FILE = "437-write-test.yml"; - - @Test - public void shouldWriteProperties() { - File file = getConfigFile(); - NewSetting setting = new NewSetting(file); - setting.save(); - - // assert that we can load the file again -- i.e. that it's valid YAML! - NewSetting newSetting = new NewSetting(file); - assertThat(newSetting.getOption(SecuritySettings.CAPTCHA_LENGTH), - equalTo(SecuritySettings.CAPTCHA_LENGTH.getDefaultValue())); - assertThat(newSetting.getOption(ProtectionSettings.COUNTRIES_BLACKLIST), - equalTo(ProtectionSettings.COUNTRIES_BLACKLIST.getDefaultValue())); - } - - - private File getConfigFile() { - URL url = getClass().getClassLoader().getResource(CONFIG_FILE); - if (url == null) { - throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); - } - return new File(url.getFile()); - } - -} diff --git a/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java b/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java new file mode 100644 index 000000000..5f43262c7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +/** + * Sample properties for testing purposes. + */ +class TestConfiguration implements SettingsClass { + + public static final Property DURATION_IN_SECONDS = + newProperty("test.duration", 4); + + public static final Property SYSTEM_NAME = + newProperty("test.systemName", "[TestDefaultValue]"); + + public static final Property RATIO_LIMIT = + newProperty(PropertyType.DOUBLE, "sample.ratio.limit", 3.0); + + public static final Property> RATIO_FIELDS = + newProperty(PropertyType.STRING_LIST, "sample.ratio.fields", "a", "b", "c"); + + public static final Property VERSION_NUMBER = + newProperty("version", 32046); + + public static final Property SKIP_BORING_FEATURES = + newProperty("features.boring.skip", false); + + public static final Property> BORING_COLORS = + newProperty(PropertyType.STRING_LIST, "features.boring.colors"); + + public static final Property DUST_LEVEL = + newProperty(PropertyType.DOUBLE, "features.boring.dustLevel", 0.2); + + public static final Property USE_COOL_FEATURES = + newProperty("features.cool.enabled", false); + + public static final Property> COOL_OPTIONS = + newProperty(PropertyType.STRING_LIST, "features.cool.options", "Sparks", "Sprinkles"); + +} diff --git a/src/test/resources/437-config-test.yml b/src/test/resources/437-config-test.yml deleted file mode 100644 index 5057bea92..000000000 --- a/src/test/resources/437-config-test.yml +++ /dev/null @@ -1,44 +0,0 @@ -test: - duration: 22 -DataSource: - # What type of database do you want to use? - # Valid values: sqlite, mysql - backend: mysql - # Enable database caching, should improve database performance - caching: true -settings: - # The name shown in the help messages. - helpHeader: AuthMeReloaded - GameMode: - # ForceSurvivalMode to player when join ? - ForceSurvivalMode: false -Security: - SQLProblem: - # Stop the server if we can't contact the sql database - # Take care with this, if you set that to false, - # AuthMe automatically disable and the server is not protected! - stopServer: true - ReloadCommand: - # /reload support - useReloadCommandSupport: true - console: - # Remove spam console - noConsoleSpam: false - # Replace passwords in the console when player type a command like /login - removePassword: true - captcha: - # Player need to put a captcha when he fails too lot the password - useCaptcha: false - # Max allowed tries before request a captcha - maxLoginTry: 5 - # Captcha length - captchaLength: 5 - unsafePasswords: - - '123456' - - 'qwerty' - - '54321' -Email: - # Email SMTP server host - mailSMTP: smtp.gmail.com - # Email SMTP server port - mailPort: 465 diff --git a/src/test/resources/437-write-test.yml b/src/test/resources/437-write-test.yml deleted file mode 100644 index de2b762f8..000000000 --- a/src/test/resources/437-write-test.yml +++ /dev/null @@ -1,172 +0,0 @@ - - -Converter: - Rakamak: - # Rakamak file name - fileName: 'users.rak' - # Rakamak use IP? - useIP: false - # Rakamak IP file name - ipFileName: 'UsersIp.rak' - CrazyLogin: - # CrazyLogin database file name - fileName: 'accounts.db' - -DataSource: - # What type of database do you want to use? - # Valid values: sqlite, mysql - backend: 'sqlite' - # Enable database caching, should improve database performance - caching: true - # Database host address - mySQLHost: '127.0.0.1' - # Database port - mySQLPort: '3306' - # Username about Database Connection Infos - mySQLUsername: 'authme' - # Password about Database Connection Infos - mySQLPassword: '123456' - # Database Name, use with converters or as SQLITE database name - mySQLDatabase: 'authme' - # Table of the database - mySQLTablename: 'authme' - # Column of IDs to sort data - mySQLColumnId: 'id' - # Column for storing or checking players nickname - mySQLColumnName: 'username' - # Column for storing or checking players RealName - mySQLRealName: 'realname' - # Column for storing players passwords - mySQLColumnPassword: 'password' - # Column for storing players emails - mySQLColumnEmail: 'email' - # Column for storing if a player is logged in or not - mySQLColumnLogged: 'email' - # Column for storing players ips - mySQLColumnIp: 'ip' - # Column for storing players lastlogins - mySQLColumnLastLogin: 'lastlogin' - # Column for storing player LastLocation - X - mySQLlastlocX: 'x' - # Column for storing player LastLocation - Y - mySQLlastlocY: 'y' - # Column for storing player LastLocation - Z - mySQLlastlocZ: 'z' - # Column for storing player LastLocation - World Name - mySQLlastlocWorld: 'world' - # Enable this when you allow registration through a website - mySQLWebsite: false - -ExternalBoardOptions: - # Column for storing players passwords salts - mySQLColumnSalt: '' - # Column for storing players groups - mySQLColumnGroup: '' - -Email: - # Email SMTP server host - mailSMTP: 'smtp.gmail.com' - # Email SMTP server port - mailPort: 465 - # Email account which sends the mails - mailAccount: '' - # Email account password - mailPassword: '' - # Recovery password length - RecoveryPasswordLength: 8 - # Mail Subject - mailSubject: 'Your new AuthMe password' - # Like maxRegPerIP but with email - maxRegPerEmail: 1 - # Recall players to add an email? - recallPlayers: false - # Delay in minute for the recall scheduler - delayRecall: 5 - # Blacklist these domains for emails - emailBlacklisted: - - '10minutemail.com' - # Whitelist ONLY these domains for emails - emailWhitelisted: [] - # Send the new password drawn in an image? - generateImage: false - # The OAuth2 token - emailOauth2Token: '' - -Hooks: - # Do we need to hook with multiverse for spawn checking? - multiverse: true - # Do we need to hook with BungeeCord? - bungeecord: false - # Do we need to disable Essentials SocialSpy on join? - disableSocialSpy: false - # Do we need to force /motd Essentials command on join? - useEssentialsMotd: false - # Do we need to cache custom Attributes? - customAttributes: false - -bungeecord: - # Send player to this BungeeCord server after register/login - server: '' - -Protection: - # Enable some servers protection (country based login, antibot) - enableProtection: false - # Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes - # PLEASE USE QUOTES! - countries: - - 'US' - - 'GB' - - 'A1' - # Countries not allowed to join the server and register - # PLEASE USE QUOTES! - countriesBlacklist: [] - # Do we need to enable automatic antibot system? - enableAntiBot: false - # Max number of player allowed to login in 5 secs before enable AntiBot system automatically - antiBotSensibility: 5 - # Duration in minutes of the antibot automatic system - antiBotDuration: 10 - -Purge: - # If enabled, AuthMe automatically purges old, unused accounts - useAutoPurge: false - # Number of Days an account become Unused - daysBeforeRemovePlayer: 60 - # Do we need to remove the player.dat file during purge process? - removePlayerDat: false - # Do we need to remove the Essentials/users/player.yml file during purge process? - removeEssentialsFiles: false - # World where are players.dat stores - defaultWorld: 'world' - # Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ? - removeLimitedCreativesInventories: false - # Do we need to remove the AntiXRayData/PlayerData/player file during purge process? - removeAntiXRayFile: false - # Do we need to remove permissions? - removePermissions: false - -Security: - SQLProblem: - # Stop the server if we can't contact the sql database - # Take care with this, if you set this to false, - # AuthMe will automatically disable and the server won't be protected! - stopServer: true - ReloadCommand: - # /reload support - useReloadCommandSupport: true - console: - # Remove spam from console? - noConsoleSpam: false - # Remove passwords from console? - removePassword: true - captcha: - # Player need to put a captcha when he fails too lot the password - useCaptcha: false - # Max allowed tries before request a captcha - maxLoginTry: 5 - # Captcha length - captchaLength: 5 - stop: - # Kick players before stopping the server, that allow us to save position of players - # and all needed information correctly without any corruption. - kickPlayersBeforeStopping: true diff --git a/src/test/resources/config-incomplete-sample.yml b/src/test/resources/config-incomplete-sample.yml new file mode 100644 index 000000000..a29879720 --- /dev/null +++ b/src/test/resources/config-incomplete-sample.yml @@ -0,0 +1,27 @@ +# Test config file with missing options from TestConfiguration +# Notice the commented out lines! + +test: + duration: 22 +# systemName: 'Custom sys name' +sample: + ratio: +# limit: 3.0 + fields: + - 'Australia' + - 'Burundi' + - 'Colombia' +#version: 2492 +features: +# boring: +# skip: false +# colors: +# - 'beige' +# - 'gray' +# dustLevel: 0.81 + cool: +# enabled: true + options: + - 'Dinosaurs' + - 'Explosions' + - 'Big trucks' diff --git a/src/test/resources/config-sample-values.yml b/src/test/resources/config-sample-values.yml new file mode 100644 index 000000000..1bd99d771 --- /dev/null +++ b/src/test/resources/config-sample-values.yml @@ -0,0 +1,27 @@ +# Test config file with all options +# defined in the TestConfiguration class + +test: + duration: 22 + systemName: 'Custom sys name' +sample: + ratio: + limit: -4.1 + fields: + - 'Australia' + - 'Burundi' + - 'Colombia' +version: 2492 +features: + boring: + skip: false + colors: + - 'beige' + - 'gray' + dustLevel: 0.81 + cool: + enabled: true + options: + - 'Dinosaurs' + - 'Explosions' + - 'Big trucks' From 752ebe5022f3df880042c07f56e5ee34a1016eb7 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 8 Jan 2016 23:40:54 +0100 Subject: [PATCH 09/26] Fix cast exception in integration test - Although the class only has fields of Property type it would appear that CircleCI et al. may use libraries that add fields to classes later on, so we need to check for the field type --- .../settings/custom/NewSettingIntegrationTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java index 8e6801fe6..dcb3c6833 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java @@ -35,10 +35,12 @@ public class NewSettingIntegrationTest { public static void generatePropertyMap() { propertyMap = new PropertyMap(); for (Field field : TestConfiguration.class.getDeclaredFields()) { - Property property = - (Property) ReflectionTestUtils.getFieldValue(TestConfiguration.class, null, field.getName()); - String[] comments = new String[]{"Comment for '" + property.getPath() + "'"}; - propertyMap.put(property, comments); + Object fieldValue = ReflectionTestUtils.getFieldValue(TestConfiguration.class, null, field.getName()); + if (fieldValue instanceof Property) { + Property property = (Property) fieldValue; + String[] comments = new String[]{"Comment for '" + property.getPath() + "'"}; + propertyMap.put(property, comments); + } } } From 88629702f508ed2c97d81aae575c95d89cb7edfe Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 9 Jan 2016 09:30:49 +0100 Subject: [PATCH 10/26] #347 Test that all read properties exist as Property field - Create consistency test to verify that all properties in config.yml are defined as a Property field in a SettingsClass implementation (currently fails) - Add some missing properties - Minor: convert tabs to spaces --- .../settings/custom/ConverterSettings.java | 12 +- .../settings/custom/DatabaseSettings.java | 54 +++---- .../authme/settings/custom/EmailSettings.java | 34 ++-- .../authme/settings/custom/HooksSettings.java | 12 +- .../settings/custom/PluginSettings.java | 46 ++++++ .../settings/custom/ProtectionSettings.java | 16 +- .../authme/settings/custom/PurgeSettings.java | 18 +-- .../settings/custom/RestrictionSettings.java | 147 ++++++++++++++++++ .../settings/custom/SecuritySettings.java | 16 +- .../custom/SettingsFieldRetriever.java | 5 +- .../settings/propertymap/PropertyMap.java | 19 ++- src/main/resources/config.yml | 2 - .../custom/ConfigFileConsistencyTest.java | 57 ++++++- .../custom/SettingsClassConsistencyTest.java | 2 +- 14 files changed, 345 insertions(+), 95 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java index f208727b7..f32cf7483 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -10,19 +10,19 @@ import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class ConverterSettings implements SettingsClass { - @Comment("Rakamak file name") - public static final Property RAKAMAK_FILE_NAME = + @Comment("Rakamak file name") + public static final Property RAKAMAK_FILE_NAME = newProperty(STRING, "Converter.Rakamak.fileName", "users.rak"); - @Comment("Rakamak use IP?") - public static final Property RAKAMAK_USE_IP = + @Comment("Rakamak use IP?") + public static final Property RAKAMAK_USE_IP = newProperty(BOOLEAN, "Converter.Rakamak.useIP", false); - @Comment("Rakamak IP file name") + @Comment("Rakamak IP file name") public static final Property RAKAMAK_IP_FILE_NAME = newProperty(STRING, "Converter.Rakamak.ipFileName", "UsersIp.rak"); - @Comment("CrazyLogin database file name") + @Comment("CrazyLogin database file name") public static final Property CRAZYLOGIN_FILE_NAME = newProperty(STRING, "Converter.CrazyLogin.fileName", "accounts.db"); diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index d638fd8f0..582e5d6eb 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -11,100 +11,100 @@ import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class DatabaseSettings implements SettingsClass { - @Comment({"What type of database do you want to use?", - "Valid values: sqlite, mysql"}) + @Comment({"What type of database do you want to use?", + "Valid values: sqlite, mysql"}) public static final Property BACKEND = newProperty(DataSource.DataSourceType.class, "DataSource.backend", DataSource.DataSourceType.SQLITE); - @Comment("Enable database caching, should improve database performance") + @Comment("Enable database caching, should improve database performance") public static final Property USE_CACHING = newProperty(BOOLEAN, "DataSource.caching", true); - @Comment("Database host address") + @Comment("Database host address") public static final Property MYSQL_HOST = newProperty(STRING, "DataSource.mySQLHost", "127.0.0.1"); - @Comment("Database port") + @Comment("Database port") public static final Property MYSQL_PORT = newProperty(STRING, "DataSource.mySQLPort", "3306"); - @Comment("Username about Database Connection Infos") + @Comment("Username about Database Connection Infos") public static final Property MYSQL_USERNAME = newProperty(STRING, "DataSource.mySQLUsername", "authme"); - @Comment("Password about Database Connection Infos") + @Comment("Password about Database Connection Infos") public static final Property MYSQL_PASSWORD = newProperty(STRING, "DataSource.mySQLPassword", "123456"); - @Comment("Database Name, use with converters or as SQLITE database name") + @Comment("Database Name, use with converters or as SQLITE database name") public static final Property MYSQL_DATABASE = newProperty(STRING, "DataSource.mySQLDatabase", "authme"); - @Comment("Table of the database") + @Comment("Table of the database") public static final Property MYSQL_TABLE = newProperty(STRING, "DataSource.mySQLTablename", "authme"); - @Comment("Column of IDs to sort data") + @Comment("Column of IDs to sort data") public static final Property MYSQL_COL_ID = newProperty(STRING, "DataSource.mySQLColumnId", "id"); - @Comment("Column for storing or checking players nickname") + @Comment("Column for storing or checking players nickname") public static final Property MYSQL_COL_NAME = newProperty(STRING, "DataSource.mySQLColumnName", "username"); - @Comment("Column for storing or checking players RealName ") + @Comment("Column for storing or checking players RealName ") public static final Property MYSQL_COL_REALNAME = newProperty(STRING, "DataSource.mySQLRealName", "realname"); - @Comment("Column for storing players passwords") + @Comment("Column for storing players passwords") public static final Property MYSQL_COL_PASSWORD = newProperty(STRING, "DataSource.mySQLColumnPassword", "password"); - @Comment("Column for storing players passwords salts") + @Comment("Column for storing players passwords salts") public static final Property MYSQL_COL_SALT = newProperty(STRING, "ExternalBoardOptions.mySQLColumnSalt", ""); - @Comment("Column for storing players emails") + @Comment("Column for storing players emails") public static final Property MYSQL_COL_EMAIL = newProperty(STRING, "DataSource.mySQLColumnEmail", "email"); - @Comment("Column for storing if a player is logged in or not") + @Comment("Column for storing if a player is logged in or not") public static final Property MYSQL_COL_ISLOGGED = newProperty(STRING, "DataSource.mySQLColumnLogged", "isLogged"); - @Comment("Column for storing players ips") + @Comment("Column for storing players ips") public static final Property MYSQL_COL_IP = newProperty(STRING, "DataSource.mySQLColumnIp", "ip"); - @Comment("Column for storing players lastlogins") + @Comment("Column for storing players lastlogins") public static final Property MYSQL_COL_LASTLOGIN = newProperty(STRING, "DataSource.mySQLColumnLastLogin", "lastlogin"); - @Comment("Column for storing player LastLocation - X") + @Comment("Column for storing player LastLocation - X") public static final Property MYSQL_COL_LASTLOC_X = newProperty(STRING, "DataSource.mySQLlastlocX", "x"); - @Comment("Column for storing player LastLocation - Y") + @Comment("Column for storing player LastLocation - Y") public static final Property MYSQL_COL_LASTLOC_Y = newProperty(STRING, "DataSource.mySQLlastlocY", "y"); - @Comment("Column for storing player LastLocation - Z") + @Comment("Column for storing player LastLocation - Z") public static final Property MYSQL_COL_LASTLOC_Z = newProperty(STRING, "DataSource.mySQLlastlocZ", "z"); - @Comment("Column for storing player LastLocation - World Name") + @Comment("Column for storing player LastLocation - World Name") public static final Property MYSQL_COL_LASTLOC_WORLD = newProperty(STRING, "DataSource.mySQLlastlocWorld", "world"); - @Comment("Column for storing players groups") + @Comment("Column for storing players groups") public static final Property MYSQL_COL_GROUP = newProperty(STRING, "ExternalBoardOptions.mySQLColumnGroup", ""); - @Comment("Enable this when you allow registration through a website") - public static final Property MYSQL_WEBSITE = + @Comment("Enable this when you allow registration through a website") + public static final Property MYSQL_WEBSITE = newProperty(BOOLEAN, "DataSource.mySQLWebsite", false); - private DatabaseSettings() { - } + private DatabaseSettings() { + } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java index dcde50191..683143652 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -14,51 +14,55 @@ import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; public class EmailSettings implements SettingsClass { - @Comment("Email SMTP server host") - public static final Property SMTP_HOST = + @Comment("Email SMTP server host") + public static final Property SMTP_HOST = newProperty(STRING, "Email.mailSMTP", "smtp.gmail.com"); - @Comment("Email SMTP server port") + @Comment("Email SMTP server port") public static final Property SMTP_PORT = newProperty(INTEGER, "Email.mailPort", 465); - @Comment("Email account which sends the mails") + @Comment("Email account which sends the mails") public static final Property MAIL_ACCOUNT = newProperty(STRING, "Email.mailAccount", ""); - @Comment("Email account password") + @Comment("Email account password") public static final Property MAIL_PASSWORD = newProperty(STRING, "Email.mailPassword", ""); - @Comment("Recovery password length") + @Comment("Custom sender name, replacing the mailAccount name in the email") + public static final Property MAIL_SENDER_NAME = + newProperty("Email.mailSenderName", ""); + + @Comment("Recovery password length") public static final Property RECOVERY_PASSWORD_LENGTH = newProperty(INTEGER, "Email.RecoveryPasswordLength", 8); - @Comment("Mail Subject") + @Comment("Mail Subject") public static final Property RECOVERY_MAIL_SUBJECT = newProperty(STRING, "Email.mailSubject", "Your new AuthMe password"); - @Comment("Like maxRegPerIP but with email") + @Comment("Like maxRegPerIP but with email") public static final Property MAX_REG_PER_EMAIL = newProperty(INTEGER, "Email.maxRegPerEmail", 1); - @Comment("Recall players to add an email?") + @Comment("Recall players to add an email?") public static final Property RECALL_PLAYERS = newProperty(BOOLEAN, "Email.recallPlayers", false); - @Comment("Delay in minute for the recall scheduler") + @Comment("Delay in minute for the recall scheduler") public static final Property DELAY_RECALL = newProperty(INTEGER, "Email.delayRecall", 5); - @Comment("Blacklist these domains for emails") - public static final Property> DOMAIN_BLACKLIST = + @Comment("Blacklist these domains for emails") + public static final Property> DOMAIN_BLACKLIST = newProperty(STRING_LIST, "Email.emailBlacklisted", "10minutemail.com"); - @Comment("Whitelist ONLY these domains for emails") - public static final Property> DOMAIN_WHITELIST = + @Comment("Whitelist ONLY these domains for emails") + public static final Property> DOMAIN_WHITELIST = newProperty(STRING_LIST, "Email.emailWhitelisted"); - @Comment("Send the new password drawn in an image?") + @Comment("Send the new password drawn in an image?") public static final Property PASSWORD_AS_IMAGE = newProperty(BOOLEAN, "Email.generateImage", false); diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index 707821e05..d838c33ad 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -9,27 +9,27 @@ import static fr.xephi.authme.settings.domain.Property.newProperty; public class HooksSettings implements SettingsClass { - @Comment("Do we need to hook with multiverse for spawn checking?") + @Comment("Do we need to hook with multiverse for spawn checking?") public static final Property MULTIVERSE = newProperty(PropertyType.BOOLEAN, "Hooks.multiverse", true); - @Comment("Do we need to hook with BungeeCord?") + @Comment("Do we need to hook with BungeeCord?") public static final Property BUNGEECORD = newProperty(PropertyType.BOOLEAN, "Hooks.bungeecord", false); - @Comment("Send player to this BungeeCord server after register/login") + @Comment("Send player to this BungeeCord server after register/login") public static final Property BUNGEECORD_SERVER = newProperty(PropertyType.STRING, "Hooks.sendPlayerTo", ""); - @Comment("Do we need to disable Essentials SocialSpy on join?") + @Comment("Do we need to disable Essentials SocialSpy on join?") public static final Property DISABLE_SOCIAL_SPY = newProperty(PropertyType.BOOLEAN, "Hooks.disableSocialSpy", false); - @Comment("Do we need to force /motd Essentials command on join?") + @Comment("Do we need to force /motd Essentials command on join?") public static final Property USE_ESSENTIALS_MOTD = newProperty(PropertyType.BOOLEAN, "Hooks.useEssentialsMotd", false); - @Comment("Do we need to cache custom Attributes?") + @Comment("Do we need to cache custom Attributes?") public static final Property CACHE_CUSTOM_ATTRIBUTES = newProperty(PropertyType.BOOLEAN, "Hooks.customAttributes", false); diff --git a/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java new file mode 100644 index 000000000..9778d04e4 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class PluginSettings implements SettingsClass { + + @Comment("The name shown in the help messages") + public static final Property HELP_HEADER = + newProperty("settings.helpHeader", "AuthMeReloaded"); + + @Comment({ + "Do you want to enable the session feature?", + "If enabled, when a player authenticates successfully,", + "his IP and his nickname is saved.", + "The next time the player joins the server, if his IP", + "is the same as last time and the timeout hasn't", + "expired, he will not need to authenticate." + }) + public static final Property SESSIONS_ENABLED = + newProperty("settings.sessions.enabled", false); + + @Comment({ + "After how many minutes should a session expire?", + "0 for unlimited time (Very dangerous, use it at your own risk!)", + "Remember that sessions will end only after the timeout, and", + "if the player's IP has changed but the timeout hasn't expired,", + "the player will be kicked from the server due to invalid session" + }) + public static final Property SESSIONS_TIMEOUT = + newProperty("settings.sessions.timeout", 10); + + @Comment({ + "Should the session expire if the player tries to log in with", + "another IP address?" + }) + public static final Property SESSIONS_EXPIRE_ON_IP_CHANGE = + newProperty("settings.sessions.sessionExpireOnIpChange", true); + + private PluginSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java index b71a9589b..2582c277f 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -14,29 +14,29 @@ import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; public class ProtectionSettings implements SettingsClass { - @Comment("Enable some servers protection (country based login, antibot)") + @Comment("Enable some servers protection (country based login, antibot)") public static final Property ENABLE_PROTECTION = newProperty(BOOLEAN, "Protection.enableProtection", false); - @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", - "PLEASE USE QUOTES!"}) + @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", + "PLEASE USE QUOTES!"}) public static final Property> COUNTRIES_WHITELIST = newProperty(STRING_LIST, "Protection.countries", "US", "GB", "A1"); - @Comment({"Countries not allowed to join the server and register", - "PLEASE USE QUOTES!"}) + @Comment({"Countries not allowed to join the server and register", + "PLEASE USE QUOTES!"}) public static final Property> COUNTRIES_BLACKLIST = newProperty(STRING_LIST, "Protection.countriesBlacklist"); - @Comment("Do we need to enable automatic antibot system?") + @Comment("Do we need to enable automatic antibot system?") public static final Property ENABLE_ANTIBOT = newProperty(BOOLEAN, "Protection.enableAntiBot", false); - @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") + @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") public static final Property ANTIBOT_SENSIBILITY = newProperty(INTEGER, "Protection.antiBotSensibility", 5); - @Comment("Duration in minutes of the antibot automatic system") + @Comment("Duration in minutes of the antibot automatic system") public static final Property ANTIBOT_DURATION = newProperty(INTEGER, "Protection.antiBotDuration", 10); diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java index cff4a7c98..5fcc139d7 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -11,35 +11,35 @@ import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class PurgeSettings implements SettingsClass { - @Comment("If enabled, AuthMe automatically purges old, unused accounts") - public static final Property USE_AUTO_PURGE = + @Comment("If enabled, AuthMe automatically purges old, unused accounts") + public static final Property USE_AUTO_PURGE = newProperty(BOOLEAN, "Purge.useAutoPurge", false); - @Comment("Number of Days an account become Unused") + @Comment("Number of Days an account become Unused") public static final Property DAYS_BEFORE_REMOVE_PLAYER = newProperty(INTEGER, "Purge.daysBeforeRemovePlayer", 60); - @Comment("Do we need to remove the player.dat file during purge process?") + @Comment("Do we need to remove the player.dat file during purge process?") public static final Property REMOVE_PLAYER_DAT = newProperty(BOOLEAN, "Purge.removePlayerDat", false); - @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") + @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") public static final Property REMOVE_ESSENTIALS_FILES = newProperty(BOOLEAN, "Purge.removeEssentialsFile", false); - @Comment("World where are players.dat stores") + @Comment("World where are players.dat stores") public static final Property DEFAULT_WORLD = newProperty(STRING, "Purge.defaultWorld", "world"); - @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") + @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") public static final Property REMOVE_LIMITED_CREATIVE_INVENTORIES = newProperty(BOOLEAN, "Purge.removeLimitedCreativesInventories", false); - @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") + @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") public static final Property REMOVE_ANTI_XRAY_FILE = newProperty(BOOLEAN, "Purge.removeAntiXRayFile", false); - @Comment("Do we need to remove permissions?") + @Comment("Do we need to remove permissions?") public static final Property REMOVE_PERMISSIONS = newProperty(BOOLEAN, "Purge.removePermissions", false); diff --git a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java new file mode 100644 index 000000000..f5fa9c061 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java @@ -0,0 +1,147 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class RestrictionSettings implements SettingsClass { + + @Comment({ + "Can not authenticated players chat and see the chat log?", + "Keep in mind that this feature also blocks all commands not", + "listed in the list below."}) + public static final Property ALLOW_CHAT = + newProperty("settings.restrictions.allowChat", false); + + @Comment("Allowed commands for unauthenticated players") + public static final Property> ALLOW_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.allowCommands", + "login", "register", "l", "reg", "email", "captcha"); + + @Comment("Max number of allowed registrations per IP") + // TODO ljacqu 20160109: If 0 == unlimited, add this fact ot the comment + public static final Property MAX_REGISTRATION_PER_IP = + newProperty("settings.restrictions.maxRegPerIp", 1); + + @Comment("Minimum allowed username length") + public static final Property MIN_NICKNAME_LENGTH = + newProperty("settings.restrictions.minNicknameLength", 4); + + @Comment("Maximum allowed username length") + public static final Property MAX_NICKNAME_LENGTH = + newProperty("settings.restrictions.maxNicknameLength", 16); + + @Comment({ + "When this setting is enabled, online players can't be kicked out", + "due to \"Logged in from another Location\"", + "This setting will prevent potential security exploits."}) + public static final Property FORCE_SINGLE_SESSION = + newProperty("settings.restrictions.ForceSingleSession", true); + + @Comment({ + "If enabled, every player will be teleported to the world spawnpoint", + "after successful authentication.", + "The quit location of the player will be overwritten.", + "This is different from \"teleportUnAuthedToSpawn\" that teleport player", + "back to his quit location after the authentication."}) + public static final Property FORCE_SPAWN_LOCATION_AFTER_LOGIN = + newProperty("settings.restrictions.ForceSpawnLocOnJoinEnabled", false); + + @Comment("This option will save the quit location of the players.") + public static final Property SAVE_QUIT_LOCATION = + newProperty("settings.restrictions.SaveQuitLocation", false); + + @Comment({ + "To activate the restricted user feature you need", + "to enable this option and configure the AllowedRestrctedUser field."}) + public static final Property ENABLE_RESTRICTED_USERS = + newProperty("settings.restrictions.AllowRestrictedUser", false); + + @Comment({ + "The restricted user feature will kick players listed below", + "if they don't match the defined IP address.", + "Example:", + " AllowedRestrictedUser:", + " - playername;127.0.0.1"}) + public static final Property> ALLOWED_RESTRICTED_USERS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.AllowedRestrictedUser"); + + @Comment("Should unregistered players be kicked immediately?") + public static final Property KICK_NON_REGISTERED = + newProperty("settings.restrictions.kickNonRegistered", false); + + @Comment("Should players be kicked on wrong password?") + public static final Property KICK_ON_WRONG_PASSWORD = + newProperty("settings.restrictions.kickOnWrongPassword", false); + + @Comment({ + "Should not logged in players be teleported to the spawn?", + "After the authentication they will be teleported back to", + "their normal position."}) + public static final Property TELEPORT_UNAUTHED_TO_SPAWN = + newProperty("settings.restrictions.teleportUnAuthedToSpawn", false); + + @Comment("Can unregistered players walk around?") + public static final Property ALLOW_UNAUTHED_MOVEMENT = + newProperty("settings.restrictions.allowMovement", false); + + @Comment({ + "Should not authenticated players have speed = 0?", + "This will reset the fly/walk speed to default value after the login."}) + public static final Property REMOVE_SPEED = + newProperty("settings.restrictions.removeSpeed", true); + + @Comment({ + "After how many seconds should players who fail to login or register", + "be kicked? Set to 0 to disable."}) + public static final Property TIMEOUT = + newProperty("settings.restrictions.timeout", 30); + + @Comment("Regex syntax of allowed characters in the player name.") + public static final Property ALLOWED_NICKNAME_CHARACTERS = + newProperty("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_]*"); + + @Comment({ + "How far can unregistered players walk?", + "Set to 0 for unlimited radius" + }) + public static final Property ALLOWED_MOVEMENT_RADIUS = + newProperty("settings.restrictions.allowedMovementRadius", 100); + + @Comment({ + "Enable double check of password when you register", + "when it's true, registration requires that kind of command:", + "/register "}) + public static final Property ENABLE_PASSWORD_CONFIRMATION = + newProperty("settings.restrictions.enablePasswordConfirmation", true); + + @Comment("Should we protect the player inventory before logging in?") + public static final Property PROTECT_INVENTORY_BEFORE_LOGIN = + newProperty("settings.restrictions.ProtectInventoryBeforeLogIn", true); + + @Comment({ + "Should we display all other accounts from a player when he joins?", + "permission: /authme.admin.accounts"}) + public static final Property DISPLAY_OTHER_ACCOUNTS = + newProperty("settings.restrictions.displayOtherAccounts", true); + + @Comment({ + "WorldNames where we need to force the spawn location for ForceSpawnLocOnJoinEnabled", + "Case-sensitive!"}) + public static final Property> FORCE_SPAWN_ON_WORLDS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.ForceSpawnOnTheseWorlds", + "world", "world_nether", "world_the_end"); + + @Comment("Ban ip when the ip is not the ip registered in database") + public static final Property BAN_UNKNOWN_IP = + newProperty("settings.restrictions.banUnsafedIP", false); + + private RestrictionSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index c18e7e7ca..1605c166f 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -10,33 +10,33 @@ import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; public class SecuritySettings implements SettingsClass { - @Comment({"Stop the server if we can't contact the sql database", + @Comment({"Stop the server if we can't contact the sql database", "Take care with this, if you set this to false,", "AuthMe will automatically disable and the server won't be protected!"}) - public static final Property STOP_SERVER_ON_PROBLEM = + public static final Property STOP_SERVER_ON_PROBLEM = newProperty(BOOLEAN, "Security.SQLProblem.stopServer", true); - @Comment("/reload support") + @Comment("/reload support") public static final Property USE_RELOAD_COMMAND_SUPPORT = newProperty(BOOLEAN, "Security.ReloadCommand.useReloadCommandSupport", true); - @Comment("Remove spam from console?") + @Comment("Remove spam from console?") public static final Property REMOVE_SPAM_FROM_CONSOLE = newProperty(BOOLEAN, "Security.console.noConsoleSpam", false); - @Comment("Remove passwords from console?") + @Comment("Remove passwords from console?") public static final Property REMOVE_PASSWORD_FROM_CONSOLE = newProperty(BOOLEAN, "Security.console.removePassword", true); - @Comment("Player need to put a captcha when he fails too lot the password") + @Comment("Player need to put a captcha when he fails too lot the password") public static final Property USE_CAPTCHA = newProperty(BOOLEAN, "Security.captcha.useCaptcha", false); - @Comment("Max allowed tries before request a captcha") + @Comment("Max allowed tries before request a captcha") public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = newProperty(INTEGER, "Security.captcha.maxLoginTry", 5); - @Comment("Captcha length") + @Comment("Captcha length") public static final Property CAPTCHA_LENGTH = newProperty(INTEGER, "Security.captcha.captchaLength", 5); diff --git a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java index 9727ffac7..f5c7f23d8 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java @@ -18,8 +18,9 @@ final class SettingsFieldRetriever { /** The classes to scan for properties. */ private static final List> CONFIGURATION_CLASSES = Arrays.asList( - ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, - ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); + ConverterSettings.class, PluginSettings.class, RestrictionSettings.class, + DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); private SettingsFieldRetriever() { } diff --git a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java index 07cfc7144..9cac52d30 100644 --- a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java @@ -14,7 +14,7 @@ import java.util.TreeMap; */ public class PropertyMap { - private Map, String[]> propertyMap; + private Map, String[]> map; private PropertyMapComparator comparator; /** @@ -22,7 +22,7 @@ public class PropertyMap { */ public PropertyMap() { comparator = new PropertyMapComparator(); - propertyMap = new TreeMap<>(comparator); + map = new TreeMap<>(comparator); } /** @@ -33,7 +33,7 @@ public class PropertyMap { */ public void put(Property property, String[] comments) { comparator.add(property); - propertyMap.put(property, comments); + map.put(property, comments); } /** @@ -42,7 +42,7 @@ public class PropertyMap { * @return The entry set */ public Set, String[]>> entrySet() { - return propertyMap.entrySet(); + return map.entrySet(); } /** @@ -51,7 +51,16 @@ public class PropertyMap { * @return The key set */ public Set> keySet() { - return propertyMap.keySet(); + return map.keySet(); + } + + /** + * Return the size of the map. + * + * @return The size + */ + public int size() { + return map.size(); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index bc1725133..960cfc6ee 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -340,8 +340,6 @@ Email: RecoveryPasswordLength: 8 # Email subject of password get mailSubject: 'Your new AuthMe Password' - # Email text here - mailText: 'Dear ,

This is your new AuthMe password for the server

:



Do not forget to change password after login!
/changepassword newPassword' # Like maxRegPerIp but with email maxRegPerEmail: 1 # Recall players to add an email? diff --git a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java index b5bdc4df9..eb12ab030 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java @@ -1,14 +1,24 @@ package fr.xephi.authme.settings.custom; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.YamlConfiguration; import org.junit.Test; import java.io.File; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.junit.Assume.assumeThat; +import static org.junit.Assert.fail; /** * Test for {@link NewSetting} and the project's config.yml, @@ -16,14 +26,15 @@ import static org.junit.Assume.assumeThat; */ public class ConfigFileConsistencyTest { + /** The file name of the project's sample config file. */ + private static final String CONFIG_FILE = "/config.yml"; + @Test public void shouldHaveAllConfigs() throws IOException { - URL url = this.getClass().getResource("/config.yml"); - File configFile = new File(url.getFile()); - // given - assumeThat(configFile.exists(), equalTo(true)); - NewSetting settings = new NewSetting(configFile); + URL url = this.getClass().getResource(CONFIG_FILE); + File configFile = new File(url.getFile()); + NewSetting settings = new NewSetting(YamlConfiguration.loadConfiguration(configFile), new File("bogus"), null); // when boolean result = settings.containsAllSettings(SettingsFieldRetriever.getAllPropertyFields()); @@ -32,4 +43,38 @@ public class ConfigFileConsistencyTest { assertThat(result, equalTo(true)); } + @Test + public void shouldNotHaveUnknownConfigs() { + // given + URL url = this.getClass().getResource(CONFIG_FILE); + File configFile = new File(url.getFile()); + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile); + Map allReadProperties = configuration.getValues(true); + Set knownKeys = getAllKnownPropertyPaths(); + + // when + List unknownPaths = new ArrayList<>(); + for (Map.Entry entry : allReadProperties.entrySet()) { + // The value being a MemorySection means it's a parent node + if (!(entry.getValue() instanceof MemorySection) && !knownKeys.contains(entry.getKey())) { + unknownPaths.add(entry.getKey()); + } + } + + // then + if (!unknownPaths.isEmpty()) { + fail("Found " + unknownPaths.size() + " unknown property paths in the project's config.yml: \n- " + + StringUtils.join("\n- ", unknownPaths)); + } + } + + private static Set getAllKnownPropertyPaths() { + PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); + Set paths = new HashSet<>(propertyMap.size()); + for (Property property : propertyMap.keySet()) { + paths.add(property.getPath()); + } + return paths; + } + } diff --git a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java index c06ed11b1..6dab6bdfc 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java @@ -54,7 +54,7 @@ public class SettingsClassConsistencyTest { for (Field field : fields) { if (Property.class.isAssignableFrom(field.getType())) { String fieldName = "Field " + clazz.getSimpleName() + "#" + field.getName(); - assertThat(fieldName + "should be public, static, and final", + assertThat(fieldName + " should be public, static, and final", isValidConstantField(field), equalTo(true)); } } From 3845c1e0eb754d89f395be6f972f233ed3055c92 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 9 Jan 2016 12:45:58 +0100 Subject: [PATCH 11/26] #347 Add NewSetting to command service - Adjust NewSetting constructor to match needs in AuthMe - Add NewSetting to the command service - See CaptchaCommand for a sample replacement from Settings to NewSetting --- src/main/java/fr/xephi/authme/AuthMe.java | 20 +++++++--- .../xephi/authme/command/CommandService.java | 17 ++++++++- .../executable/captcha/CaptchaCommand.java | 10 ++--- .../executable/register/RegisterCommand.java | 2 +- .../authme/settings/custom/NewSetting.java | 38 ++++++++++--------- .../settings/domain/EnumPropertyType.java | 6 +-- .../authme/settings/domain/Property.java | 8 ++-- .../authme/settings/domain/PropertyType.java | 20 +++++----- .../authme/command/CommandServiceTest.java | 21 +++++++++- .../captcha/CaptchaCommandTest.java | 11 +++--- .../custom/NewSettingIntegrationTest.java | 4 +- .../settings/custom/NewSettingTest.java | 10 ++--- 12 files changed, 107 insertions(+), 60 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 70c8bfd6b..e9de7d941 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -48,6 +48,7 @@ import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.OtherAccounts; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Spawn; +import fr.xephi.authme.settings.custom.NewSetting; import fr.xephi.authme.util.GeoLiteAPI; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; @@ -67,6 +68,7 @@ import org.bukkit.scheduler.BukkitTask; import org.mcstats.Metrics; import org.mcstats.Metrics.Graph; +import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Calendar; @@ -97,6 +99,7 @@ public class AuthMe extends JavaPlugin { private CommandHandler commandHandler = null; private PermissionsManager permsMan = null; private Settings settings; + private NewSetting newSettings; private Messages messages; private JsonCache playerBackup; private ModuleManager moduleManager; @@ -213,6 +216,7 @@ public class AuthMe extends JavaPlugin { setEnabled(false); return; } + newSettings = createNewSetting(); // Set up messages & password security messages = Messages.getInstance(); @@ -233,7 +237,7 @@ public class AuthMe extends JavaPlugin { // Set up the permissions manager and command handler permsMan = initializePermissionsManager(); - commandHandler = initializeCommandHandler(permsMan, messages, passwordSecurity); + commandHandler = initializeCommandHandler(permsMan, messages, passwordSecurity, newSettings); // Set up the module manager setupModuleManager(); @@ -415,11 +419,12 @@ public class AuthMe extends JavaPlugin { } private CommandHandler initializeCommandHandler(PermissionsManager permissionsManager, Messages messages, - PasswordSecurity passwordSecurity) { + PasswordSecurity passwordSecurity, NewSetting settings) { HelpProvider helpProvider = new HelpProvider(permissionsManager); Set baseCommands = CommandInitializer.buildCommands(); CommandMapper mapper = new CommandMapper(baseCommands, messages, permissionsManager, helpProvider); - CommandService commandService = new CommandService(this, mapper, helpProvider, messages, passwordSecurity); + CommandService commandService = new CommandService( + this, mapper, helpProvider, messages, passwordSecurity, settings); return new CommandHandler(commandService); } @@ -441,19 +446,24 @@ public class AuthMe extends JavaPlugin { * @return True on success, false on failure. */ private boolean loadSettings() { - // TODO: new configuration style (more files) try { settings = new Settings(this); Settings.reload(); } catch (Exception e) { ConsoleLogger.writeStackTrace(e); - ConsoleLogger.showError("Can't load the configuration file... Something went wrong, to avoid security issues the server will shutdown!"); + ConsoleLogger.showError("Can't load the configuration file... Something went wrong. " + + "To avoid security issues the server will shut down!"); server.shutdown(); return true; } return false; } + private NewSetting createNewSetting() { + File configFile = new File(getDataFolder() + "config.yml"); + return new NewSetting(getConfig(), configFile); + } + /** * Set up the console filter. */ diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 08a2790e8..8a986b0ee 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -2,6 +2,8 @@ package fr.xephi.authme.command; import java.util.List; +import fr.xephi.authme.settings.custom.NewSetting; +import fr.xephi.authme.settings.domain.Property; import org.bukkit.command.CommandSender; import fr.xephi.authme.AuthMe; @@ -24,6 +26,7 @@ public class CommandService { private final HelpProvider helpProvider; private final CommandMapper commandMapper; private final PasswordSecurity passwordSecurity; + private final NewSetting settings; /** * Constructor. @@ -35,12 +38,13 @@ public class CommandService { * @param passwordSecurity The Password Security instance */ public CommandService(AuthMe authMe, CommandMapper commandMapper, HelpProvider helpProvider, Messages messages, - PasswordSecurity passwordSecurity) { + PasswordSecurity passwordSecurity, NewSetting settings) { this.authMe = authMe; this.messages = messages; this.helpProvider = helpProvider; this.commandMapper = commandMapper; this.passwordSecurity = passwordSecurity; + this.settings = settings; } /** @@ -156,4 +160,15 @@ public class CommandService { return messages.retrieve(key); } + /** + * Retrieve the given property's value. + * + * @param property The property to retrieve + * @param The type of the property + * @return The property's value + */ + public T getProperty(Property property) { + return settings.getProperty(property); + } + } 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 d21bac778..79f963f0b 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 @@ -6,7 +6,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.RandomString; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.util.Wrapper; import org.bukkit.entity.Player; @@ -28,20 +28,20 @@ public class CaptchaCommand extends PlayerCommand { return; } - if (!Settings.useCaptcha) { + if (!commandService.getProperty(SecuritySettings.USE_CAPTCHA)) { commandService.send(player, MessageKey.USAGE_LOGIN); return; } - if (!plugin.cap.containsKey(playerNameLowerCase)) { commandService.send(player, MessageKey.USAGE_LOGIN); return; } - if (Settings.useCaptcha && !captcha.equals(plugin.cap.get(playerNameLowerCase))) { + if (!captcha.equals(plugin.cap.get(playerNameLowerCase))) { plugin.cap.remove(playerNameLowerCase); - String randStr = RandomString.generate(Settings.captchaLength); + int captchaLength = commandService.getProperty(SecuritySettings.CAPTCHA_LENGTH); + String randStr = RandomString.generate(captchaLength); plugin.cap.put(playerNameLowerCase, randStr); commandService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, plugin.cap.get(playerNameLowerCase)); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java index 51bf6c106..8ba9f2cb7 100644 --- a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java @@ -43,6 +43,6 @@ public class RegisterCommand extends PlayerCommand { @Override public String getAlternativeCommand() { - return "authme register "; + return "/authme register "; } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java index 00aa9220e..6f9abdc44 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -6,7 +6,7 @@ import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import java.io.File; import java.io.FileWriter; @@ -22,41 +22,43 @@ import java.util.Map; public class NewSetting { private File file; - private YamlConfiguration configuration; + private FileConfiguration configuration; /** * Constructor. * Loads the file as YAML and checks its integrity. * + * @param configuration The configuration to interact with * @param file The configuration file */ - public NewSetting(File file) { - this.configuration = YamlConfiguration.loadConfiguration(file); + public NewSetting(FileConfiguration configuration, File file) { + this.configuration = configuration; this.file = file; - PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); - if (!containsAllSettings(propertyMap)) { - save(propertyMap); - } + // TODO ljacqu 20160109: Ensure that save() works as desired (i.e. that it always produces valid YAML) + // and then uncomment the lines below. Once this is uncommented, the checks in the old Settings.java should + // be removed as we should check to rewrite the config.yml file only at one place + // -------- + // PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); + // if (!containsAllSettings(propertyMap)) { + // save(propertyMap); + // } } /** - * Simple constructor for testing purposes. Does not check for all properties and - * never saves to the file. + * Constructor for testing purposes, allowing more options. * - * @param yamlConfiguration The YamlConfiguration object to use + * @param configuration The FileConfiguration object to use * @param file The file to write to * @param propertyMap The property map whose properties should be verified for presence, or null to skip this */ @VisibleForTesting - NewSetting(YamlConfiguration yamlConfiguration, File file, PropertyMap propertyMap) { - this.configuration = yamlConfiguration; + NewSetting(FileConfiguration configuration, File file, PropertyMap propertyMap) { + this.configuration = configuration; this.file = file; - if (propertyMap != null) { - if (!containsAllSettings(propertyMap)) { - save(propertyMap); - } + if (propertyMap != null && !containsAllSettings(propertyMap)) { + save(propertyMap); } } @@ -67,7 +69,7 @@ public class NewSetting { * @param The property's type * @return The property's value */ - public T getOption(Property property) { + public T getProperty(Property property) { return property.getFromFile(configuration); } diff --git a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java index 357067d8e..ed184bb7d 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java @@ -1,6 +1,6 @@ package fr.xephi.authme.settings.domain; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import java.util.List; @@ -19,7 +19,7 @@ class EnumPropertyType> extends PropertyType { } @Override - public E getFromFile(Property property, YamlConfiguration configuration) { + public E getFromFile(Property property, FileConfiguration configuration) { String textValue = configuration.getString(property.getPath()); if (textValue == null) { return property.getDefaultValue(); @@ -34,7 +34,7 @@ class EnumPropertyType> extends PropertyType { } @Override - public boolean contains(Property property, YamlConfiguration configuration) { + public boolean contains(Property property, FileConfiguration configuration) { return super.contains(property, configuration) && mapToEnum(configuration.getString(property.getPath())) != null; } diff --git a/src/main/java/fr/xephi/authme/settings/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java index c0fd2bfe0..6b15b5d3b 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -1,6 +1,6 @@ package fr.xephi.authme.settings.domain; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import java.util.Arrays; import java.util.List; @@ -60,7 +60,7 @@ public class Property { * @param configuration The configuration to read the value from * @return The value, or default if not present */ - public T getFromFile(YamlConfiguration configuration) { + public T getFromFile(FileConfiguration configuration) { return type.getFromFile(this, configuration); } @@ -70,7 +70,7 @@ public class Property { * @param configuration The configuration to read the value from * @return The property value as YAML */ - public List formatValueAsYaml(YamlConfiguration configuration) { + public List formatValueAsYaml(FileConfiguration configuration) { return type.asYaml(this, configuration); } @@ -80,7 +80,7 @@ public class Property { * @param configuration The configuration file to verify * @return True if the property is present, false otherwise */ - public boolean isPresent(YamlConfiguration configuration) { + public boolean isPresent(FileConfiguration configuration) { return type.contains(this, configuration); } diff --git a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java index 5338da7af..dc1975bba 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -1,6 +1,6 @@ package fr.xephi.authme.settings.domain; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import java.util.ArrayList; import java.util.List; @@ -28,7 +28,7 @@ public abstract class PropertyType { * @param configuration The YAML configuration to read from * @return The read value, or the default value if absent */ - public abstract T getFromFile(Property property, YamlConfiguration configuration); + public abstract T getFromFile(Property property, FileConfiguration configuration); /** * Return the property's value (or its default) as YAML. @@ -37,7 +37,7 @@ public abstract class PropertyType { * @param configuration The YAML configuration to read from * @return The read value or its default in YAML format */ - public List asYaml(Property property, YamlConfiguration configuration) { + public List asYaml(Property property, FileConfiguration configuration) { return asYaml(getFromFile(property, configuration)); } @@ -48,7 +48,7 @@ public abstract class PropertyType { * @param configuration The configuration to verify * @return True if the property is present, false otherwise */ - public boolean contains(Property property, YamlConfiguration configuration) { + public boolean contains(Property property, FileConfiguration configuration) { return configuration.contains(property.getPath()); } @@ -66,7 +66,7 @@ public abstract class PropertyType { */ private static final class BooleanProperty extends PropertyType { @Override - public Boolean getFromFile(Property property, YamlConfiguration configuration) { + public Boolean getFromFile(Property property, FileConfiguration configuration) { return configuration.getBoolean(property.getPath(), property.getDefaultValue()); } @@ -81,7 +81,7 @@ public abstract class PropertyType { */ private static final class DoubleProperty extends PropertyType { @Override - public Double getFromFile(Property property, YamlConfiguration configuration) { + public Double getFromFile(Property property, FileConfiguration configuration) { return configuration.getDouble(property.getPath(), property.getDefaultValue()); } @@ -96,7 +96,7 @@ public abstract class PropertyType { */ private static final class IntegerProperty extends PropertyType { @Override - public Integer getFromFile(Property property, YamlConfiguration configuration) { + public Integer getFromFile(Property property, FileConfiguration configuration) { return configuration.getInt(property.getPath(), property.getDefaultValue()); } @@ -111,7 +111,7 @@ public abstract class PropertyType { */ private static final class StringProperty extends PropertyType { @Override - public String getFromFile(Property property, YamlConfiguration configuration) { + public String getFromFile(Property property, FileConfiguration configuration) { return configuration.getString(property.getPath(), property.getDefaultValue()); } @@ -131,7 +131,7 @@ public abstract class PropertyType { */ private static final class StringListProperty extends PropertyType> { @Override - public List getFromFile(Property> property, YamlConfiguration configuration) { + public List getFromFile(Property> property, FileConfiguration configuration) { if (!configuration.isList(property.getPath())) { return property.getDefaultValue(); } @@ -154,7 +154,7 @@ public abstract class PropertyType { } @Override - public boolean contains(Property> property, YamlConfiguration configuration) { + public boolean contains(Property> property, FileConfiguration configuration) { return configuration.contains(property.getPath()) && configuration.isList(property.getPath()); } } diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 3b457291e..a08e18efd 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -8,6 +8,9 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.custom.NewSetting; +import fr.xephi.authme.settings.custom.SecuritySettings; +import fr.xephi.authme.settings.domain.Property; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; @@ -36,6 +39,7 @@ public class CommandServiceTest { private Messages messages; private PasswordSecurity passwordSecurity; private CommandService commandService; + private NewSetting settings; @Before public void setUpService() { @@ -44,7 +48,8 @@ public class CommandServiceTest { helpProvider = mock(HelpProvider.class); messages = mock(Messages.class); passwordSecurity = mock(PasswordSecurity.class); - commandService = new CommandService(authMe, commandMapper, helpProvider, messages, passwordSecurity); + settings = mock(NewSetting.class); + commandService = new CommandService(authMe, commandMapper, helpProvider, messages, passwordSecurity, settings); } @Test @@ -195,4 +200,18 @@ public class CommandServiceTest { assertThat(result, equalTo(givenMessages)); verify(messages).retrieve(key); } + + @Test + public void shouldRetrieveProperty() { + // given + Property property = SecuritySettings.CAPTCHA_LENGTH; + given(settings.getProperty(property)).willReturn(7); + + // when + int result = settings.getProperty(property); + + // then + assertThat(result, equalTo(7)); + verify(settings).getProperty(property); + } } diff --git a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java index 60c72bd31..785b0b003 100644 --- a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java @@ -5,7 +5,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.util.WrapperMock; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; @@ -20,6 +20,7 @@ import java.util.Collections; 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; import static org.mockito.Mockito.when; @@ -35,8 +36,8 @@ public class CaptchaCommandTest { @Before public void setUpWrapperMock() { wrapperMock = WrapperMock.createInstance(); - Settings.useCaptcha = true; commandService = mock(CommandService.class); + given(commandService.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true); } @Test @@ -46,7 +47,7 @@ public class CaptchaCommandTest { ExecutableCommand command = new CaptchaCommand(); // when - command.executeCommand(sender, new ArrayList(), mock(CommandService.class)); + command.executeCommand(sender, new ArrayList(), commandService); // then assertThat(wrapperMock.wasMockCalled(AuthMe.class), equalTo(false)); @@ -54,14 +55,14 @@ public class CaptchaCommandTest { } @Test - @Ignore public void shouldRejectIfCaptchaIsNotUsed() { // given Player player = mockPlayerWithName("testplayer"); ExecutableCommand command = new CaptchaCommand(); + given(commandService.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(false); // when - command.executeCommand(player, Collections.singletonList("1234"), mock(CommandService.class)); + command.executeCommand(player, Collections.singletonList("1234"), commandService); // then verify(commandService).send(player, MessageKey.USAGE_LOGIN); diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java index dcb3c6833..73b80b020 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java @@ -67,7 +67,7 @@ public class NewSettingIntegrationTest { .build(); for (Map.Entry, Object> entry : expectedValues.entrySet()) { assertThat("Property '" + entry.getKey().getPath() + "' has expected value", - settings.getOption(entry.getKey()), equalTo(entry.getValue())); + settings.getProperty(entry.getKey()), equalTo(entry.getValue())); } assertThat(file.exists(), equalTo(false)); } @@ -100,7 +100,7 @@ public class NewSettingIntegrationTest { .build(); for (Map.Entry, Object> entry : expectedValues.entrySet()) { assertThat("Property '" + entry.getKey().getPath() + "' has expected value", - settings.getOption(entry.getKey()), equalTo(entry.getValue())); + settings.getProperty(entry.getKey()), equalTo(entry.getValue())); } } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java index 080e1d0d0..64e213aed 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -41,10 +41,10 @@ public class NewSettingTest { // when / then NewSetting settings = new NewSetting(file, new File("conf.txt"), null); - assertThat(settings.getOption(TestConfiguration.VERSION_NUMBER), equalTo(20)); - assertThat(settings.getOption(TestConfiguration.SKIP_BORING_FEATURES), equalTo(true)); - assertThat(settings.getOption(TestConfiguration.RATIO_LIMIT), equalTo(4.25)); - assertThat(settings.getOption(TestConfiguration.SYSTEM_NAME), equalTo("myTestSys")); + assertThat(settings.getProperty(TestConfiguration.VERSION_NUMBER), equalTo(20)); + assertThat(settings.getProperty(TestConfiguration.SKIP_BORING_FEATURES), equalTo(true)); + assertThat(settings.getProperty(TestConfiguration.RATIO_LIMIT), equalTo(4.25)); + assertThat(settings.getProperty(TestConfiguration.SYSTEM_NAME), equalTo("myTestSys")); assertDefaultValue(TestConfiguration.DURATION_IN_SECONDS, settings); assertDefaultValue(TestConfiguration.DUST_LEVEL, settings); @@ -68,7 +68,7 @@ public class NewSettingTest { private static void assertDefaultValue(Property property, NewSetting setting) { assertThat(property.getPath() + " has default value", - setting.getOption(property).equals(property.getDefaultValue()), equalTo(true)); + setting.getProperty(property).equals(property.getDefaultValue()), equalTo(true)); } private static Answer withDefaultArgument() { From fc0b7c46ac52fc356a6c80f99b09aba3834447e1 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 9 Jan 2016 21:57:20 +0100 Subject: [PATCH 12/26] #306 Add permission manager to command service - Inject permission manager into command service explicitly via constructor - Change command mapper to only care about generating FoundCommandResult objects, and command handler to worry about dealing with it later on --- src/main/java/fr/xephi/authme/AuthMe.java | 4 +- .../xephi/authme/command/CommandHandler.java | 77 +++++++++++++++-- .../xephi/authme/command/CommandMapper.java | 85 +------------------ .../xephi/authme/command/CommandService.java | 32 +++---- .../authme/security/PasswordSecurity.java | 2 +- .../authme/command/CommandHandlerTest.java | 5 +- .../authme/command/CommandMapperTest.java | 4 +- .../authme/command/CommandServiceTest.java | 27 ++---- 8 files changed, 101 insertions(+), 135 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index e9de7d941..5e7c383f1 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -422,9 +422,9 @@ public class AuthMe extends JavaPlugin { PasswordSecurity passwordSecurity, NewSetting settings) { HelpProvider helpProvider = new HelpProvider(permissionsManager); Set baseCommands = CommandInitializer.buildCommands(); - CommandMapper mapper = new CommandMapper(baseCommands, messages, permissionsManager, helpProvider); + CommandMapper mapper = new CommandMapper(baseCommands, permissionsManager); CommandService commandService = new CommandService( - this, mapper, helpProvider, messages, passwordSecurity, settings); + this, mapper, helpProvider, messages, passwordSecurity, permissionsManager, settings); return new CommandHandler(commandService); } diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index e3ab671b4..37c1a2a16 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -3,6 +3,9 @@ package fr.xephi.authme.command; import java.util.ArrayList; import java.util.List; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import fr.xephi.authme.util.StringUtils; @@ -13,6 +16,12 @@ import fr.xephi.authme.util.StringUtils; */ public class CommandHandler { + /** + * The threshold for suggesting a similar command. If the difference is below this value, we will + * ask the player whether he meant the similar command. + */ + private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; + private final CommandService commandService; /** @@ -40,14 +49,32 @@ public class CommandHandler { parts.add(0, bukkitCommandLabel); FoundCommandResult result = commandService.mapPartsToCommand(sender, parts); - if (FoundResultStatus.SUCCESS.equals(result.getResultStatus())) { - executeCommand(sender, result); - } else { - commandService.outputMappingError(sender, result); - } + handleCommandResult(sender, result); return !FoundResultStatus.MISSING_BASE_COMMAND.equals(result.getResultStatus()); } + private void handleCommandResult(CommandSender sender, FoundCommandResult result) { + switch (result.getResultStatus()) { + case SUCCESS: + executeCommand(sender, result); + break; + case MISSING_BASE_COMMAND: + sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!"); + break; + case INCORRECT_ARGUMENTS: + sendImproperArgumentsMessage(sender, result); + break; + case UNKNOWN_LABEL: + sendUnknownCommandMessage(sender, result); + break; + case NO_PERMISSION: + sendPermissionDeniedError(sender); + break; + default: + throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'"); + } + } + /** * Execute the command for the given command sender. * @@ -76,6 +103,46 @@ public class CommandHandler { return cleanArguments; } + /** + * Show an "unknown command" message to the user and suggest an existing command if its similarity is within + * the defined threshold. + * + * @param sender The command sender + * @param result The command that was found during the mapping process + */ + private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) { + sender.sendMessage(ChatColor.DARK_RED + "Unknown command!"); + // Show a command suggestion if available and the difference isn't too big + if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) { + sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD + + CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?"); + } + + sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0) + + " help" + ChatColor.YELLOW + " to view help."); + } + + private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { + CommandDescription command = result.getCommandDescription(); + if (!commandService.getPermissionsManager().hasPermission(sender, command)) { + sendPermissionDeniedError(sender); + return; + } + + // Show the command argument help + sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); + commandService.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); + + List labels = result.getLabels(); + String childLabel = labels.size() >= 2 ? labels.get(1) : ""; + sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + + "/" + labels.get(0) + " help " + childLabel); + } + + // TODO ljacqu 20151212: Remove me once I am a MessageKey + private static void sendPermissionDeniedError(CommandSender sender) { + sender.sendMessage(ChatColor.DARK_RED + "You don't have permission to use this command!"); + } } diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 69d31fe5f..8bc5dccdd 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -1,13 +1,11 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.executable.HelpCommand; import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; -import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import java.util.ArrayList; @@ -19,101 +17,24 @@ import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; /** - * The AuthMe command handler, responsible for mapping incoming command parts to the correct {@link CommandDescription} - * or to display help messages for erroneous invocations (unknown command, no permission, etc.). + * The AuthMe command handler, responsible for mapping incoming + * command parts to the correct {@link CommandDescription}. */ public class CommandMapper { - /** - * The threshold for suggesting a similar command. If the difference is below this value, we will - * ask the player whether he meant the similar command. - */ - private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; - /** * The class of the help command, to which the base label should also be passed in the arguments. */ private static final Class HELP_COMMAND_CLASS = HelpCommand.class; private final Set baseCommands; - private final Messages messages; private final PermissionsManager permissionsManager; - private final HelpProvider helpProvider; - public CommandMapper(Set baseCommands, Messages messages, - PermissionsManager permissionsManager, HelpProvider helpProvider) { + public CommandMapper(Set baseCommands, PermissionsManager permissionsManager) { this.baseCommands = baseCommands; - this.messages = messages; this.permissionsManager = permissionsManager; - this.helpProvider = helpProvider; } - public void outputStandardError(CommandSender sender, FoundCommandResult result) { - switch (result.getResultStatus()) { - case SUCCESS: - // Successful mapping, so no error to output - break; - case MISSING_BASE_COMMAND: - sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!"); - break; - case INCORRECT_ARGUMENTS: - sendImproperArgumentsMessage(sender, result); - break; - case UNKNOWN_LABEL: - sendUnknownCommandMessage(sender, result); - break; - case NO_PERMISSION: - sendPermissionDeniedError(sender); - break; - default: - throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'"); - } - } - - /** - * Show an "unknown command" message to the user and suggest an existing command if its similarity is within - * the defined threshold. - * - * @param sender The command sender - * @param result The command that was found during the mapping process - */ - private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) { - sender.sendMessage(ChatColor.DARK_RED + "Unknown command!"); - - // Show a command suggestion if available and the difference isn't too big - if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) { - sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD - + CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?"); - } - - sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0) - + " help" + ChatColor.YELLOW + " to view help."); - } - - private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { - CommandDescription command = result.getCommandDescription(); - if (!permissionsManager.hasPermission(sender, command)) { - sendPermissionDeniedError(sender); - return; - } - - // Show the command argument help - sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); - List lines = helpProvider.printHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); - for (String line : lines) { - sender.sendMessage(line); - } - - List labels = result.getLabels(); - String childLabel = labels.size() >= 2 ? labels.get(1) : ""; - sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE - + "/" + labels.get(0) + " help " + childLabel); - } - - // TODO ljacqu 20151212: Remove me once I am a MessageKey - private static void sendPermissionDeniedError(CommandSender sender) { - sender.sendMessage(ChatColor.DARK_RED + "You don't have permission to use this command!"); - } /** * Map incoming command parts to a command. This processes all parts and distinguishes the labels from arguments. diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 8a986b0ee..8dd1da599 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -1,11 +1,5 @@ package fr.xephi.authme.command; -import java.util.List; - -import fr.xephi.authme.settings.custom.NewSetting; -import fr.xephi.authme.settings.domain.Property; -import org.bukkit.command.CommandSender; - import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.datasource.DataSource; @@ -14,6 +8,11 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.custom.NewSetting; +import fr.xephi.authme.settings.domain.Property; +import org.bukkit.command.CommandSender; + +import java.util.List; /** * Service for implementations of {@link ExecutableCommand} to execute some common tasks. @@ -26,6 +25,7 @@ public class CommandService { private final HelpProvider helpProvider; private final CommandMapper commandMapper; private final PasswordSecurity passwordSecurity; + private final PermissionsManager permissionsManager; private final NewSetting settings; /** @@ -36,14 +36,18 @@ public class CommandService { * @param helpProvider Help provider * @param messages Messages instance * @param passwordSecurity The Password Security instance + * @param permissionsManager The permissions manager + * @param settings The settings manager */ public CommandService(AuthMe authMe, CommandMapper commandMapper, HelpProvider helpProvider, Messages messages, - PasswordSecurity passwordSecurity, NewSetting settings) { + PasswordSecurity passwordSecurity, PermissionsManager permissionsManager, + NewSetting settings) { this.authMe = authMe; this.messages = messages; this.helpProvider = helpProvider; this.commandMapper = commandMapper; this.passwordSecurity = passwordSecurity; + this.permissionsManager = permissionsManager; this.settings = settings; } @@ -79,17 +83,6 @@ public class CommandService { return commandMapper.mapPartsToCommand(sender, commandParts); } - /** - * Output the standard error message for the status in the provided {@link FoundCommandResult} object. - * Does not output anything for successful mappings. - * - * @param sender The sender to output the error to - * @param result The mapping result to process - */ - public void outputMappingError(CommandSender sender, FoundCommandResult result) { - commandMapper.outputStandardError(sender, result); - } - /** * Run the given task asynchronously with the Bukkit scheduler. * @@ -146,8 +139,7 @@ public class CommandService { * @return the permissions manager */ public PermissionsManager getPermissionsManager() { - // TODO ljacqu 20151226: Might be nicer to pass the perm manager via constructor - return authMe.getPermissionsManager(); + return permissionsManager; } /** diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 552b64dd2..ddeb979d3 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -62,7 +62,7 @@ public class PasswordSecurity { * @param hashedPassword The encrypted password to test the clear-text password against * @param playerName The name of the player * - * @return True if the + * @return True if there was a password match with another encryption method, false otherwise */ private boolean compareWithAllEncryptionMethods(String password, HashedPassword hashedPassword, String playerName) { diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index 018721c0d..ed3943a2b 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -14,6 +14,7 @@ import static fr.xephi.authme.command.FoundResultStatus.SUCCESS; import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyListOf; @@ -89,7 +90,9 @@ public class CommandHandlerTest { assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); - verify(serviceMock).outputMappingError(eq(sender), any(FoundCommandResult.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("don't have permission")); } @Test diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index e65c08c68..a7d233050 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -1,7 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; @@ -43,7 +41,7 @@ public class CommandMapperTest { @Before public void setUpMocks() { permissionsManagerMock = mock(PermissionsManager.class); - mapper = new CommandMapper(commands, mock(Messages.class), permissionsManagerMock, mock(HelpProvider.class)); + mapper = new CommandMapper(commands, permissionsManagerMock); } // ----------- diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index a08e18efd..e3e67e3ed 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -39,6 +39,7 @@ public class CommandServiceTest { private Messages messages; private PasswordSecurity passwordSecurity; private CommandService commandService; + private PermissionsManager permissionsManager; private NewSetting settings; @Before @@ -48,8 +49,10 @@ public class CommandServiceTest { helpProvider = mock(HelpProvider.class); messages = mock(Messages.class); passwordSecurity = mock(PasswordSecurity.class); + permissionsManager = mock(PermissionsManager.class); settings = mock(NewSetting.class); - commandService = new CommandService(authMe, commandMapper, helpProvider, messages, passwordSecurity, settings); + commandService = new CommandService( + authMe, commandMapper, helpProvider, messages, passwordSecurity, permissionsManager, settings); } @Test @@ -92,19 +95,6 @@ public class CommandServiceTest { verify(commandMapper).mapPartsToCommand(sender, commandParts); } - @Test - public void shouldOutputMappingError() { - // given - CommandSender sender = mock(CommandSender.class); - FoundCommandResult result = mock(FoundCommandResult.class); - - // when - commandService.outputMappingError(sender, result); - - // then - verify(commandMapper).outputStandardError(sender, result); - } - @Test @Ignore public void shouldRunTaskInAsync() { @@ -174,16 +164,11 @@ public class CommandServiceTest { @Test public void shouldReturnPermissionsManager() { - // given - PermissionsManager manager = mock(PermissionsManager.class); - given(authMe.getPermissionsManager()).willReturn(manager); - - // when + // given / when PermissionsManager result = commandService.getPermissionsManager(); // then - assertThat(result, equalTo(manager)); - verify(authMe).getPermissionsManager(); + assertThat(result, equalTo(permissionsManager)); } @Test From 4d2f39f06ed04d91adac473077174e9e0c1c01d6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 9 Jan 2016 22:51:22 +0100 Subject: [PATCH 13/26] #306 Add tests for CommandHandler and CommandMapper --- .../authme/command/CommandHandlerTest.java | 136 ++++++++++++++++++ .../authme/command/CommandMapperTest.java | 19 +++ .../captcha/CaptchaCommandTest.java | 1 - 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index ed3943a2b..345082bc9 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command; +import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.Test; @@ -7,10 +8,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import java.util.Collections; import java.util.List; +import static fr.xephi.authme.command.FoundResultStatus.INCORRECT_ARGUMENTS; +import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; import static fr.xephi.authme.command.FoundResultStatus.NO_PERMISSION; import static fr.xephi.authme.command.FoundResultStatus.SUCCESS; +import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; @@ -20,8 +25,10 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; /** @@ -95,6 +102,135 @@ public class CommandHandlerTest { assertThat(captor.getValue(), containsString("don't have permission")); } + @Test + public void shouldNotCallExecutableForWrongArguments() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); + PermissionsManager permissionsManager = mock(PermissionsManager.class); + given(permissionsManager.hasPermission(sender, command)).willReturn(true); + given(serviceMock.getPermissionsManager()).willReturn(permissionsManager); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Incorrect command arguments")); + } + + @Test + public void shouldNotCallExecutableForWrongArgumentsAndPermissionDenied() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); + PermissionsManager permissionsManager = mock(PermissionsManager.class); + given(permissionsManager.hasPermission(sender, command)).willReturn(false); + given(serviceMock.getPermissionsManager()).willReturn(permissionsManager); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("You don't have permission")); + } + + @Test + public void shouldNotCallExecutableForFailedParsing() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, MISSING_BASE_COMMAND)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("Failed to parse")); + } + + @Test + public void shouldNotCallExecutableForUnknownLabelAndHaveSuggestion() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.01, UNKNOWN_LABEL)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, times(3)).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Unknown command")); + assertThat(captor.getAllValues().get(1), containsString("Did you mean")); + assertThat(captor.getAllValues().get(1), containsString("/test_cmd")); + assertThat(captor.getAllValues().get(2), containsString("Use the command")); + assertThat(captor.getAllValues().get(2), containsString("to view help")); + } + + @Test + public void shouldNotCallExecutableForUnknownLabelAndNotSuggestCommand() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 1.0, UNKNOWN_LABEL)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, times(2)).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Unknown command")); + assertThat(captor.getAllValues().get(1), containsString("Use the command")); + assertThat(captor.getAllValues().get(1), containsString("to view help")); + } + @Test public void shouldStripWhitespace() { // given diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index a7d233050..8da420431 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -254,4 +254,23 @@ public class CommandMapperTest { assertThat(result.getDifference(), equalTo(0.0)); } + @Test + public void shouldRecognizeMissingPermissionForCommand() { + // given + List parts = Arrays.asList("authme", "login", "test1"); + CommandSender sender = mock(CommandSender.class); + given(permissionsManagerMock.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(false); + + // when + FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); + + // then + assertThat(result.getCommandDescription(), equalTo(getCommandWithLabel(commands, "authme", "login"))); + assertThat(result.getResultStatus(), equalTo(FoundResultStatus.NO_PERMISSION)); + assertThat(result.getArguments(), contains("test1")); + assertThat(result.getDifference(), equalTo(0.0)); + assertThat(result.getLabels(), equalTo(parts.subList(0, 2))); + assertThat(result.getArguments(), contains(parts.get(2))); + } + } diff --git a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java index 785b0b003..c33b13ef3 100644 --- a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java @@ -11,7 +11,6 @@ import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; From ac164c73b934e3ab5bf4852cc012170550ddd7ba Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 9 Jan 2016 23:42:56 +0100 Subject: [PATCH 14/26] #347 Add missing properties (incomplete) --- .../authme/settings/custom/HooksSettings.java | 17 +++-- .../settings/custom/RestrictionSettings.java | 25 +++++++ .../settings/custom/SecuritySettings.java | 68 ++++++++++++++++--- .../xephi/authme/settings/domain/Comment.java | 2 +- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index d838c33ad..f13e66055 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -2,7 +2,6 @@ package fr.xephi.authme.settings.custom; import fr.xephi.authme.settings.domain.Comment; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.settings.domain.PropertyType; import fr.xephi.authme.settings.domain.SettingsClass; import static fr.xephi.authme.settings.domain.Property.newProperty; @@ -11,27 +10,31 @@ public class HooksSettings implements SettingsClass { @Comment("Do we need to hook with multiverse for spawn checking?") public static final Property MULTIVERSE = - newProperty(PropertyType.BOOLEAN, "Hooks.multiverse", true); + newProperty("Hooks.multiverse", true); @Comment("Do we need to hook with BungeeCord?") public static final Property BUNGEECORD = - newProperty(PropertyType.BOOLEAN, "Hooks.bungeecord", false); + newProperty("Hooks.bungeecord", false); @Comment("Send player to this BungeeCord server after register/login") public static final Property BUNGEECORD_SERVER = - newProperty(PropertyType.STRING, "Hooks.sendPlayerTo", ""); + newProperty("Hooks.sendPlayerTo", ""); @Comment("Do we need to disable Essentials SocialSpy on join?") public static final Property DISABLE_SOCIAL_SPY = - newProperty(PropertyType.BOOLEAN, "Hooks.disableSocialSpy", false); + newProperty("Hooks.disableSocialSpy", false); @Comment("Do we need to force /motd Essentials command on join?") public static final Property USE_ESSENTIALS_MOTD = - newProperty(PropertyType.BOOLEAN, "Hooks.useEssentialsMotd", false); + newProperty("Hooks.useEssentialsMotd", false); @Comment("Do we need to cache custom Attributes?") public static final Property CACHE_CUSTOM_ATTRIBUTES = - newProperty(PropertyType.BOOLEAN, "Hooks.customAttributes", false); + newProperty("Hooks.customAttributes", false); + + @Comment("These features are only available on VeryGames Server Provider") + public static final Property ENABLE_VERYGAMES_IP_CHECK = + newProperty("VeryGames.enableIpCheck", false); private HooksSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java index f5fa9c061..e34a2c2fe 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java @@ -141,6 +141,31 @@ public class RestrictionSettings implements SettingsClass { public static final Property BAN_UNKNOWN_IP = newProperty("settings.restrictions.banUnsafedIP", false); + @Comment("Spawn priority; values: authme, essentials, multiverse, default") + public static final Property SPAWN_PRIORITY = + newProperty("settings.restrictions.spawnPriority", "authme,essentials,multiverse,default"); + + @Comment("Maximum Login authorized by IP") + public static final Property MAX_LOGIN_PER_IP = + newProperty("settings.restrictions.maxLoginPerIp", 0); + + @Comment("Maximum Join authorized by IP") + public static final Property MAX_JOIN_PER_IP = + newProperty("settings.restrictions.maxJoinPerIp", 0); + + @Comment("AuthMe will NEVER teleport players if set to true!") + public static final Property NO_TELEPORT = + newProperty("settings.restrictions.noTeleport", false); + + @Comment("Regex syntax for allowed chars in passwords") + public static final Property ALLOWED_PASSWORD_CHARS = + newProperty("settings.restrictions.allowedPasswordCharacters", "[\\x21-\\x7E]*"); + + @Comment("Force survival gamemode when player joins?") + public static final Property FORCE_SURVIVAL_MODE = + newProperty("settings.GameMode.ForceSurvivalMode", false); + + private RestrictionSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index 1605c166f..dcb44b73e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -1,12 +1,14 @@ package fr.xephi.authme.settings.custom; +import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.settings.domain.Comment; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.domain.SettingsClass; +import java.util.List; + import static fr.xephi.authme.settings.domain.Property.newProperty; -import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; public class SecuritySettings implements SettingsClass { @@ -14,36 +16,82 @@ public class SecuritySettings implements SettingsClass { "Take care with this, if you set this to false,", "AuthMe will automatically disable and the server won't be protected!"}) public static final Property STOP_SERVER_ON_PROBLEM = - newProperty(BOOLEAN, "Security.SQLProblem.stopServer", true); + newProperty("Security.SQLProblem.stopServer", true); @Comment("/reload support") public static final Property USE_RELOAD_COMMAND_SUPPORT = - newProperty(BOOLEAN, "Security.ReloadCommand.useReloadCommandSupport", true); + newProperty("Security.ReloadCommand.useReloadCommandSupport", true); @Comment("Remove spam from console?") public static final Property REMOVE_SPAM_FROM_CONSOLE = - newProperty(BOOLEAN, "Security.console.noConsoleSpam", false); + newProperty("Security.console.noConsoleSpam", false); @Comment("Remove passwords from console?") public static final Property REMOVE_PASSWORD_FROM_CONSOLE = - newProperty(BOOLEAN, "Security.console.removePassword", true); + newProperty("Security.console.removePassword", true); @Comment("Player need to put a captcha when he fails too lot the password") public static final Property USE_CAPTCHA = - newProperty(BOOLEAN, "Security.captcha.useCaptcha", false); + newProperty("Security.captcha.useCaptcha", false); @Comment("Max allowed tries before request a captcha") public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = - newProperty(INTEGER, "Security.captcha.maxLoginTry", 5); + newProperty("Security.captcha.maxLoginTry", 5); @Comment("Captcha length") public static final Property CAPTCHA_LENGTH = - newProperty(INTEGER, "Security.captcha.captchaLength", 5); + newProperty("Security.captcha.captchaLength", 5); @Comment({"Kick players before stopping the server, that allow us to save position of players", "and all needed information correctly without any corruption."}) public static final Property KICK_PLAYERS_BEFORE_STOPPING = - newProperty(BOOLEAN, "Security.stop.kickPlayersBeforeStopping", true); + newProperty("Security.stop.kickPlayersBeforeStopping", true); + + @Comment("Minimum length of password") + public static final Property MIN_PASSWORD_LENGTH = + newProperty("settings.security.minPasswordLength", 5); + + @Comment({ + "This is a very important option: every time a player joins the server,", + "if they are registered, AuthMe will switch him to unLoggedInGroup.", + "This should prevent all major exploits.", + "You can set up your permission plugin with this special group to have no permissions,", + "or only permission to chat (or permission to send private messages etc.).", + "The better way is to set up this group with few permissions, so if a player", + "tries to exploit an account they can do only what you've defined for the group.", + "After, a logged in player will be moved to his correct permissions group!", + "Please note that the group name is case-sensitive, so 'admin' is different from 'Admin'", + "Otherwise your group will be wiped and the player will join in the default group []!", + "Example unLoggedinGroup: NotLogged" + }) + public static final Property UNLOGGEDIN_GROUP = + newProperty("settings.security.unLoggedinGroup", "unLoggedinGroup"); + + @Comment({ + "Possible values: MD5, SHA1, SHA256, WHIRLPOOL, XAUTH, MD5VB, PHPBB,", + "MYBB, IPB3, PHPFUSION, SMF, XENFORO, SALTED2MD5, JOOMLA, BCRYPT, WBB3, SHA512,", + "DOUBLEMD5, PBKDF2, PBKDF2DJANGO, WORDPRESS, ROYALAUTH, CUSTOM (for developers only)" + }) + public static final Property PASSWORD_HASH = + newProperty(HashAlgorithm.class, "settings.security.passwordHash", HashAlgorithm.SHA256); + + @Comment("Salt length for the SALTED2MD5 MD5(MD5(password)+salt)") + public static final Property DOUBLE_MD5_SALT_LENGTH = + newProperty("settings.security.doubleMD5SaltLength", 8); + + @Comment({"If password checking return false, do we need to check with all", + "other password algorithm to check an old password?", + "AuthMe will update the password to the new password hash"}) + public static final Property SUPPORT_OLD_PASSWORD_HASH = + newProperty("settings.security.supportOldPasswordHash", false); + + @Comment({"Prevent unsafe passwords from being used; put them in lowercase!", + "unsafePasswords:", + "- '123456'", + "- 'password'"}) + public static final Property> UNSAFE_PASSWORDS = + newProperty(STRING_LIST, "settings.security.unsafePasswords", + "123456", "password", "qwerty", "12345", "54321"); private SecuritySettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/domain/Comment.java b/src/main/java/fr/xephi/authme/settings/domain/Comment.java index 92713aeec..07f20e257 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/Comment.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Comment.java @@ -12,6 +12,6 @@ import java.lang.annotation.Target; @Target(ElementType.FIELD) public @interface Comment { - String[] value() default ""; + String[] value(); } From 391e1b04a2bd091637a76e2fa08ea6a0ede56087 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 14 Jan 2016 21:55:09 +0100 Subject: [PATCH 15/26] Fix #440 Hash algo's sometimes skipped for old algorithm support - Fix check that discards potentially trying all encryption methods if password didn't match - Wrap call to encryption method properly to avoid calling methods with hasSeparateSalt() = true and a null salt --- .../authme/security/PasswordSecurity.java | 27 ++++++++++++------- .../fr/xephi/authme/security/crypts/WBB4.java | 9 ++++++- .../authme/security/PasswordSecurityTest.java | 4 ++- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 552b64dd2..1946df6ab 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -41,15 +41,8 @@ public class PasswordSecurity { public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { EncryptionMethod method = initializeEncryptionMethod(algorithm, playerName); - // User is not in data source, so the result will invariably be wrong because an encryption - // method with hasSeparateSalt() == true NEEDS the salt to evaluate the password - String salt = hashedPassword.getSalt(); - if (method.hasSeparateSalt() && salt == null) { - return false; - } - String playerLowerCase = playerName.toLowerCase(); - return method.comparePassword(password, hashedPassword, playerLowerCase) + return methodMatches(method, password, hashedPassword, playerLowerCase) || supportOldAlgorithm && compareWithAllEncryptionMethods(password, hashedPassword, playerLowerCase); } @@ -69,7 +62,7 @@ public class PasswordSecurity { for (HashAlgorithm algorithm : HashAlgorithm.values()) { if (!HashAlgorithm.CUSTOM.equals(algorithm)) { EncryptionMethod method = initializeEncryptionMethodWithoutEvent(algorithm); - if (method != null && method.comparePassword(password, hashedPassword, playerName)) { + if (methodMatches(method, password, hashedPassword, playerName)) { hashPasswordForNewAlgorithm(password, playerName); return true; } @@ -78,6 +71,22 @@ public class PasswordSecurity { return false; } + /** + * Verify with the given encryption method whether the password matches the hash after checking that + * the method can be called safely with the given data. + * + * @param method The encryption method to use + * @param password The password to check + * @param hashedPassword The hash to check against + * @param playerName The name of the player + * @return True if the password matched, false otherwise + */ + private static boolean methodMatches(EncryptionMethod method, String password, + HashedPassword hashedPassword, String playerName) { + return method != null && (!method.hasSeparateSalt() || hashedPassword.getSalt() != null) + && method.comparePassword(password, hashedPassword, playerName); + } + /** * Get the encryption method from the given {@link HashAlgorithm} value and emit a * {@link PasswordEncryptionEvent}. The encryption method from the event is then returned, 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 9c7d13d3a..cbd77fc66 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WBB4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/WBB4.java @@ -1,7 +1,9 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.StringUtils; @Recommendation(Usage.DOES_NOT_WORK) public class WBB4 extends HexSaltedMethod { @@ -13,7 +15,12 @@ public class WBB4 extends HexSaltedMethod { @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { - return BCRYPT.checkpw(password, hashedPassword.getHash(), 2); + try { + return BCRYPT.checkpw(password, hashedPassword.getHash(), 2); + } catch (IllegalArgumentException e) { + ConsoleLogger.showError("WBB4 compare password returned: " + StringUtils.formatException(e)); + } + return false; } @Override diff --git a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java index d36810ab6..cf15eb5f9 100644 --- a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java +++ b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java @@ -7,6 +7,7 @@ import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.JOOMLA; import fr.xephi.authme.security.crypts.PHPBB; +import fr.xephi.authme.util.WrapperMock; import org.bukkit.event.Event; import org.bukkit.plugin.PluginManager; import org.junit.Before; @@ -42,6 +43,7 @@ public class PasswordSecurityTest { @Before public void setUpMocks() { + WrapperMock.createInstance(); pluginManager = mock(PluginManager.class); dataSource = mock(DataSource.class); method = mock(EncryptionMethod.class); @@ -209,7 +211,7 @@ public class PasswordSecurityTest { HashedPassword hashedPassword = new HashedPassword("~T!est#Hash"); given(method.computeHash(password, username)).willReturn(hashedPassword); given(method.hasSeparateSalt()).willReturn(true); - PasswordSecurity security = new PasswordSecurity(dataSource, HashAlgorithm.XAUTH, pluginManager, true); + PasswordSecurity security = new PasswordSecurity(dataSource, HashAlgorithm.XAUTH, pluginManager, false); // when boolean result = security.comparePassword(password, hashedPassword, username); From fb6cff2e073a043ea06bb3219f8c9e1dd83ab81e Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 17 Jan 2016 09:23:01 +0100 Subject: [PATCH 16/26] #347 Add remaining missing properties --- .../settings/custom/BackupSettings.java | 29 +++++ .../settings/custom/DatabaseSettings.java | 46 ++++---- .../authme/settings/custom/HooksSettings.java | 30 ++++++ .../settings/custom/PluginSettings.java | 14 +++ .../settings/custom/RegistrationSettings.java | 101 ++++++++++++++++++ .../settings/custom/RestrictionSettings.java | 12 +++ .../custom/SettingsFieldRetriever.java | 7 +- src/main/resources/config.yml | 2 +- .../custom/ConfigFileConsistencyTest.java | 18 +++- .../custom/SettingsClassConsistencyTest.java | 13 +++ 10 files changed, 241 insertions(+), 31 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java diff --git a/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java b/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java new file mode 100644 index 000000000..f2e78931b --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class BackupSettings implements SettingsClass { + + @Comment("Enable or disable automatic backup") + public static final Property ENABLED = + newProperty("BackupSystem.ActivateBackup", false); + + @Comment("Set backup at every start of server") + public static final Property ON_SERVER_START = + newProperty("BackupSystem.OnServerStart", false); + + @Comment("Set backup at every stop of server") + public static final Property ON_SERVER_STOP = + newProperty("BackupSystem.OnServerStop", true); + + @Comment(" Windows only mysql installation Path") + public static final Property MYSQL_WINDOWS_PATH = + newProperty("BackupSystem.MysqlWindowsPath", "C:\\Program Files\\MySQL\\MySQL Server 5.1\\"); + + private BackupSettings() { + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index 582e5d6eb..84f0b7085 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -6,8 +6,6 @@ import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.domain.SettingsClass; import static fr.xephi.authme.settings.domain.Property.newProperty; -import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; -import static fr.xephi.authme.settings.domain.PropertyType.STRING; public class DatabaseSettings implements SettingsClass { @@ -18,91 +16,91 @@ public class DatabaseSettings implements SettingsClass { @Comment("Enable database caching, should improve database performance") public static final Property USE_CACHING = - newProperty(BOOLEAN, "DataSource.caching", true); + newProperty("DataSource.caching", true); @Comment("Database host address") public static final Property MYSQL_HOST = - newProperty(STRING, "DataSource.mySQLHost", "127.0.0.1"); + newProperty("DataSource.mySQLHost", "127.0.0.1"); @Comment("Database port") public static final Property MYSQL_PORT = - newProperty(STRING, "DataSource.mySQLPort", "3306"); + newProperty("DataSource.mySQLPort", "3306"); @Comment("Username about Database Connection Infos") public static final Property MYSQL_USERNAME = - newProperty(STRING, "DataSource.mySQLUsername", "authme"); + newProperty("DataSource.mySQLUsername", "authme"); @Comment("Password about Database Connection Infos") public static final Property MYSQL_PASSWORD = - newProperty(STRING, "DataSource.mySQLPassword", "123456"); + newProperty("DataSource.mySQLPassword", "123456"); @Comment("Database Name, use with converters or as SQLITE database name") public static final Property MYSQL_DATABASE = - newProperty(STRING, "DataSource.mySQLDatabase", "authme"); + newProperty("DataSource.mySQLDatabase", "authme"); @Comment("Table of the database") public static final Property MYSQL_TABLE = - newProperty(STRING, "DataSource.mySQLTablename", "authme"); + newProperty("DataSource.mySQLTablename", "authme"); @Comment("Column of IDs to sort data") public static final Property MYSQL_COL_ID = - newProperty(STRING, "DataSource.mySQLColumnId", "id"); + newProperty("DataSource.mySQLColumnId", "id"); @Comment("Column for storing or checking players nickname") public static final Property MYSQL_COL_NAME = - newProperty(STRING, "DataSource.mySQLColumnName", "username"); + newProperty("DataSource.mySQLColumnName", "username"); @Comment("Column for storing or checking players RealName ") public static final Property MYSQL_COL_REALNAME = - newProperty(STRING, "DataSource.mySQLRealName", "realname"); + newProperty("DataSource.mySQLRealName", "realname"); @Comment("Column for storing players passwords") public static final Property MYSQL_COL_PASSWORD = - newProperty(STRING, "DataSource.mySQLColumnPassword", "password"); + newProperty("DataSource.mySQLColumnPassword", "password"); @Comment("Column for storing players passwords salts") public static final Property MYSQL_COL_SALT = - newProperty(STRING, "ExternalBoardOptions.mySQLColumnSalt", ""); + newProperty("ExternalBoardOptions.mySQLColumnSalt", ""); @Comment("Column for storing players emails") public static final Property MYSQL_COL_EMAIL = - newProperty(STRING, "DataSource.mySQLColumnEmail", "email"); + newProperty("DataSource.mySQLColumnEmail", "email"); @Comment("Column for storing if a player is logged in or not") public static final Property MYSQL_COL_ISLOGGED = - newProperty(STRING, "DataSource.mySQLColumnLogged", "isLogged"); + newProperty("DataSource.mySQLColumnLogged", "isLogged"); @Comment("Column for storing players ips") public static final Property MYSQL_COL_IP = - newProperty(STRING, "DataSource.mySQLColumnIp", "ip"); + newProperty("DataSource.mySQLColumnIp", "ip"); @Comment("Column for storing players lastlogins") public static final Property MYSQL_COL_LASTLOGIN = - newProperty(STRING, "DataSource.mySQLColumnLastLogin", "lastlogin"); + newProperty("DataSource.mySQLColumnLastLogin", "lastlogin"); @Comment("Column for storing player LastLocation - X") public static final Property MYSQL_COL_LASTLOC_X = - newProperty(STRING, "DataSource.mySQLlastlocX", "x"); + newProperty("DataSource.mySQLlastlocX", "x"); @Comment("Column for storing player LastLocation - Y") public static final Property MYSQL_COL_LASTLOC_Y = - newProperty(STRING, "DataSource.mySQLlastlocY", "y"); + newProperty("DataSource.mySQLlastlocY", "y"); @Comment("Column for storing player LastLocation - Z") public static final Property MYSQL_COL_LASTLOC_Z = - newProperty(STRING, "DataSource.mySQLlastlocZ", "z"); + newProperty("DataSource.mySQLlastlocZ", "z"); @Comment("Column for storing player LastLocation - World Name") public static final Property MYSQL_COL_LASTLOC_WORLD = - newProperty(STRING, "DataSource.mySQLlastlocWorld", "world"); + newProperty("DataSource.mySQLlastlocWorld", "world"); @Comment("Column for storing players groups") public static final Property MYSQL_COL_GROUP = - newProperty(STRING, "ExternalBoardOptions.mySQLColumnGroup", ""); + newProperty("ExternalBoardOptions.mySQLColumnGroup", ""); @Comment("Enable this when you allow registration through a website") public static final Property MYSQL_WEBSITE = - newProperty(BOOLEAN, "DataSource.mySQLWebsite", false); + newProperty("DataSource.mySQLWebsite", false); private DatabaseSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index f13e66055..af0458ac4 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -2,8 +2,11 @@ package fr.xephi.authme.settings.custom; import fr.xephi.authme.settings.domain.Comment; import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; import fr.xephi.authme.settings.domain.SettingsClass; +import java.util.List; + import static fr.xephi.authme.settings.domain.Property.newProperty; public class HooksSettings implements SettingsClass { @@ -36,6 +39,33 @@ public class HooksSettings implements SettingsClass { public static final Property ENABLE_VERYGAMES_IP_CHECK = newProperty("VeryGames.enableIpCheck", false); + @Comment({ + "-1 means disabled. If you want that only activated players", + "can log into your server, you can set here the group number", + "of unactivated users, needed for some forum/CMS support"}) + public static final Property NON_ACTIVATED_USERS_GROUP = + newProperty("ExternalBoardOptions.nonActivedUserGroup", -1); + + @Comment("Other MySQL columns where we need to put the username (case-sensitive)") + public static final Property> MYSQL_OTHER_USERNAME_COLS = + newProperty(PropertyType.STRING_LIST, "ExternalBoardOptions.mySQLOtherUsernameColumns"); + + @Comment("How much log2 rounds needed in BCrypt (do not change if you do not know what it does)") + public static final Property BCRYPT_LOG2_ROUND = + newProperty("ExternalBoardOptions.bCryptLog2Round", 10); + + @Comment("phpBB table prefix defined during the phpBB installation process") + public static final Property PHPBB_TABLE_PREFIX = + newProperty("ExternalBoardOptions.phpbbTablePrefix", "phpbb_"); + + @Comment("phpBB activated group ID; 2 is the default registered group defined by phpBB") + public static final Property PHPBB_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.phpbbActivatedGroupId", 2); + + @Comment("Wordpress prefix defined during WordPress installation") + public static final Property WORDPRESS_TABLE_PREFIX = + newProperty("ExternalBoardOptions.wordpressTablePrefix", "wp_"); + private HooksSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java index 9778d04e4..e35076db2 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java @@ -40,6 +40,20 @@ public class PluginSettings implements SettingsClass { public static final Property SESSIONS_EXPIRE_ON_IP_CHANGE = newProperty("settings.sessions.sessionExpireOnIpChange", true); + @Comment("Message language, available: en, de, br, cz, pl, fr, ru, hu, sk, es, zhtw, fi, zhcn, lt, it, ko, pt") + public static final Property MESSAGES_LANGUAGE = + newProperty("settings.messagesLanguage", "en"); + + @Comment({ + "Take care with this option; if you don't want", + "to use Vault and group switching of AuthMe", + "for unloggedIn players, set this setting to true.", + "Default is false." + }) + public static final Property ENABLE_PERMISSION_CHECK = + newProperty("permission.EnablePermissionCheck", false); + + private PluginSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java new file mode 100644 index 000000000..7e8301456 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java @@ -0,0 +1,101 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class RegistrationSettings implements SettingsClass { + + @Comment("Enable registration on the server?") + public static final Property IS_ENABLED = + newProperty("settings.registration.enabled", true); + + @Comment({ + "Send every X seconds a message to a player to", + "remind him that he has to login/register"}) + public static final Property MESSAGE_INTERVAL = + newProperty("settings.registration.messageInterval", 5); + + @Comment({ + "Only registered and logged in players can play.", + "See restrictions for exceptions"}) + public static final Property FORCE = + newProperty("settings.registration.force", true); + + @Comment("Do we replace password registration by an email registration method?") + public static final Property USE_EMAIL_REGISTRATION = + newProperty("settings.registration.enableEmailRegistrationSystem", false); + + @Comment({ + "Enable double check of email when you register", + "when it's true, registration requires that kind of command:", + "/register "}) + public static final Property ENABLE_CONFIRM_EMAIL = + newProperty("settings.registration.doubleEmailCheck", false); + + @Comment({ + "Do we force kicking player after a successful registration?", + "Do not use with login feature below"}) + public static final Property FORCE_KICK_AFTER_REGISTER = + newProperty("settings.registration.forceKickAfterRegister", false); + + @Comment("Does AuthMe need to enforce a /login after a successful registration?") + public static final Property FORCE_LOGIN_AFTER_REGISTER = + newProperty("settings.registration.forceLoginAfterRegister", false); + + @Comment("Force these commands after /login, without any '/', use %p to replace with player name") + public static final Property> FORCE_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.forceCommands"); + + @Comment("Force these commands after /login as service console, without any '/'. " + + "Use %p to replace with player name") + public static final Property> FORCE_COMMANDS_AS_CONSOLE = + newProperty(PropertyType.STRING_LIST, "settings.forceCommandsAsConsole"); + + @Comment("Force these commands after /register, without any '/', use %p to replace with player name") + public static final Property> FORCE_REGISTER_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.forceRegisterCommands"); + + @Comment("Force these commands after /register as a server console, without any '/'. " + + "Use %p to replace with player name") + public static final Property> FORCE_REGISTER_COMMANDS_AS_CONSOLE = + newProperty(PropertyType.STRING_LIST, "settings.forceRegisterCommandsAsConsole"); + + @Comment({ + "Enable to display the welcome message (welcome.txt) after a registration or a login", + "You can use colors in this welcome.txt + some replaced strings:", + "{PLAYER}: player name, {ONLINE}: display number of online players, {MAXPLAYERS}: display server slots,", + "{IP}: player ip, {LOGINS}: number of players logged, {WORLD}: player current world, {SERVER}: server name", + "{VERSION}: get current bukkit version, {COUNTRY}: player country"}) + public static final Property USE_WELCOME_MESSAGE = + newProperty("settings.useWelcomeMessage", true); + + @Comment("Do we need to broadcast the welcome message to all server or only to the player? set true for " + + "server or false for player") + public static final Property BROADCAST_WELCOME_MESSAGE = + newProperty("settings.broadcastWelcomeMessage", false); + + @Comment("Do we need to delay the join/leave message to be displayed only when the player is authenticated?") + public static final Property DELAY_JOIN_LEAVE_MESSAGES = + newProperty("settings.delayJoinLeaveMessages", true); + + @Comment("Do we need to add potion effect Blinding before login/reigster?") + public static final Property APPLY_BLIND_EFFECT = + newProperty("settings.applyBlindEffect", false); + + @Comment({ + "Do we need to prevent people to login with another case?", + "If Xephi is registered, then Xephi can login, but not XEPHI/xephi/XePhI"}) + public static final Property PREVENT_OTHER_CASE = + newProperty("settings.preventOtherCase", false); + + + private RegistrationSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java index e34a2c2fe..651c7833d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java @@ -165,6 +165,18 @@ public class RestrictionSettings implements SettingsClass { public static final Property FORCE_SURVIVAL_MODE = newProperty("settings.GameMode.ForceSurvivalMode", false); + @Comment({ + "Below you can list all account names that", + "AuthMe will ignore for registration or login, configure it", + "at your own risk!! Remember that if you are going to add", + "nickname with [], you have to delimit name with ' '.", + "this option add compatibility with BuildCraft and some", + "other mods.", + "It is case-sensitive!" + }) + public static final Property> UNRESTRICTED_NAMES = + newProperty(PropertyType.STRING_LIST, "settings.unrestrictions.UnrestrictedName"); + private RestrictionSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java index f5c7f23d8..74b722e8f 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java @@ -18,9 +18,10 @@ final class SettingsFieldRetriever { /** The classes to scan for properties. */ private static final List> CONFIGURATION_CLASSES = Arrays.asList( - ConverterSettings.class, PluginSettings.class, RestrictionSettings.class, - DatabaseSettings.class, EmailSettings.class, HooksSettings.class, - ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); + ConverterSettings.class, PluginSettings.class, RestrictionSettings.class, + DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class, + RegistrationSettings.class, BackupSettings.class); private SettingsFieldRetriever() { } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 960cfc6ee..22dd7431c 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -209,7 +209,7 @@ settings: # Only registered and logged in players can play. # See restrictions for exceptions force: true - # Does we replace password registration by an Email registration method? + # Do we replace password registration by an email registration method? enableEmailRegistrationSystem: false # Enable double check of email when you register # when it's true, registration require that kind of command: diff --git a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java index eb12ab030..d012a50a4 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java @@ -1,9 +1,11 @@ package fr.xephi.authme.settings.custom; +import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.Test; @@ -16,8 +18,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; /** @@ -40,7 +40,19 @@ public class ConfigFileConsistencyTest { boolean result = settings.containsAllSettings(SettingsFieldRetriever.getAllPropertyFields()); // then - assertThat(result, equalTo(true)); + if (!result) { + FileConfiguration configuration = + (FileConfiguration) ReflectionTestUtils.getFieldValue(NewSetting.class, settings, "configuration"); + + Set knownProperties = getAllKnownPropertyPaths(); + List missingProperties = new ArrayList<>(); + for (String path : knownProperties) { + if (!configuration.contains(path)) { + missingProperties.add(path); + } + } + fail("Found missing properties!\n-" + StringUtils.join("\n-", missingProperties)); + } } @Test diff --git a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java index 6dab6bdfc..a6b67763a 100644 --- a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java @@ -7,6 +7,7 @@ import org.junit.BeforeClass; import org.junit.Test; import java.io.File; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -14,6 +15,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -82,6 +84,17 @@ public class SettingsClassConsistencyTest { } } + @Test + public void shouldHaveHiddenDefaultConstructorOnly() { + for (Class clazz : classes) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + assertThat(clazz + " should only have one constructor", + constructors, arrayWithSize(1)); + assertThat("Constructor of " + clazz + " is private", + Modifier.isPrivate(constructors[0].getModifiers()), equalTo(true)); + } + } + private static boolean isValidConstantField(Field field) { int modifiers = field.getModifiers(); return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); From e18be3024adbffc7ab9e6c497ee643f4b31b4ee0 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 17 Jan 2016 12:33:29 +0100 Subject: [PATCH 17/26] #347 Use NewSetting properties for change password command --- .../authme/ChangePasswordAdminCommand.java | 11 +++++----- .../authme/RegisterAdminCommand.java | 5 +++-- .../changepassword/ChangePasswordCommand.java | 12 ++++++----- .../fr/xephi/authme/settings/Settings.java | 3 ++- .../settings/custom/RestrictionSettings.java | 2 +- .../settings/custom/SecuritySettings.java | 4 ++++ src/main/resources/config.yml | 4 +++- .../ChangePasswordCommandTest.java | 20 ++++++++++--------- 8 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java index ec2b7d98d..a1b27f16f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java @@ -8,7 +8,8 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import org.bukkit.command.CommandSender; import java.util.List; @@ -27,7 +28,7 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { // Validate the password String playerPassLowerCase = playerPass.toLowerCase(); - if (!playerPassLowerCase.matches(Settings.getPassRegex)) { + if (!playerPassLowerCase.matches(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX))) { commandService.send(sender, MessageKey.PASSWORD_MATCH_ERROR); return; } @@ -35,12 +36,12 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(sender, MessageKey.INVALID_PASSWORD_LENGTH); return; } - if (!Settings.unsafePasswords.isEmpty() && Settings.unsafePasswords.contains(playerPassLowerCase)) { + if (commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(playerPassLowerCase)) { commandService.send(sender, MessageKey.PASSWORD_UNSAFE_ERROR); return; } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java index 46916b7b8..107f8c7ae 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java @@ -7,6 +7,7 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -35,8 +36,8 @@ public class RegisterAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(sender, MessageKey.INVALID_PASSWORD_LENGTH); return; } diff --git a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java index 0d1cdc481..a3b7f4451 100644 --- a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java @@ -5,7 +5,8 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.Wrapper; import org.bukkit.entity.Player; @@ -32,7 +33,7 @@ public class ChangePasswordCommand extends PlayerCommand { // Make sure the password is allowed String playerPassLowerCase = newPassword.toLowerCase(); - if (!playerPassLowerCase.matches(Settings.getPassRegex)) { + if (!playerPassLowerCase.matches(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX))) { commandService.send(player, MessageKey.PASSWORD_MATCH_ERROR); return; } @@ -40,17 +41,18 @@ public class ChangePasswordCommand extends PlayerCommand { commandService.send(player, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(player, MessageKey.INVALID_PASSWORD_LENGTH); return; } - if (!Settings.unsafePasswords.isEmpty() && Settings.unsafePasswords.contains(playerPassLowerCase)) { + if (commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(playerPassLowerCase)) { commandService.send(player, MessageKey.PASSWORD_UNSAFE_ERROR); return; } AuthMe plugin = AuthMe.getInstance(); + // TODO ljacqu 20160117: Call async task via Management commandService.runTaskAsynchronously(new ChangePasswordTask(plugin, player, oldPassword, newPassword)); } } diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 4adfe15bd..ebad00fa4 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -7,6 +7,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSource.DataSourceType; import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Wrapper; import org.bukkit.configuration.file.YamlConfiguration; @@ -311,7 +312,7 @@ public final class Settings { try { return Files.toString(EMAIL_FILE, Charsets.UTF_8); } catch (IOException e) { - ConsoleLogger.showError(e.getMessage()); + ConsoleLogger.showError("Error loading email text: " + StringUtils.formatException(e)); ConsoleLogger.writeStackTrace(e); return ""; } diff --git a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java index 651c7833d..32b68586d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java @@ -158,7 +158,7 @@ public class RestrictionSettings implements SettingsClass { newProperty("settings.restrictions.noTeleport", false); @Comment("Regex syntax for allowed chars in passwords") - public static final Property ALLOWED_PASSWORD_CHARS = + public static final Property ALLOWED_PASSWORD_REGEX = newProperty("settings.restrictions.allowedPasswordCharacters", "[\\x21-\\x7E]*"); @Comment("Force survival gamemode when player joins?") diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index dcb44b73e..94e33258c 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -51,6 +51,10 @@ public class SecuritySettings implements SettingsClass { public static final Property MIN_PASSWORD_LENGTH = newProperty("settings.security.minPasswordLength", 5); + @Comment("Maximum length of password") + public static final Property MAX_PASSWORD_LENGTH = + newProperty("settings.security.passwordMaxLength", 30); + @Comment({ "This is a very important option: every time a player joins the server,", "if they are registered, AuthMe will switch him to unLoggedInGroup.", diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 22dd7431c..d2e0a9e74 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -156,8 +156,10 @@ settings: # ForceSurvivalMode to player when join ? ForceSurvivalMode: false security: - # minimum Length of password + # Minimum length of password minPasswordLength: 5 + # Maximum length of password + passwordMaxLength: 30 # this is very important options, # every time player join the server, # if they are registered, AuthMe will switch him diff --git a/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java index 380480ae6..d00a4e37d 100644 --- a/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java @@ -4,7 +4,8 @@ import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.WrapperMock; import org.bukkit.Server; @@ -19,9 +20,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -44,11 +45,11 @@ public class ChangePasswordCommandTest { cacheMock = wrapperMock.getPlayerCache(); commandService = mock(CommandService.class); + when(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).thenReturn(2); + when(commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).thenReturn(50); // Only allow passwords with alphanumerical characters for the test - Settings.getPassRegex = "[a-zA-Z0-9]+"; - Settings.getPasswordMinLen = 2; - Settings.passwordMaxLength = 50; - Settings.unsafePasswords = Collections.EMPTY_LIST; + when(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)).thenReturn("[a-zA-Z0-9]+"); + when(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).thenReturn(Collections.EMPTY_LIST); } @Test @@ -112,7 +113,7 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("abc12", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.passwordMaxLength = 3; + given(commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).willReturn(3); // when command.executeCommand(sender, Arrays.asList("12", "test"), commandService); @@ -127,7 +128,7 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("abc12", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.getPasswordMinLen = 7; + given(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).willReturn(7); // when command.executeCommand(sender, Arrays.asList("oldverylongpassword", "tester"), commandService); @@ -142,7 +143,8 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("player", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.unsafePasswords = asList("test", "abc123"); + given(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)) + .willReturn(Arrays.asList("test", "abc123")); // when command.executeCommand(sender, Arrays.asList("oldpw", "abc123"), commandService); From ff034d5a44bab01fefda79cd31ae54fbac640970 Mon Sep 17 00:00:00 2001 From: games647 Date: Sun, 17 Jan 2016 19:40:55 +0100 Subject: [PATCH 18/26] Fix resetting of sendPlayerTo setting by removing a typo (Fixes Xephi/AuthMeReloaded#451) --- src/main/java/fr/xephi/authme/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index ebad00fa4..42ee6ed48 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -727,7 +727,7 @@ public final class Settings { if (!contains("Email.emailOauth2Token")) set("Email.emailOauth2Token", ""); - if (!contains("Hook.sendPlayerTo")) { + if (!contains("Hooks.sendPlayerTo")) { set("Hooks.sendPlayerTo", ""); changes = true; } From 3b33dc774d45dd0eaf35bcddaf3e43c5f1c09178 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 18 Jan 2016 13:31:54 +0100 Subject: [PATCH 19/26] #369 Fix WBB4 algorithm - Update BCrypt implementation version - Separate third-party BCrypt implementation from our BCRYPT EncryptionMethod extension - Fix WBB4: ensure password is hashed with bcrypt twice and that we check accordingly --- .../xephi/authme/security/crypts/BCRYPT.java | 517 +----------- .../authme/security/crypts/BCRYPT2Y.java | 2 +- .../authme/security/crypts/BCryptService.java | 780 ++++++++++++++++++ .../fr/xephi/authme/security/crypts/WBB4.java | 19 +- .../authme/security/crypts/XFBCRYPT.java | 33 +- .../crypts/AbstractEncryptionMethodTest.java | 4 +- .../authme/security/crypts/WBB4Test.java | 12 +- 7 files changed, 831 insertions(+), 536 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/security/crypts/BCryptService.java 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 a8ea653bb..181571b35 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java @@ -1,542 +1,33 @@ -// Copyright (c) 2006 Damien Miller -// -// Permission to use, copy, modify, and distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.HasSalt; -import fr.xephi.authme.security.crypts.description.Usage; 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.settings.Settings; import fr.xephi.authme.util.StringUtils; -import java.io.UnsupportedEncodingException; -import java.security.SecureRandom; -/** - *

- * BCrypt implements OpenBSD-style Blowfish password hashing using the scheme - * described in "A Future-Adaptable Password Scheme" by Niels Provos and David - * Mazieres. - *

- * This password hashing system tries to thwart off-line password cracking using - * a computationally-intensive hashing algorithm, based on Bruce Schneier's - * Blowfish cipher. The work factor of the algorithm is parameterised, so it can - * be increased as computers get faster. - *

- * Usage is really simple. To hash a password for the first time, call the - * hashpw method with a random salt, like this: - *

- * - * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
- *
- *

- * To check whether a plaintext password matches one that has been hashed - * previously, use the checkpw method: - *

- * - * if (BCrypt.checkpw(candidate_password, stored_hash))
- *     System.out.println("It matches");
- * else
- *     System.out.println("It does not match");
- *
- *

- * The gensalt() method takes an optional parameter (log_rounds) that determines - * the computational complexity of the hashing: - *

- * - * String strong_salt = BCrypt.gensalt(10)
- * String stronger_salt = BCrypt.gensalt(12)
- *
- *

- * The amount of work increases exponentially (2**log_rounds), so each increment - * is twice as much work. The default log_rounds is 10, and the valid range is 4 - * to 31. - *

- * - * @author Damien Miller - * @version 0.2 - */ @Recommendation(Usage.RECOMMENDED) // provided the salt length is >= 8 @HasSalt(value = SaltType.TEXT) // length depends on Settings.bCryptLog2Rounds public class BCRYPT implements EncryptionMethod { - // BCrypt parameters - private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; - protected static final int BCRYPT_SALT_LEN = 16; - - // Blowfish parameters - private static final int BLOWFISH_NUM_ROUNDS = 16; - - // Initial contents of key schedule - private static final int P_orig[] = {0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b}; - private static final int S_orig[] = {0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6}; - - // bcrypt IV: "OrpheanBeholderScryDoubt" - static private final int bf_crypt_ciphertext[] = {0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274}; - - // Table for Base64 encoding - static private final char base64_code[] = {'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; - - // Table for Base64 decoding - static private final byte index_64[] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1}; - - // Expanded Blowfish key - private int P[]; - private int S[]; - - /** - * Encode a byte array using bcrypt's slightly-modified base64 encoding - * scheme. Note that this is *not* compatible with the standard MIME-base64 - * encoding. - * - * @param d the byte array to encode - * @param len the number of bytes to encode - * - * @return base64-encoded string - * - * @throws IllegalArgumentException if the length is invalid - */ - private static String encode_base64(byte d[], int len) - throws IllegalArgumentException { - int off = 0; - StringBuilder rs = new StringBuilder(); - int c1, c2; - - if (len <= 0 || len > d.length) - throw new IllegalArgumentException("Invalid len"); - - while (off < len) { - c1 = d[off++] & 0xff; - rs.append(base64_code[(c1 >> 2) & 0x3f]); - c1 = (c1 & 0x03) << 4; - if (off >= len) { - rs.append(base64_code[c1 & 0x3f]); - break; - } - c2 = d[off++] & 0xff; - c1 |= (c2 >> 4) & 0x0f; - rs.append(base64_code[c1 & 0x3f]); - c1 = (c2 & 0x0f) << 2; - if (off >= len) { - rs.append(base64_code[c1 & 0x3f]); - break; - } - c2 = d[off++] & 0xff; - c1 |= (c2 >> 6) & 0x03; - rs.append(base64_code[c1 & 0x3f]); - rs.append(base64_code[c2 & 0x3f]); - } - return rs.toString(); - } - - /** - * Look up the 3 bits base64-encoded by the specified character, - * range-checking againt conversion table - * - * @param x the base64-encoded value - * - * @return the decoded value of x - */ - private static byte char64(char x) { - if ((int) x > index_64.length) - return -1; - return index_64[(int) x]; - } - - /** - * Decode a string encoded using bcrypt's base64 scheme to a byte array. - * Note that this is *not* compatible with the standard MIME-base64 - * encoding. - * - * @param s the string to decode - * @param maxolen the maximum number of bytes to decode - * - * @return an array containing the decoded bytes - * - * @throws IllegalArgumentException if maxolen is invalid - */ - private static byte[] decode_base64(String s, int maxolen) - throws IllegalArgumentException { - StringBuilder rs = new StringBuilder(); - int off = 0, slen = s.length(), olen = 0; - byte ret[]; - byte c1, c2, c3, c4, o; - - if (maxolen <= 0) - throw new IllegalArgumentException("Invalid maxolen"); - - while (off < slen - 1 && olen < maxolen) { - c1 = char64(s.charAt(off++)); - c2 = char64(s.charAt(off++)); - if (c1 == -1 || c2 == -1) - break; - o = (byte) (c1 << 2); - o |= (c2 & 0x30) >> 4; - rs.append((char) o); - if (++olen >= maxolen || off >= slen) - break; - c3 = char64(s.charAt(off++)); - if (c3 == -1) - break; - o = (byte) ((c2 & 0x0f) << 4); - o |= (c3 & 0x3c) >> 2; - rs.append((char) o); - if (++olen >= maxolen || off >= slen) - break; - c4 = char64(s.charAt(off++)); - o = (byte) ((c3 & 0x03) << 6); - o |= c4; - rs.append((char) o); - ++olen; - } - - ret = new byte[olen]; - for (off = 0; off < olen; off++) - ret[off] = (byte) rs.charAt(off); - return ret; - } - - /** - * Cycically extract a word of key material - * - * @param data the string to extract the data from - * @param offp a "pointer" (as a one-entry array) to the current offset into - * data - * - * @return the next word of material from data - */ - private static int streamtoword(byte data[], int offp[]) { - int i; - int word = 0; - int off = offp[0]; - - for (i = 0; i < 4; i++) { - word = (word << 8) | (data[off] & 0xff); - off = (off + 1) % data.length; - } - - offp[0] = off; - return word; - } - - /** - * Hash a password using the OpenBSD bcrypt scheme - * - * @param password the password to hash - * @param salt the salt to hash with (perhaps generated using BCrypt.gensalt) - * - * @return the hashed password - */ - public static String hashpw(String password, String salt) { - BCRYPT B; - String real_salt; - byte passwordb[], saltb[], hashed[]; - char minor = (char) 0; - int rounds, off = 0; - StringBuilder rs = new StringBuilder(); - - if (salt.charAt(0) != '$' || salt.charAt(1) != '2') - throw new IllegalArgumentException("Invalid salt version"); - if (salt.charAt(2) == '$') - off = 3; - else { - minor = salt.charAt(2); - if (minor < 'a' || minor > 'z' || salt.charAt(3) != '$') - throw new IllegalArgumentException("Invalid salt revision"); - off = 4; - } - - // Extract number of rounds - if (salt.charAt(off + 2) > '$') - throw new IllegalArgumentException("Missing salt rounds"); - rounds = Integer.parseInt(salt.substring(off, off + 2)); - - real_salt = salt.substring(off + 3, off + 25); - try { - passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); - } catch (UnsupportedEncodingException uee) { - throw new AssertionError("UTF-8 is not supported"); - } - - saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); - - B = new BCRYPT(); - hashed = B.crypt_raw(passwordb, saltb, rounds); - - rs.append("$2"); - if (minor >= 'a') - rs.append(minor); - rs.append('$'); - if (rounds < 10) - rs.append('0'); - rs.append(Integer.toString(rounds)); - rs.append('$'); - rs.append(encode_base64(saltb, saltb.length)); - rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1)); - return rs.toString(); - } - - /** - * Generate a salt for use with the BCrypt.hashpw() method - * - * @param log_rounds the log2 of the number of rounds of hashing to apply - the - * work factor therefore increases as 2**log_rounds. - * @param random an instance of SecureRandom to use - * - * @return an encoded salt value - */ - public static String gensalt(int log_rounds, SecureRandom random) { - StringBuilder rs = new StringBuilder(); - byte rnd[] = new byte[BCRYPT_SALT_LEN]; - - random.nextBytes(rnd); - - rs.append("$2a$"); - if (log_rounds < 10) - rs.append('0'); - rs.append(Integer.toString(log_rounds)); - rs.append('$'); - rs.append(encode_base64(rnd, rnd.length)); - return rs.toString(); - } - - /** - * Generate a salt for use with the BCrypt.hashpw() method - * - * @param log_rounds the log2 of the number of rounds of hashing to apply - the - * work factor therefore increases as 2**log_rounds. - * - * @return an encoded salt value - */ - public static String gensalt(int log_rounds) { - return gensalt(log_rounds, new SecureRandom()); - } - - /** - * Generate a salt for use with the BCrypt.hashpw() method, selecting a - * reasonable default for the number of hashing rounds to apply - * - * @return an encoded salt value - */ - public static String gensalt() { - return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); - } - - /** - * Check that a plaintext password matches a previously hashed one - * - * @param plaintext the plaintext password to verify - * @param hashed the previously-hashed password - * - * @return true if the passwords match, false otherwise - */ - public static boolean checkpw(String plaintext, String hashed) { - return (hashed.compareTo(hashpw(plaintext, hashed)) == 0); - } - - /** - * Check that a text password matches a previously hashed one with the - * specified number of rounds using recursion - * - * @param text plaintext or hashed text - * @param hashed the previously-hashed password - * @param rounds number of rounds to hash the password - * - * @return boolean - */ - public static boolean checkpw(String text, String hashed, int rounds) { - boolean matched = false; - - if (rounds > 0) { - String hash = hashpw(text, hashed); - - if (rounds > 1) { - matched = checkpw(hash, hashed, rounds - 1); - } else { - matched = hash.compareTo(hashed) == 0; - } - } else { - matched = text.compareTo(hashed) == 0; - } - - return matched; - } - - public static String getDoubleHash(String text, String salt) { - String hash = hashpw(text, salt); - return hashpw(text, hash); - } - - /** - * Blowfish encipher a single 64-bit block encoded as two 32-bit halves - * - * @param lr an array containing the two 32-bit half blocks - * @param off the position in the array of the blocks - */ - private final void encipher(int lr[], int off) { - int i, n, l = lr[off], r = lr[off + 1]; - - l ^= P[0]; - for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) { - // Feistel substitution on left word - n = S[(l >> 24) & 0xff]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[++i]; - - // Feistel substitution on right word - n = S[(r >> 24) & 0xff]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[++i]; - } - lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; - lr[off + 1] = l; - } - - /** - * Initialise the Blowfish key schedule - */ - private void init_key() { - P = P_orig.clone(); - S = S_orig.clone(); - } - - /** - * Key the Blowfish cipher - * - * @param key an array containing the key - */ - private void key(byte key[]) { - int i; - int koffp[] = {0}; - int lr[] = {0, 0}; - int plen = P.length, slen = S.length; - - for (i = 0; i < plen; i++) - P[i] = P[i] ^ streamtoword(key, koffp); - - for (i = 0; i < plen; i += 2) { - encipher(lr, 0); - P[i] = lr[0]; - P[i + 1] = lr[1]; - } - - for (i = 0; i < slen; i += 2) { - encipher(lr, 0); - S[i] = lr[0]; - S[i + 1] = lr[1]; - } - } - - /** - * Perform the "enhanced key schedule" step described by Provos and Mazieres - * in "A Future-Adaptable Password Scheme" - * http://www.openbsd.org/papers/bcrypt-paper.ps - * - * @param data salt information - * @param key password information - */ - private void ekskey(byte data[], byte key[]) { - int i; - int koffp[] = {0}, doffp[] = {0}; - int lr[] = {0, 0}; - int plen = P.length, slen = S.length; - - for (i = 0; i < plen; i++) - P[i] = P[i] ^ streamtoword(key, koffp); - - for (i = 0; i < plen; i += 2) { - lr[0] ^= streamtoword(data, doffp); - lr[1] ^= streamtoword(data, doffp); - encipher(lr, 0); - P[i] = lr[0]; - P[i + 1] = lr[1]; - } - - for (i = 0; i < slen; i += 2) { - lr[0] ^= streamtoword(data, doffp); - lr[1] ^= streamtoword(data, doffp); - encipher(lr, 0); - S[i] = lr[0]; - S[i + 1] = lr[1]; - } - } - - /** - * Perform the central password hashing step in the bcrypt scheme - * - * @param password the password to hash - * @param salt the binary salt to hash with the password - * @param log_rounds the binary logarithm of the number of rounds of hashing to - * apply - * - * @return an array containing the binary hashed password - */ - private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) { - int rounds, i, j; - int cdata[] = bf_crypt_ciphertext.clone(); - int clen = cdata.length; - byte ret[]; - - if (log_rounds < 4 || log_rounds > 31) - throw new IllegalArgumentException("Bad number of rounds"); - rounds = 1 << log_rounds; - if (salt.length != BCRYPT_SALT_LEN) - throw new IllegalArgumentException("Bad salt length"); - - init_key(); - ekskey(salt, password); - for (i = 0; i < rounds; i++) { - key(password); - key(salt); - } - - for (i = 0; i < 64; i++) { - for (j = 0; j < (clen >> 1); j++) - encipher(cdata, j << 1); - } - - ret = new byte[clen * 4]; - for (i = 0, j = 0; i < clen; i++) { - ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); - ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); - ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); - ret[j++] = (byte) (cdata[i] & 0xff); - } - return ret; - } - @Override public String computeHash(String password, String salt, String name) { - return hashpw(password, salt); + return BCryptService.hashpw(password, salt); } @Override public HashedPassword computeHash(String password, String name) { String salt = generateSalt(); - return new HashedPassword(hashpw(password, salt), null); + return new HashedPassword(BCryptService.hashpw(password, salt), null); } @Override public boolean comparePassword(String password, HashedPassword hash, String name) { try { - return hash.getHash().length() > 3 && checkpw(password, hash.getHash()); + return hash.getHash().length() > 3 && BCryptService.checkpw(password, hash.getHash()); } catch (IllegalArgumentException e) { ConsoleLogger.showError("Bcrypt checkpw() returned " + StringUtils.formatException(e)); } @@ -545,7 +36,7 @@ public class BCRYPT implements EncryptionMethod { @Override public String generateSalt() { - return BCRYPT.gensalt(Settings.bCryptLog2Rounds); + return BCryptService.gensalt(Settings.bCryptLog2Rounds); } @Override 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 cca6fb4ae..49bd45f8a 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT2Y.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT2Y.java @@ -11,7 +11,7 @@ public class BCRYPT2Y extends HexSaltedMethod { if (salt.length() == 22) { salt = "$2y$10$" + salt; } - return BCRYPT.hashpw(password, salt); + return BCryptService.hashpw(password, salt); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java new file mode 100644 index 000000000..c2f58f605 --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java @@ -0,0 +1,780 @@ +package fr.xephi.authme.security.crypts; + +// Copyright (c) 2006 Damien Miller +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import java.io.UnsupportedEncodingException; + +import java.security.SecureRandom; + +/** + * BCrypt implements OpenBSD-style Blowfish password hashing using + * the scheme described in "A Future-Adaptable Password Scheme" by + * Niels Provos and David Mazieres. + *

+ * This password hashing system tries to thwart off-line password + * cracking using a computationally-intensive hashing algorithm, + * based on Bruce Schneier's Blowfish cipher. The work factor of + * the algorithm is parameterised, so it can be increased as + * computers get faster. + *

+ * Usage is really simple. To hash a password for the first time, + * call the hashpw method with a random salt, like this: + *

+ * + * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ *
+ *

+ * To check whether a plaintext password matches one that has been + * hashed previously, use the checkpw method: + *

+ * + * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
+ *
+ *

+ * The gensalt() method takes an optional parameter (log_rounds) + * that determines the computational complexity of the hashing: + *

+ * + * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
+ *
+ *

+ * The amount of work increases exponentially (2**log_rounds), so + * each increment is twice as much work. The default log_rounds is + * 10, and the valid range is 4 to 30. + * + * @author Damien Miller + * @version 0.4 + */ +public class BCryptService { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + }; + private static final int S_orig[] = { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls + // this "ciphertext", but it is really plaintext or an IV. We keep + // the name to make code comparison easier. + static private final int bf_crypt_ciphertext[] = { + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 + }; + + // Table for Base64 encoding + static private final char base64_code[] = { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + // Table for Base64 decoding + static private final byte index_64[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, + -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + -1, -1, -1, -1, -1, -1, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, -1, -1, -1, -1, -1 + }; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 + * encoding scheme. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte d[], int len) + throws IllegalArgumentException { + int off = 0; + StringBuffer rs = new StringBuffer(); + int c1, c2; + + if (len <= 0 || len > d.length) + throw new IllegalArgumentException ("Invalid len"); + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, + * range-checking againt conversion table + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int)x < 0 || (int)x > index_64.length) + return -1; + return index_64[(int)x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a + * byte array. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + StringBuffer rs = new StringBuffer(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) + throw new IllegalArgumentException ("Invalid maxolen"); + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) + break; + o = (byte)(c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c3 = char64(s.charAt(off++)); + if (c3 == -1) + break; + o = (byte)((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c4 = char64(s.charAt(off++)); + o = (byte)((c3 & 0x03) << 6); + o |= c4; + rs.append((char)o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) + ret[off] = (byte)rs.charAt(off); + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as + * two 32-bit halves + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private final void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = (word << 8) | (data[off] & 0xff); + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = (int[])P_orig.clone(); + S = (int[])S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * @param key an array containing the key + */ + private void key(byte key[]) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by + * Provos and Mazieres in "A Future-Adaptable Password Scheme" + * http://www.openbsd.org/papers/bcrypt-paper.ps + * @param data salt information + * @param key password information + */ + private void ekskey(byte data[], byte key[]) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the central password hashing step in the + * bcrypt scheme + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number + * of rounds of hashing to apply + * @param cdata the plaintext to encrypt + * @return an array containing the binary hashed password + */ + public byte[] crypt_raw(byte password[], byte salt[], int log_rounds, + int cdata[]) { + int rounds, i, j; + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 30) + throw new IllegalArgumentException ("Bad number of rounds"); + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) + throw new IllegalArgumentException ("Bad salt length"); + + init_key(); + ekskey(salt, password); + for (i = 0; i != rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) + encipher(cdata, j << 1); + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte)((cdata[i] >> 24) & 0xff); + ret[j++] = (byte)((cdata[i] >> 16) & 0xff); + ret[j++] = (byte)((cdata[i] >> 8) & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + return ret; + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated + * using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(String password, String salt) { + BCryptService B; + String real_salt; + byte passwordb[], saltb[], hashed[]; + char minor = (char)0; + int rounds, off = 0; + StringBuffer rs = new StringBuffer(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') + throw new IllegalArgumentException ("Invalid salt version"); + if (salt.charAt(2) == '$') + off = 3; + else { + minor = salt.charAt(2); + // Note ljacqu 20160118: Added check to also allow minor version 'y' + // cf. https://security.stackexchange.com/questions/20541/insecure-versions-of-crypt-hashes + if ((minor != 'a' && minor != 'y') || salt.charAt(3) != '$') + throw new IllegalArgumentException ("Invalid salt revision"); + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') + throw new IllegalArgumentException ("Missing salt rounds"); + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + try { + passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + throw new AssertionError("UTF-8 is not supported"); + } + + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + B = new BCryptService(); + hashed = B.crypt_raw(passwordb, saltb, rounds, + (int[])bf_crypt_ciphertext.clone()); + + rs.append("$2"); + if (minor >= 'a') + rs.append(minor); + rs.append("$"); + if (rounds < 10) + rs.append("0"); + if (rounds > 30) { + throw new IllegalArgumentException( + "rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(rounds)); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, + bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + StringBuffer rs = new StringBuffer(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) + rs.append("0"); + if (log_rounds > 30) { + throw new IllegalArgumentException( + "log_rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(log_rounds)); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method, + * selecting a reasonable default for the number of hashing + * rounds to apply + * @return an encoded salt value + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Check that a plaintext password matches a previously hashed + * one + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + */ + public static boolean checkpw(String plaintext, String hashed) { + byte hashed_bytes[]; + byte try_bytes[]; + try { + String try_pw = hashpw(plaintext, hashed); + hashed_bytes = hashed.getBytes("UTF-8"); + try_bytes = try_pw.getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + return false; + } + if (hashed_bytes.length != try_bytes.length) + return false; + byte ret = 0; + for (int i = 0; i < try_bytes.length; i++) + ret |= hashed_bytes[i] ^ try_bytes[i]; + return ret == 0; + } +} 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 cbd77fc66..a81503374 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WBB4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/WBB4.java @@ -1,31 +1,30 @@ package fr.xephi.authme.security.crypts; -import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; -import fr.xephi.authme.util.StringUtils; -@Recommendation(Usage.DOES_NOT_WORK) +import static fr.xephi.authme.security.crypts.BCryptService.hashpw; + +@Recommendation(Usage.RECOMMENDED) public class WBB4 extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { - return BCRYPT.getDoubleHash(password, salt); + return hashpw(hashpw(password, salt), salt); } @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { - try { - return BCRYPT.checkpw(password, hashedPassword.getHash(), 2); - } catch (IllegalArgumentException e) { - ConsoleLogger.showError("WBB4 compare password returned: " + StringUtils.formatException(e)); + if (hashedPassword.getHash().length() != 60) { + return false; } - return false; + String salt = hashedPassword.getHash().substring(0, 29); + return computeHash(password, salt, null).equals(hashedPassword.getHash()); } @Override public String generateSalt() { - return BCRYPT.gensalt(8); + return BCryptService.gensalt(8); } /** 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 75c6a7911..4f7ed04ca 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XFBCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XFBCRYPT.java @@ -1,15 +1,44 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.util.StringUtils; + import java.util.regex.Matcher; import java.util.regex.Pattern; -public class XFBCRYPT extends BCRYPT { +public class XFBCRYPT implements EncryptionMethod { public static final String SCHEME_CLASS = "XenForo_Authentication_Core12"; private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\";s.*\"(.*)?\""); @Override public String generateSalt() { - return BCRYPT.gensalt(); + return BCryptService.gensalt(); + } + + @Override + public String computeHash(String password, String salt, String name) { + return BCryptService.hashpw(password, salt); + } + + @Override + public HashedPassword computeHash(String password, String name) { + String salt = generateSalt(); + return new HashedPassword(BCryptService.hashpw(password, salt), null); + } + + @Override + public boolean comparePassword(String password, HashedPassword hash, String salt) { + try { + return hash.getHash().length() > 3 && BCryptService.checkpw(password, hash.getHash()); + } catch (IllegalArgumentException e) { + ConsoleLogger.showError("XfBCrypt checkpw() returned " + StringUtils.formatException(e)); + } + return false; + } + + @Override + public boolean hasSeparateSalt() { + return false; } public static String getHashFromBlob(byte[] blob) { diff --git a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java index ef78c5fda..73e52fa39 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java @@ -26,8 +26,8 @@ public abstract class AbstractEncryptionMethodTest { */ public static final String[] GIVEN_PASSWORDS = {"password", "PassWord1", "&^%te$t?Pw@_", "âË_3(íù*"}; /** - * List of passwords that are hashed at runtime and then tested against; this verifies that hashes that are - * generated are valid. + * List of passwords that are hashed at runtime and then tested against; this verifies that newly generated hashes + * are valid. */ private static final List INTERNAL_PASSWORDS = ImmutableList.of("test1234", "Ab_C73", "(!#&$~`_-Aa0", "Ûïé1&?+A"); diff --git a/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java b/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java index 3579be2f0..5b714cab3 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java @@ -1,20 +1,16 @@ package fr.xephi.authme.security.crypts; -import org.junit.Ignore; - /** * Test for {@link WBB4}. */ -@Ignore -// TODO #369: Fix WBB4 hash and un-ignore this test public class WBB4Test extends AbstractEncryptionMethodTest { public WBB4Test() { super(new WBB4(), - "$2a$08$GktrHRoOk0EHrl3ONsFmieIbjq7EIzBx8dhsWiCmn6sWwO3b3DoRO", // password - "$2a$08$ouvtovnHgPWz6YHuOhyct.I2/j1xTOLG8OTuEn1/YqtkiRJYUV7lq", // PassWord1 - "$2a$08$z.qWFh7k0qvIu5.qiq/Wuu2HDCNH7LNlMDNhN61F1ISsV8wZRKD0.", // &^%te$t?Pw@_ - "$2a$08$OU8e9dncXyz8UP5Z.gWP8Os1IK89pspCS4FPzj8hBjgCWmjbLVcO2"); // âË_3(íù* + "$2a$08$7DGr.wROqEPe0Z3XJS7n5.k.QWehovLHbpI.UkdfRb4ns268WsR6C", // password + "$2a$08$yWWVUA4PB4mqW.0wyIvV3OdoH492HuLk5L3iaqUrpRK2.2zn08d/K", // PassWord1 + "$2a$08$EHXUFt7bTT9Fnsu22KWvF.QDssiosV8YzH8CyWqulB/ckOA7qioJG", // &^%te$t?Pw@_ + "$2a$08$ZZu5YH4zwpk0cr2dOYZpF.CkTKMvCBOAtTbAH7AwnOiL.n0mWkgDC"); // âË_3(íù* } } From 07e7a8815b4b34233bc25f93b37b92dbe93cbe1e Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 18 Jan 2016 14:19:04 +0100 Subject: [PATCH 20/26] Fix #391 Wordpress algorithm fails sometimes --- .../xephi/authme/security/crypts/WORDPRESS.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) 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 8ecb41465..f331d1fc6 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WORDPRESS.java +++ b/src/main/java/fr/xephi/authme/security/crypts/WORDPRESS.java @@ -1,5 +1,7 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.MessageDigestAlgorithm; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; @@ -7,12 +9,10 @@ import fr.xephi.authme.security.crypts.description.Usage; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; -// TODO #391: Wordpress algorithm fails sometimes. Fix it and change the Recommendation to "ACCEPTABLE" if appropriate -@Recommendation(Usage.DO_NOT_USE) +@Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 9) // Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally // and isn't exposed to the outside, so we treat it as an unsalted implementation @@ -30,6 +30,7 @@ public class WORDPRESS extends UnsaltedMethod { byte[] t = new byte[count]; System.arraycopy(src, 0, t, 0, src.length); Arrays.fill(t, src.length, count - 1, (byte) 0); + src = t; } do { @@ -73,13 +74,7 @@ public class WORDPRESS extends UnsaltedMethod { if (salt.length() != 8) { return output; } - MessageDigest md; - try { - md = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return output; - } + MessageDigest md = HashUtils.getDigest(MessageDigestAlgorithm.MD5); byte[] pass = stringToUtf8(password); byte[] hash = md.digest(stringToUtf8(salt + password)); do { From 393f1a0f3650b3a8f0742e7a6d9d5bcc176d8128 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 18 Jan 2016 15:17:02 +0100 Subject: [PATCH 21/26] Minor: replace self-closing tags in javadoc --- .../authme/security/crypts/BCryptService.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java index c2f58f605..94ed32c3c 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java @@ -33,25 +33,25 @@ import java.security.SecureRandom; * call the hashpw method with a random salt, like this: *

* - * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
*
*

* To check whether a plaintext password matches one that has been * hashed previously, use the checkpw method: *

* - * if (BCrypt.checkpw(candidate_password, stored_hash))
- *     System.out.println("It matches");
- * else
- *     System.out.println("It does not match");
+ * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
*
*

* The gensalt() method takes an optional parameter (log_rounds) * that determines the computational complexity of the hashing: *

* - * String strong_salt = BCrypt.gensalt(10)
- * String stronger_salt = BCrypt.gensalt(12)
+ * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
*
*

* The amount of work increases exponentially (2**log_rounds), so From 5b2909d7d69e56ad5c429a99cd9cbb79d415bfc7 Mon Sep 17 00:00:00 2001 From: games647 Date: Mon, 18 Jan 2016 20:34:49 +0100 Subject: [PATCH 22/26] Wait for unfinished tasks of the Bukkit scheduler before closing the executor service --- src/main/java/fr/xephi/authme/AuthMe.java | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 15e861bda..51bac6980 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -74,9 +74,11 @@ import java.net.URL; import java.util.Calendar; import java.util.Collection; import java.util.Date; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -535,6 +537,40 @@ public class AuthMe extends JavaPlugin { moduleManager.unloadModules(); } + List pendingTasks = getServer().getScheduler().getPendingTasks(); + for (Iterator iterator = pendingTasks.iterator(); iterator.hasNext();) { + BukkitTask pendingTask = iterator.next(); + if (pendingTask.getOwner().equals(this) || pendingTask.isSync()) { + //remove all unrelevant tasks + iterator.remove(); + } + } + + getLogger().log(Level.INFO, "Waiting for {0} tasks to finish", pendingTasks.size()); + int progress = 0; + try { + for (BukkitTask pendingTask : pendingTasks) { + int maxTries = 5; + int taskId = pendingTask.getTaskId(); + while (getServer().getScheduler().isCurrentlyRunning(taskId)) { + if (maxTries <= 0) { + getLogger().log(Level.INFO, "Async task {0} times out after to many tries", taskId); + break; + } + + //one second + Thread.sleep(1000); + maxTries--; + } + + progress++; + getLogger().log(Level.INFO, "Progress: {0} / {1}", new Object[]{progress, pendingTasks.size()}); + } + } catch (InterruptedException interruptedException) { + + } + + // Close the database if (database != null) { database.close(); From 562ed40f70aae289cf1f2080ca69281f5c094cdd Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 18 Jan 2016 21:40:47 +0100 Subject: [PATCH 23/26] add new configuration file [WIP] --- NewConfig.yml | 570 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 NewConfig.yml diff --git a/NewConfig.yml b/NewConfig.yml new file mode 100644 index 000000000..7c79362d1 --- /dev/null +++ b/NewConfig.yml @@ -0,0 +1,570 @@ +# ======================================================================================================= +# _____ __ .__ _____ __________ .__ .___ .___ +# / _ \ __ ___/ |_| |__ / \ ____\______ \ ____ | | _________ __| _/____ __| _/ +# / /_\ \| | \ __| | \ / \ / \_/ __ \| __/ __ \| | / _ \__ \ / __ _/ __ \ / __ | +# / | | | /| | | Y / Y \ ___/| | \ ___/| |_( <_> / __ \/ /_/ \ ___// /_/ | +# \____|__ |____/ |__| |___| \____|__ /\___ |____|_ /\___ |____/\____(____ \____ |\___ \____ | +# \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ +# +# ======================================================================================================= +# +# Authme Main Configuration File. +# +# ======================================================================================================= + +# Plugin infos (overwritten on start, just a simple way to find out your plugin version). +authors: ${pluginAuthors} +version: ${project.version} +buildNumber: ${buildNumber} + +# Set this setting to true when you have configured the plugin, +# when false the server will be stopped with a warning message. +enabled: false + +# Database settings. +data_source: + # =========================== + # Database general settings. + # =========================== + + # Database backend (sqlite, mysql). + backend: sqlite + # Enable database queries caching, should improve performance. + caching: true + + # =========================== + # SqLite db parameters. + # =========================== + + sqlite: + # The name of the database storage file. + filename: 'authme.db' + + # =========================== + # MySql db parameters. + # =========================== + + mysql: + # Connection parameters. + host: '127.0.0.1' + port: 3306 + username: 'change_me' + password: 'change_me' + database: 'my_minecraft_server' + tablename: 'authme' + + # Column names. + column_names: + id: id + # Column for storing nicknames (ignore case nickname). + name: username + # Column for storing the realname (case sensitive nickname). + real_name: realname + # Column for storing passwords. + password: password + # Column for storing email addresses. + email: email + # Column for storing the authentication status (logged or not). + login_status: isLogged + # Column for storing player IPs. + ip: ip + # Column for storing lastlogins date and time. + last_login_timestamp: lastlogin + # Latest logout location of the players. + last_location: + world: world + x: x + y: y + z: z + # Enabled only if the bungeecord integration is activated. + server: world + + # Support for registrations via WebInterfaces/CSM. + # Disable some backend caching parameters. + disableAggressiveCaching: false + +# Main settings +settings: + + # =========================== + # Bungeecord integration + # =========================== + + bungeecord: + # Enable bungeecord integration features + enabled: true + + # Server name (must be unique, please use the name in the bungeecord configuration). + # Use 'auto' for auto configuration (requires the bungeecord module). + serverName: LoginLobby1 + # Keep the auth status when the player moves between servers. + # Required if you're using the bungeecord module. + keepAuthBetweenServers: true + + # Target server after login + send_after_login: + enabled: false + message: '' + delay: 5 + # Server name ("ServerName") or group ("G:GroupName") + # Groups are avariable only when the bungeecord module is avariable. + # If the server change fails the player will be kicked. + target: Lobby1 + failKickMessage: 'Failed to connect to the lobby! Please try to join the server again!' + # Target server after logout + send_after_logout: + enabled: false + message: '' + delay: 5 + # Server name ("ServerName") or group ("G:GroupName") + # Groups are avariable only when the bungeecord module is avariable. + # If the server change fails the player will be kicked. + target: LoginLobby1 + failKickMessage: 'Failed to connect to the lobby! Please try to join the server again!' + + # Variables: + # %p playername + bungee_commands: + player_command_after_register: + enabled: false + cmd: '' + console_command_after_register: + enabled: false + cmd: 'alert %p joined for the first time the network!' + player_command_after_login: + enabled: false + cmd: 'glist' + console_command_after_login: + enabled: false + cmd: 'alert %p logged in correctly!' + player_command_after_join: + enabled: false + cmd: '' + console_command_after_join: + enabled: false + cmd: 'alert %p joined the network!' + player_command_first_join: + enabled: false + cmd: '' + console_command_first_join: + enabled: false + cmd: 'alert %p joined for the first time the network!' + + # =========================== + # Sessions configuration. + # =========================== + + sessions: + # Enable sessions. + # When a player is authenticated, his IP and his nickname is saved. + # The next time the player will join the server, if his IP is the same + # of the last time, and the timeout time hasn't expired, he will be + # automatically authenticated. + enabled: false + # Session timeout. + # 0 for unlimited time (Very dangerous, use it at your own risk!) + # Consider that if player's ip has changed but the timeout hasn't + # expired, player will be kicked out of the sever! + timeout: 10 + # When enabled a player's session will expire if someone tries to + # login with a different IP Address. + expire_on_ip_change: true + + # =========================== + # Registration settings. + # =========================== + + registration: + # After how many time unregistered players should be kicked? + # Set to 0 to disable. (default: 30) + timeout: 30 + + nickname: + min_length: 4 + max_lenght: 16 + # Regex syntax. + allowed_characters: '[a-zA-Z0-9_]*' + + password: + # Enable double check of password on registration: + # /register + double_check: true + # Minimum password lenght. + min_length: 5 + # Regex syntax. + allowed_characters: '[\x21-\x7E]*' + # Denied unsafe passwords. + unsafePasswords: + - '123456' + - 'password' + - 'qwerty' + - '12345' + - '54321' + + # =========================== + # Login settings. + # =========================== + + login: + # After how many time unlogged players should be kicked? + # Set to 0 to disable. (default: 30) + timeout: 30 + + + + # =========================== + # Encryption parameters. + # =========================== + + password_encryption: + # The hashing algorithm. + # Possible values: MD5, SHA1, SHA256, WHIRLPOOL, XAUTH, MD5VB, PHPBB, MYBB, IPB3, + # PHPFUSION, SMF, XENFORO, SALTED2MD5, JOOMLA, BCRYPT, WBB3, SHA512, DOUBLEMD5, + # PBKDF2, PBKDF2DJANGO, WORDPRESS, ROYALAUTH, CUSTOM (developpers only). + encryption_algorithm: SHA256 + # The salt length for the SALTED2MD5 and MD5(MD5(password)+salt) algorithms. + md5_salt_length: 8 + # If password check fails try all the other hash algorithm. + # AuthMe will update the password to the new passwordHash. + enable_convertion: false + + # =========================== + # Unlogged user restrictions. + # =========================== + + unlogged_restrictions: + # Deny chat messages send for unlogged users. + deny_chat: true + # Hide chat to unlogged users. + # Only player messages, plugins will be able to send messages to the player anyway. + hide_chat: false + + # Deny any command message not in the whitelist below. + deny_commands: true + command_whitelist: + - /login + - /register + - /l + - /reg + - /email + - /captcha + + movements: + # Restrict player movements. + restrict: true + # Allowed radius. + allowed_radius: 0 + # Should unlogged players have speed = 0? + # After the login the walking/flying speeed will be reset to the default value. + removeSpeed: true + + # End is there atm xD + + # This option will save the quit location of the players. + SaveQuitLocation: false + # Should not logged in players be teleported to the spawn? + # After the authentication, if SaveQuitLocation is enabled, + # they will be teleported back to their normal position. + teleportUnAuthedToSpawn: false + + # If enabled, after the login, if the ForceSpawnOnTheseWorlds setting contains + # the player's world, he will be teleported to the world spawnpoint. + # The quit location of the player will be overwritten. + # This is different from "teleportUnAuthedToSpawn" that teleports player + # back to his quit location after the authentication. + ForceSpawnLocOnJoinEnabled: false + # WorldNames where we need to force the spawn location + # Warning: This setting is Case Sensitive! + ForceSpawnOnTheseWorlds: + - world + - world_nether + - world_the_end + + # this is very important options, + # every time player join the server, + # if they are registered, AuthMe will switch him + # to unLoggedInGroup, this + # should prevent all major exploit. + # So you can set up on your Permission Plugin + # this special group with 0 permissions, or permissions to chat, + # or permission to + # send private message or all other perms that you want, + # the better way is to set up + # this group with few permissions, + # so if player try to exploit some account, + # they can + # do anything except what you set in perm Group. + # After a correct logged-in player will be + # moved to his correct permissions group! + # Pay attention group name is case sensitive, + # so Admin is different from admin, + # otherwise your group will be wiped, + # and player join in default group []! + # Example unLoggedinGroup: NotLogged + unLoggedinGroup: unLoggedinGroup + + # =========================== + # Address restrictions + # =========================== + + # Max number of registrations per IP (default: 1) + maxRegPerIp: 1 + # Maximum allowed number of Logins per IP, 0 to disable (default: 0) + maxLoginPerIp: 0 + # Maximum allowed number of Joins per IP, 0 to disable (default: 0) + maxJoinPerIp: 0 + + # When this setting is enabled, online players can't be kicked out + # due to "Logged in from another Location" + # This setting will prevent potetial security exploits. + ForceSingleSession: true + + # To activate the restricted user feature you need + # to enable this option and configure the + # AllowedRestrctedUser field. + AllowRestrictedUser: false + # The restricted user feature will kick players listed below + # if they dont match of the defined ip address. + # Example: + # AllowedRestrictedUser: + # - playername;127.0.0.1 + AllowedRestrictedUser: + - playername;127.0.0. + # Ban ip when the ip is not the ip registered in database + banUnsafedIP: false + + + + # =============================== + # Other restrictions + # =============================== + + # Should we protect the player inventory before logging in? + # Warning: Requires the latest version of ProtocolLib! + ProtectInventoryBeforeLogIn: true + + # Should unregistered players be kicked immediately? + kickNonRegistered: false + # Should players be kicked on wrong password? + kickOnWrongPassword: false + + # Should we display all other accounts of a player when he joins? + # Required permission: authme.admin.accounts + displayOtherAccounts: true + + # =============================== + # Restrictions compatibility + # =============================== + + # Spawn Priority. Avariable values : authme, essentials, multiverse, default + spawnPriority: authme,essentials,multiverse,default + # AuthMe will NEVER teleport players! + noTeleport: false + + GameMode: + # Do you want to set player's gamemode to survival when he joins? + # This enables also the settings below. + ForceSurvivalMode: false + # Do you want to reset player's inventory if player joins with creative mode? + ResetInventoryIfCreative: false + # Do you want to force the survival mode ONLY after the /login process? + ForceOnlyAfterLogin: false + + # sgdc3: Ok, our configuration is shit.... xD Today I will stop there + + + registration: + # enable registration on the server? + enabled: true + # Send every X seconds a message to a player to + # remind him that he has to login/register + messageInterval: 5 + # Only registered and logged in players can play. + # See restrictions for exceptions + force: true + # Does we replace password registration by an Email registration method ? + enableEmailRegistrationSystem: false + # Enable double check of email when you register + # when it's true, registration require that kind of command: + # /register + doubleEmailCheck: false + # Do we force kicking player after a successful registration ? + # Do not use with login feature below + forceKickAfterRegister: false + # Does AuthMe need to enforce a /login after a successful registration ? + forceLoginAfterRegister: false + unrestrictions: + # below you can list all your account name, that + # AuthMe will ignore for registration or login, configure it + # at your own risk!! Remember that if you are going to add + # nickname with [], you have to delimit name with ' '. + # this option add compatibility with BuildCraft and some + # other mods. + # It is CaseSensitive! + UnrestrictedName: [] + # Message language, available : en, de, br, cz, pl, fr, ru, hu, sk, es, zhtw, fi, zhcn, lt, it, ko, pt + messagesLanguage: en + # Force these commands after /login, without any '/', use %p for replace with player name + forceCommands: [] + # Force these commands after /login as a server console, without any '/', use %p for replace with player name + forceCommandsAsConsole: [] + # Force these commands after /register, without any '/', use %p for replace with player name + forceRegisterCommands: [] + # Force these commands after /register as a server console, without any '/', use %p for replace with player name + forceRegisterCommandsAsConsole: [] + # Do we need to display the welcome message (welcome.txt) after a register or a login? + # You can use colors in this welcome.txt + some replaced strings : + # {PLAYER} : player name, {ONLINE} : display number of online players, {MAXPLAYERS} : display server slots, + # {IP} : player ip, {LOGINS} : number of players logged, {WORLD} : player current world, {SERVER} : server name + # {VERSION} : get current bukkit version, {COUNTRY} : player country + useWelcomeMessage: true + # Do we need to broadcast the welcome message to all server or only to the player? set true for server or false for player + broadcastWelcomeMessage: false + # Do we need to delay the join/leave message to be displayed only when the player is authenticated ? + delayJoinLeaveMessages: true + # Do we need to add potion effect Blinding before login/register ? + applyBlindEffect: false +ExternalBoardOptions: + # MySQL column for the salt , needed for some forum/cms support + mySQLColumnSalt: '' + # MySQL column for the group, needed for some forum/cms support + mySQLColumnGroup: '' + # -1 mean disabled. If u want that only + # activated player can login in your server + # u can put in this options the group number + # of unactivated user, needed for some forum/cms support + nonActivedUserGroup: -1 + # Other MySQL columns where we need to put the Username (case sensitive) + mySQLOtherUsernameColumns: [] + # How much Log to Round needed in BCrypt(do not change it if you do not know what's your doing) + bCryptLog2Round: 10 + # phpBB prefix defined during phpbb installation process + phpbbTablePrefix: 'phpbb_' + # phpBB activated group id , 2 is default registered group defined by phpbb + phpbbActivatedGroupId: 2 + # WordPress prefix defined during WordPress installation process + wordpressTablePrefix: 'wp_' +permission: + # Take care with this options, if you dont want + # to use Vault and Group Switching of + # AuthMe for unloggedIn players put true + # below, default is false. + EnablePermissionCheck: false +BackupSystem: + # Enable or Disable Automatic Backup + ActivateBackup: false + # set Backup at every start of Server + OnServerStart: false + # set Backup at every stop of Server + OnServerStop: true + # Windows only mysql installation Path + MysqlWindowsPath: 'C:\\Program Files\\MySQL\\MySQL Server 5.1\\' +Security: + SQLProblem: + # Stop the server if we can't contact the sql database + # Take care with this, if you set that to false, + # AuthMe automatically disable and the server is not protected! + stopServer: true + ReloadCommand: + # /reload support + useReloadCommandSupport: true + console: + # Remove spam console + noConsoleSpam: false + # Replace passwords in the console when player type a command like /login + removePassword: true + captcha: + # Player need to put a captcha when he fails too lot the password + useCaptcha: false + # Max allowed tries before request a captcha + maxLoginTry: 5 + # Captcha length + captchaLength: 5 +Converter: + Rakamak: + # Rakamak file name + fileName: users.rak + # Rakamak use ip ? + useIP: false + # IP file name for rakamak + ipFileName: UsersIp.rak + CrazyLogin: + # CrazyLogin database file + fileName: accounts.db +Email: + # Email SMTP server host + mailSMTP: smtp.gmail.com + # Email SMTP server port + mailPort: 465 + # Email account that send the mail + mailAccount: '' + # Email account password + mailPassword: '' + # Custom SenderName, that replace the mailAccount name in the email + mailSenderName: '' + # Random password length + RecoveryPasswordLength: 8 + # Email subject of password get + mailSubject: 'Your new AuthMe Password' + # Email text here + mailText: 'Dear ,

This is your new AuthMe password for the server

:



Do not forget to change password after login!
/changepassword newPassword' + # Like maxRegPerIp but with email + maxRegPerEmail: 1 + # Recall players to add an email ? + recallPlayers: false + # Delay in minute for the recall scheduler + delayRecall: 5 + # Blacklist these domains for emails + emailBlacklisted: + - 10minutemail.com + # WhiteList only these domains for emails + emailWhitelisted: [] + # Do we need to send new password draw in an image ? + generateImage: false +Hooks: + # Do we need to hook with multiverse for spawn checking? + multiverse: true + # Do we need to hook with BungeeCord for get the real Player ip ? + bungeecord: false + # Do we need to disable Essentials SocialSpy on join ? + disableSocialSpy: true + # Do we need to force /motd Essentials command on join ? + useEssentialsMotd: false + # Do we need to cache custom Attributes ? + customAttributes: false +Purge: + # On Enable , does AuthMe need to purge automatically old accounts unused ? + useAutoPurge: false + # Number of Days an account become Unused + daysBeforeRemovePlayer: 60 + # Do we need to remove the player.dat file during purge process ? + removePlayerDat: false + # Do we need to remove the Essentials/users/player.yml file during purge process ? + removeEssentialsFile: false + # World where are players.dat stores + defaultWorld: 'world' + # Do we need to remove LimitedCreative/inventories/player.yml , player_creative.yml files during purge process ? + removeLimitedCreativesInventories: false + # Do we need to remove the AntiXRayData/PlayerData/player file during purge process ? + removeAntiXRayFile: false + # Do we need to remove permissions ? + removePermissions: false +Protection: + # Enable some servers protection ( country based login, antibot ) + enableProtection: false + # Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes + countries: + - US + - GB + # Countries blacklisted automatically ( without any needed to enable protection ) + countriesBlacklist: + - A1 + # Do we need to enable automatic antibot system? + enableAntiBot: false + # Max number of player allowed to login in 5 secs before enable AntiBot system automatically + antiBotSensibility: 5 + # Duration in minutes of the antibot automatic system + antiBotDuration: 10 +VeryGames: + # These features are only available on VeryGames Server Provider + enableIpCheck: false From a3a3e94cd474f858981c827241f55b22e4472916 Mon Sep 17 00:00:00 2001 From: games647 Date: Mon, 18 Jan 2016 22:00:17 +0100 Subject: [PATCH 24/26] Fix logic of waiting unfinished tasks --- src/main/java/fr/xephi/authme/AuthMe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 51bac6980..1b39b3440 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -540,7 +540,7 @@ public class AuthMe extends JavaPlugin { List pendingTasks = getServer().getScheduler().getPendingTasks(); for (Iterator iterator = pendingTasks.iterator(); iterator.hasNext();) { BukkitTask pendingTask = iterator.next(); - if (pendingTask.getOwner().equals(this) || pendingTask.isSync()) { + if (!pendingTask.getOwner().equals(this) || pendingTask.isSync()) { //remove all unrelevant tasks iterator.remove(); } From dd03045bab0dfed1da6917b66094e02cd2eef355 Mon Sep 17 00:00:00 2001 From: games647 Date: Mon, 18 Jan 2016 22:11:16 +0100 Subject: [PATCH 25/26] Fix typo for delay join message configuration loading --- src/main/java/fr/xephi/authme/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 42ee6ed48..9f4285b64 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -284,7 +284,7 @@ public final class Settings { getMaxLoginPerIp = configFile.getInt("settings.restrictions.maxLoginPerIp", 0); getMaxJoinPerIp = configFile.getInt("settings.restrictions.maxJoinPerIp", 0); checkVeryGames = configFile.getBoolean("VeryGames.enableIpCheck", false); - delayJoinLeaveMessages = configFile.getBoolean("settings.delayJoinLeaveMessage", false); + delayJoinLeaveMessages = configFile.getBoolean("settings.delayJoinLeaveMessages", false); noTeleport = configFile.getBoolean("settings.restrictions.noTeleport", false); crazyloginFileName = configFile.getString("Converter.CrazyLogin.fileName", "accounts.db"); getPassRegex = configFile.getString("settings.restrictions.allowedPasswordCharacters", "[\\x21-\\x7E]*"); From 0aa6f753be5c9f9d0167860745064ac19e545edc Mon Sep 17 00:00:00 2001 From: games647 Date: Tue, 19 Jan 2016 14:31:29 +0100 Subject: [PATCH 26/26] Set utf-8 encoding for MySQL connections Fixes Xephi/AuthMeReloaded#458 --- src/main/java/fr/xephi/authme/datasource/MySQL.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 9e459ad71..3c48c6da0 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -110,6 +110,12 @@ public class MySQL implements DataSource { ds.addDataSourceProperty("cachePrepStmts", "true"); ds.addDataSourceProperty("prepStmtCacheSize", "250"); ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + //set utf-8 as default encoding + ds.addDataSourceProperty("characterEncoding", "utf8"); + ds.addDataSourceProperty("encoding","UTF-8"); + ds.addDataSourceProperty("useUnicode", "true"); + ds.setUsername(this.username); ds.setPassword(this.password); ds.setInitializationFailFast(true); // Don't start the plugin if the database is unavailable