diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 0e8c28ab..1c1240a2 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -316,7 +316,7 @@ public class CommandInitializer { .description("Converter command") .detailedDescription("Converter command for AuthMeReloaded.") .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " - + "royalauth / vauth / sqliteToSql / mysqlToSqlite", false) + + "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", false) .permission(AdminPermission.CONVERTER) .executableCommand(ConverterCommand.class) .register(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index c59c4cb9..ac542c73 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -6,6 +6,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.converter.Converter; import fr.xephi.authme.datasource.converter.CrazyLoginConverter; +import fr.xephi.authme.datasource.converter.LoginSecurityConverter; import fr.xephi.authme.datasource.converter.MySqlToSqlite; import fr.xephi.authme.datasource.converter.RakamakConverter; import fr.xephi.authme.datasource.converter.RoyalAuthConverter; @@ -85,6 +86,7 @@ public class ConverterCommand implements ExecutableCommand { .put("vauth", VAuthConverter.class) .put("sqlitetosql", SqliteToSql.class) .put("mysqltosqlite", MySqlToSqlite.class) + .put("loginsecurity", LoginSecurityConverter.class) .build(); } diff --git a/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java b/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java new file mode 100644 index 00000000..b8eabbe4 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java @@ -0,0 +1,162 @@ +package fr.xephi.authme.datasource.converter; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.ConverterSettings; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import static fr.xephi.authme.util.Utils.logAndSendMessage; + +/** + * Converts data from LoginSecurity to AuthMe. + */ +public class LoginSecurityConverter implements Converter { + + private final File dataFolder; + private final DataSource dataSource; + + private final boolean useSqlite; + private final String mySqlHost; + private final String mySqlDatabase; + private final String mySqlUser; + private final String mySqlPassword; + + @Inject + LoginSecurityConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings) { + this.dataFolder = dataFolder; + this.dataSource = dataSource; + + useSqlite = settings.getProperty(ConverterSettings.LOGINSECURITY_USE_SQLITE); + mySqlHost = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_HOST); + mySqlDatabase = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_DATABASE); + mySqlUser = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_USER); + mySqlPassword = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_PASSWORD); + } + + @Override + public void execute(CommandSender sender) { + try (Connection connection = createConnectionOrInformSender(sender)) { + if (connection != null) { + performConversion(sender, connection); + } + } catch (SQLException e) { + sender.sendMessage("Failed to convert from SQLite. Please see the log for more info"); + ConsoleLogger.logException("Could not fetch or migrate data:", e); + } + } + + @VisibleForTesting + void performConversion(CommandSender sender, Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "SELECT * from ls_players LEFT JOIN ls_locations ON ls_locations.id = ls_players.id"); + try (ResultSet resultSet = statement.getResultSet()) { + migrateData(sender, resultSet); + } + } + } + + private void migrateData(CommandSender sender, ResultSet resultSet) throws SQLException { + List skippedPlayers = new ArrayList<>(); + long successfulSaves = 0; + while (resultSet.next()) { + String name = resultSet.getString("last_name"); + if (dataSource.isAuthAvailable(name)) { + skippedPlayers.add(name); + } else { + PlayerAuth auth = buildAuthFromLoginSecurity(name, resultSet); + dataSource.saveAuth(auth); + ++successfulSaves; + } + } + + logAndSendMessage(sender, "Migrated " + successfulSaves + " accounts successfully from LoginSecurity"); + if (!skippedPlayers.isEmpty()) { + logAndSendMessage(sender, "Skipped conversion for players which were already in AuthMe: " + + String.join(", ", skippedPlayers)); + } + } + + private static PlayerAuth buildAuthFromLoginSecurity(String name, ResultSet resultSet) throws SQLException { + return PlayerAuth.builder() + .name(name) + .realName(name) + .password(resultSet.getString("password"), null) + .ip(resultSet.getString("ip_address")) + .lastLogin(resultSet.getLong("last_login")) + // TODO #792: Register date + .locX(resultSet.getDouble("x")) + .locY(resultSet.getDouble("y")) + .locZ(resultSet.getDouble("z")) + .locWorld(resultSet.getString("world")) + .locYaw(resultSet.getFloat("yaw")) + .locPitch(resultSet.getFloat("pitch")) + .build(); + } + + private Connection createConnectionOrInformSender(CommandSender sender) { + Connection connection; + if (useSqlite) { + File sqliteDatabase = new File(dataFolder.getParentFile(), "LoginSecurity/LoginSecurity.db"); + if (!sqliteDatabase.exists()) { + sender.sendMessage("The file '" + sqliteDatabase.getPath() + "' does not exist"); + return null; + } + connection = createSqliteConnection("plugins/LoginSecurity/LoginSecurity.db"); + } else { + if (mySqlDatabase.isEmpty() || mySqlUser.isEmpty()) { + sender.sendMessage("The LoginSecurity database or username is not configured in AuthMe's config.yml"); + return null; + } + connection = createMySqlConnection(); + } + + if (connection == null) { + sender.sendMessage("Could not connect to LoginSecurity using Sqlite = " + + useSqlite + ", see log for more info"); + return null; + } + return connection; + } + + @VisibleForTesting + Connection createSqliteConnection(String path) { + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + + try { + return DriverManager.getConnection( + "jdbc:sqlite:" + path, "trump", "donald"); + } catch (SQLException e) { + ConsoleLogger.logException("Could not connect to SQLite database", e); + return null; + } + } + + private Connection createMySqlConnection() { + try { + return DriverManager.getConnection( + "jdbc:mysql://" + mySqlHost + "/" + mySqlDatabase, mySqlUser, mySqlPassword); + } catch (SQLException e) { + ConsoleLogger.logException("Could not connect to SQLite database", e); + return null; + } + } +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java index d2b34c9a..c845b3bf 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java @@ -24,6 +24,26 @@ public final class ConverterSettings implements SettingsHolder { public static final Property CRAZYLOGIN_FILE_NAME = newProperty("Converter.CrazyLogin.fileName", "accounts.db"); + @Comment("LoginSecurity: convert from SQLite; if false we use MySQL") + public static final Property LOGINSECURITY_USE_SQLITE = + newProperty("Converter.loginSecurity.useSqlite", true); + + @Comment("LoginSecurity MySQL: database host") + public static final Property LOGINSECURITY_MYSQL_HOST = + newProperty("Converter.loginSecurity.mySql.host", ""); + + @Comment("LoginSecurity MySQL: database name") + public static final Property LOGINSECURITY_MYSQL_DATABASE = + newProperty("Converter.loginSecurity.mySql.database", ""); + + @Comment("LoginSecurity MySQL: database user") + public static final Property LOGINSECURITY_MYSQL_USER = + newProperty("Converter.loginSecurity.mySql.user", ""); + + @Comment("LoginSecurity MySQL: password for database user") + public static final Property LOGINSECURITY_MYSQL_PASSWORD = + newProperty("Converter.loginSecurity.mySql.password", ""); + private ConverterSettings() { } diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index 5017c476..ee7aaa5e 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -30,6 +30,7 @@ public final class TestHelper { public static final String SOURCES_FOLDER = "src/main/java/"; public static final String TEST_SOURCES_FOLDER = "src/test/java/"; + public static final String TEST_RESOURCES_FOLDER = "src/test/resources/"; public static final String PROJECT_ROOT = "/fr/xephi/authme/"; private TestHelper() { diff --git a/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java b/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java new file mode 100644 index 00000000..3d19e3d2 --- /dev/null +++ b/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.datasource.converter; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.ConverterSettings; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; + +import static fr.xephi.authme.AuthMeMatchers.equalToHash; +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.times; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link LoginSecurityConverter}. + */ +@RunWith(DelayedInjectionRunner.class) +public class LoginSecurityConverterTest { + + @InjectDelayed + private LoginSecurityConverter converter; + + @Mock + private DataSource dataSource; + @Mock + private Settings settings; + @DataFolder + private File dataFolder = new File("."); // not used but required for injection + + @BeforeInjecting + public void initMocks() { + TestHelper.setupLogger(); + given(settings.getProperty(ConverterSettings.LOGINSECURITY_USE_SQLITE)).willReturn(true); + } + + @Test + public void shouldConvertFromSqlite() throws SQLException { + // given + Connection connection = converter.createSqliteConnection( + TestHelper.TEST_RESOURCES_FOLDER + TestHelper.PROJECT_ROOT + "datasource/converter/LoginSecurity.db"); + CommandSender sender = mock(CommandSender.class); + + // when + converter.performConversion(sender, connection); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); + verify(dataSource, times(3)).saveAuth(captor.capture()); + assertThat(captor.getAllValues().get(0).getNickname(), equalTo("player1")); + assertThat(captor.getAllValues().get(0).getRealName(), equalTo("Player1")); + assertThat(captor.getAllValues().get(0).getLastLogin(), equalTo(1494242093652L)); + assertThat(captor.getAllValues().get(0).getPassword(), equalToHash("$2a$10$E1Ri7XKeIIBv4qVaiPplgepT7QH9xGFh3hbHfcmCjq7hiW.UBTiGK")); + assertThat(captor.getAllValues().get(0).getIp(), equalTo("127.0.0.1")); + + assertThat(captor.getAllValues().get(1).getNickname(), equalTo("player2")); + assertThat(captor.getAllValues().get(1).getLastLogin(), equalTo(1494242174589L)); + assertThat(captor.getAllValues().get(1).getIp(), equalTo("127.4.5.6")); + + assertThat(captor.getAllValues().get(2).getRealName(), equalTo("Player3")); + assertThat(captor.getAllValues().get(2).getPassword(), equalToHash("$2a$10$WFui8KSXMLDOVXKFpCLyPukPi4M82w1cv/rNojsAnwJjba3pp8sba")); + } + +} diff --git a/src/test/resources/fr/xephi/authme/datasource/converter/LoginSecurity.db b/src/test/resources/fr/xephi/authme/datasource/converter/LoginSecurity.db new file mode 100644 index 00000000..4c10efc7 Binary files /dev/null and b/src/test/resources/fr/xephi/authme/datasource/converter/LoginSecurity.db differ