Tyr Backbone Creation

# Changes:

## Patchwork
- Renamed FreedomExecutor to ExecutorProvider and moved the class to the provider package.
- Created an SQL Registry to prevent dependencies on Datura for SQL data. SQL is returned through an Optional, in the event that there is no SQL service registered.
- Created SQLResult, a generic ORM for ResultSets to avoid working directly with SQL data.

## Tyr
- Created Identity, which houses a username and related secret key.
- Created SQLEntry which stores the information from the Identity class into an SQL table called sessionData.
- Created TOTP, a simple static class that allows easy access to TimeBasedOneTimePasswordUtils class.
- Created OAuth2 which houses identities and performs the appropriate credential validations (incomplete)
This commit is contained in:
Paul Reilly 2023-09-09 18:57:15 -05:00
parent 85cc1f7ae0
commit 33731b611f
20 changed files with 889 additions and 172 deletions

View File

@ -32,7 +32,7 @@ import fns.datura.punishment.Locker;
import fns.datura.sql.MySQL; import fns.datura.sql.MySQL;
import fns.patchwork.base.Registration; import fns.patchwork.base.Registration;
import fns.patchwork.command.CommandHandler; import fns.patchwork.command.CommandHandler;
import fns.patchwork.service.SubscriptionProvider; import fns.patchwork.provider.SubscriptionProvider;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;

View File

@ -26,11 +26,11 @@ package fns.datura.sql;
import fns.patchwork.base.Patchwork; import fns.patchwork.base.Patchwork;
import fns.patchwork.base.Shortcuts; import fns.patchwork.base.Shortcuts;
import fns.patchwork.sql.SQL; import fns.patchwork.sql.SQL;
import fns.patchwork.sql.SQLResult;
import fns.patchwork.utils.container.Identity; import fns.patchwork.utils.container.Identity;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
@ -74,7 +74,7 @@ public class MySQL implements SQL
.append(password); .append(password);
} }
public CompletableFuture<ResultSet> getRow(final String table, final String column, final Identity identity) public CompletableFuture<SQLResult> getRow(final String table, final String column, final Identity identity)
{ {
return executeQuery("SELECT * FROM ? WHERE ? = ?", table, column, identity.getId()); return executeQuery("SELECT * FROM ? WHERE ? = ?", table, column, identity.getId());
} }
@ -83,99 +83,104 @@ public class MySQL implements SQL
public CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args) public CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args)
{ {
return getConnection() return getConnection()
.thenApplyAsync(connection -> .thenApplyAsync(connection ->
{ {
try try
{ {
final PreparedStatement statement = connection.prepareStatement(query); final PreparedStatement statement = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) for (int i = 0; i < args.length; i++)
{ {
statement.setObject(i + 1, args[i]); statement.setObject(i + 1, args[i]);
} }
return statement; return statement;
} catch (SQLException ex) }
{ catch (SQLException ex)
throw new CompletionException("Failed to prepare statement: " {
+ query + "\n", ex); throw new CompletionException("Failed to prepare statement: "
} + query + "\n", ex);
}, Shortcuts.provideModule(Patchwork.class) }
.getExecutor() }, Shortcuts.provideModule(Patchwork.class)
.getAsync()); .getExecutor()
.getAsync());
} }
private CompletableFuture<Connection> getConnection() private CompletableFuture<Connection> getConnection()
{ {
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
{ {
try try
{ {
return DriverManager.getConnection(url.toString()); return DriverManager.getConnection(url.toString());
} catch (SQLException ex) }
{ catch (SQLException ex)
throw new CompletionException("Failed to connect to the database: " {
+ url.toString() + "\n", ex); throw new CompletionException("Failed to connect to the database: "
} + url.toString() + "\n", ex);
}, Shortcuts.provideModule(Patchwork.class) }
.getExecutor() }, Shortcuts.provideModule(Patchwork.class)
.getAsync()); .getExecutor()
.getAsync());
} }
@Override @Override
public CompletableFuture<ResultSet> executeQuery(final String query, final Object... args) public CompletableFuture<SQLResult> executeQuery(final String query, final Object... args)
{ {
return prepareStatement(query, args) return prepareStatement(query, args)
.thenApplyAsync(statement -> .thenApplyAsync(statement ->
{ {
try try
{ {
return statement.executeQuery(); return new SQLResult(statement.executeQuery());
} catch (SQLException ex) }
{ catch (SQLException ex)
throw new CompletionException( {
"Failed to retrieve a result set from query: " throw new CompletionException(
"Failed to retrieve a result set from query: "
+ query + "\n", ex); + query + "\n", ex);
} }
}, Shortcuts.provideModule(Patchwork.class) }, Shortcuts.provideModule(Patchwork.class)
.getExecutor() .getExecutor()
.getAsync()); .getAsync());
} }
@Override @Override
public CompletableFuture<Integer> executeUpdate(final String query, final Object... args) public CompletableFuture<Integer> executeUpdate(final String query, final Object... args)
{ {
return prepareStatement(query, args) return prepareStatement(query, args)
.thenApplyAsync(statement -> .thenApplyAsync(statement ->
{ {
try try
{ {
return statement.executeUpdate(); return statement.executeUpdate();
} catch (SQLException ex) }
{ catch (SQLException ex)
throw new CompletionException("Failed to execute update: " {
+ query + "\n", ex); throw new CompletionException("Failed to execute update: "
} + query + "\n", ex);
}, Shortcuts.provideModule(Patchwork.class) }
.getExecutor() }, Shortcuts.provideModule(Patchwork.class)
.getAsync()); .getExecutor()
.getAsync());
} }
@Override @Override
public CompletableFuture<Boolean> execute(final String query, final Object... args) public CompletableFuture<Boolean> execute(final String query, final Object... args)
{ {
return prepareStatement(query, args) return prepareStatement(query, args)
.thenApplyAsync(statement -> .thenApplyAsync(statement ->
{ {
try try
{ {
return statement.execute(); return statement.execute();
} catch (SQLException ex) }
{ catch (SQLException ex)
throw new CompletionException("Failed to execute statement: " {
+ query + "\n", ex); throw new CompletionException("Failed to execute statement: "
} + query + "\n", ex);
}, Shortcuts.provideModule(Patchwork.class) }
.getExecutor() }, Shortcuts.provideModule(Patchwork.class)
.getAsync()); .getExecutor()
.getAsync());
} }
@Override @Override
@ -201,42 +206,27 @@ public class MySQL implements SQL
final Identity identity, final Class<T> type) final Identity identity, final Class<T> type)
{ {
return executeQuery("SELECT ? FROM ? WHERE ? = ?", column, table, key, identity.getId()) return executeQuery("SELECT ? FROM ? WHERE ? = ?", column, table, key, identity.getId())
.thenApplyAsync(resultSet -> .thenApplyAsync(resultSet -> (resultSet.hasNext()) ? resultSet.autoCast(1, column, type) : null,
{ Shortcuts.provideModule(Patchwork.class)
try .getExecutor()
{ .getAsync());
if (resultSet.next())
{
return resultSet.getObject(column, type);
}
} catch (SQLException ex)
{
throw new CompletionException(
"Failed to retrieve column: " + column + " from table: " + table + " " +
"where primary key: " + key + " is equal to: " + identity.getId() + "\n",
ex);
}
return null;
}, Shortcuts.provideModule(Patchwork.class)
.getExecutor()
.getAsync());
} }
public CompletableFuture<Boolean> updateColumn(final String table, final String column, final Object value, public CompletableFuture<Boolean> updateColumn(final String table, final String column, final Object value,
final String key, final Identity identity) final String key, final Identity identity)
{ {
return executeUpdate("UPDATE ? SET ? = ? WHERE ? = ?", table, column, value, key, identity.getId()) return executeUpdate("UPDATE ? SET ? = ? WHERE ? = ?", table, column, value, key, identity.getId())
.thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class)
.getExecutor() .getExecutor()
.getAsync()); .getAsync());
} }
public CompletableFuture<Boolean> deleteRow(final String table, final String key, final Identity identity) public CompletableFuture<Boolean> deleteRow(final String table, final String key, final Identity identity)
{ {
return executeUpdate("DELETE FROM ? WHERE ? = ?", table, key, identity.getId()) return executeUpdate("DELETE FROM ? WHERE ? = ?", table, key, identity.getId())
.thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class)
.getExecutor() .getExecutor()
.getAsync()); .getAsync());
} }
public CompletableFuture<Boolean> insertRow(final String table, final Object... values) public CompletableFuture<Boolean> insertRow(final String table, final Object... values)

View File

@ -34,10 +34,8 @@ import fns.patchwork.sql.SQL;
import fns.patchwork.user.User; import fns.patchwork.user.User;
import fns.patchwork.user.UserData; import fns.patchwork.user.UserData;
import fns.patchwork.utils.logging.FNS4J; import fns.patchwork.utils.logging.FNS4J;
import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -69,14 +67,14 @@ public class SimpleUserData implements UserData
} }
private SimpleUserData( private SimpleUserData(
final UUID uuid, final UUID uuid,
final String username, final String username,
final User user, final User user,
final Group group, final Group group,
final long playtime, final long playtime,
final boolean canInteract, final boolean canInteract,
final long balance, final long balance,
final boolean transactionsFrozen) final boolean transactionsFrozen)
{ {
this.uuid = uuid; this.uuid = uuid;
this.username = username; this.username = username;
@ -93,56 +91,53 @@ public class SimpleUserData implements UserData
{ {
return sql.executeQuery("SELECT * FROM users WHERE UUID = ?", uuid) return sql.executeQuery("SELECT * FROM users WHERE UUID = ?", uuid)
.thenApplyAsync(result -> .thenApplyAsync(result ->
{ {
try
{
if (result.next())
{
final String g = result.getString("group");
final UUID u = UUID.fromString(uuid); if (result.hasNext())
final String username = result.getString("username"); {
final String g = result.getString("group");
final Player player = Bukkit.getPlayer(u); final UUID u = UUID.fromString(uuid);
final String username = result.getString("username");
if (player == null) final Player player = Bukkit.getPlayer(u);
throw new IllegalStateException("Player should be online but they are not!");
final User user = new FreedomUser(player); if (player == null)
final Group group = Registration throw new IllegalStateException(
.getGroupRegistry() "Player should be online but they are not!");
.getGroup(g);
final long playtime = result.getLong("playtime"); final User user = new FreedomUser(player);
final boolean canInteract = result.getBoolean("canInteract"); final Group group = Registration
final long balance = result.getLong("balance"); .getGroupRegistry()
final boolean transactionsFrozen = result.getBoolean("transactionsFrozen"); .getGroup(g);
return new SimpleUserData(u, username, user, group, playtime, final long playtime = result.getLong("playtime");
canInteract, balance, transactionsFrozen); final boolean canInteract = result.getBoolean("canInteract");
} final long balance = result.getLong("balance");
} catch (SQLException ex) final boolean transactionsFrozen = result.getBoolean("transactionsFrozen");
{
final String sb = "An error occurred while trying to retrieve user data for" +
" UUID " +
uuid +
" from the database." +
"\nCaused by: " +
ExceptionUtils.getRootCauseMessage(ex) +
"\nStack trace: " +
ExceptionUtils.getStackTrace(ex);
FNS4J.getLogger("Datura") return new SimpleUserData(u, username, user, group, playtime,
.error(sb); canInteract, balance, transactionsFrozen);
} }
else
{
final String sb = "An error occurred while trying to retrieve user data for" +
" UUID " +
uuid +
" from the database.";
final Player player = Bukkit.getPlayer(UUID.fromString(uuid)); FNS4J.getLogger("Datura")
if (player == null) throw new IllegalStateException("Player should be online but they are not!"); .error(sb);
}
return new SimpleUserData(player); final Player player = Bukkit.getPlayer(UUID.fromString(uuid));
}, Shortcuts.provideModule(Patchwork.class) if (player == null)
.getExecutor() throw new IllegalStateException("Player should be online but they are not!");
.getAsync())
return new SimpleUserData(player);
}, Shortcuts.provideModule(Patchwork.class)
.getExecutor()
.getAsync())
.join(); .join();
} }

View File

@ -27,7 +27,7 @@ import fns.fossil.cmd.CakeCommand;
import fns.fossil.trail.Trailer; import fns.fossil.trail.Trailer;
import fns.patchwork.base.Registration; import fns.patchwork.base.Registration;
import fns.patchwork.command.CommandHandler; import fns.patchwork.command.CommandHandler;
import fns.patchwork.service.SubscriptionProvider; import fns.patchwork.provider.SubscriptionProvider;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
public class Fossil extends JavaPlugin public class Fossil extends JavaPlugin

View File

@ -25,8 +25,8 @@ package fns.patchwork.base;
import fns.patchwork.display.adminchat.AdminChatDisplay; import fns.patchwork.display.adminchat.AdminChatDisplay;
import fns.patchwork.event.EventBus; import fns.patchwork.event.EventBus;
import fns.patchwork.service.FreedomExecutor; import fns.patchwork.provider.ExecutorProvider;
import fns.patchwork.service.SubscriptionProvider; import fns.patchwork.provider.SubscriptionProvider;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -40,9 +40,9 @@ public class Patchwork extends JavaPlugin
*/ */
private EventBus eventBus; private EventBus eventBus;
/** /**
* The {@link FreedomExecutor} for this plugin. * The {@link ExecutorProvider} for this plugin.
*/ */
private FreedomExecutor executor; private ExecutorProvider executor;
/** /**
* The {@link AdminChatDisplay} for this plugin. * The {@link AdminChatDisplay} for this plugin.
*/ */
@ -64,7 +64,7 @@ public class Patchwork extends JavaPlugin
public void onEnable() public void onEnable()
{ {
eventBus = new EventBus(this); eventBus = new EventBus(this);
executor = new FreedomExecutor(this); executor = new ExecutorProvider(this);
acdisplay = new AdminChatDisplay(this); acdisplay = new AdminChatDisplay(this);
@ -80,11 +80,11 @@ public class Patchwork extends JavaPlugin
} }
/** /**
* Gets the {@link FreedomExecutor} for this plugin. * Gets the {@link ExecutorProvider} for this plugin.
* *
* @return the {@link FreedomExecutor} * @return the {@link ExecutorProvider}
*/ */
public FreedomExecutor getExecutor() public ExecutorProvider getExecutor()
{ {
return executor; return executor;
} }

View File

@ -27,6 +27,7 @@ import fns.patchwork.registry.ConfigRegistry;
import fns.patchwork.registry.EventRegistry; import fns.patchwork.registry.EventRegistry;
import fns.patchwork.registry.GroupRegistry; import fns.patchwork.registry.GroupRegistry;
import fns.patchwork.registry.ModuleRegistry; import fns.patchwork.registry.ModuleRegistry;
import fns.patchwork.registry.SQLRegistry;
import fns.patchwork.registry.ServiceTaskRegistry; import fns.patchwork.registry.ServiceTaskRegistry;
import fns.patchwork.registry.UserRegistry; import fns.patchwork.registry.UserRegistry;
@ -62,6 +63,10 @@ public class Registration
* The {@link ConfigRegistry} * The {@link ConfigRegistry}
*/ */
private static final ConfigRegistry configRegistry = new ConfigRegistry(); private static final ConfigRegistry configRegistry = new ConfigRegistry();
/**
* The SQL Registry
*/
private static final SQLRegistry sqlRegistry = new SQLRegistry();
private Registration() private Registration()
{ {
@ -115,4 +120,12 @@ public class Registration
{ {
return configRegistry; return configRegistry;
} }
/**
* @return The {@link SQLRegistry}
*/
public static SQLRegistry getSQLRegistry()
{
return sqlRegistry;
}
} }

View File

@ -23,7 +23,10 @@
package fns.patchwork.base; package fns.patchwork.base;
import fns.patchwork.provider.ExecutorProvider;
import fns.patchwork.sql.SQL;
import fns.patchwork.user.User; import fns.patchwork.user.User;
import java.util.Optional;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -37,13 +40,23 @@ public final class Shortcuts
public static <T extends JavaPlugin> T provideModule(final Class<T> pluginClass) public static <T extends JavaPlugin> T provideModule(final Class<T> pluginClass)
{ {
return Registration.getModuleRegistry() return Registration.getModuleRegistry()
.getProvider(pluginClass) .getProvider(pluginClass)
.getModule(); .getModule();
} }
public static User getUser(final Player player) public static User getUser(final Player player)
{ {
return Registration.getUserRegistry() return Registration.getUserRegistry()
.getUser(player); .getUser(player);
}
public static ExecutorProvider getExecutors()
{
return provideModule(Patchwork.class).getExecutor();
}
public static Optional<SQL> getSQL()
{
return Registration.getSQLRegistry().getSQL();
} }
} }

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.service; package fns.patchwork.provider;
import fns.patchwork.base.Patchwork; import fns.patchwork.base.Patchwork;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -34,7 +34,7 @@ import org.bukkit.plugin.java.JavaPlugin;
* This class is here for both convenience purposes, and also for the sake of providing easy access to executors for * This class is here for both convenience purposes, and also for the sake of providing easy access to executors for
* {@link CompletableFuture} invocations. * {@link CompletableFuture} invocations.
*/ */
public class FreedomExecutor public class ExecutorProvider
{ {
/** /**
* An executor which runs tasks synchronously. * An executor which runs tasks synchronously.
@ -46,9 +46,9 @@ public class FreedomExecutor
private final Executor asyncExecutor; private final Executor asyncExecutor;
/** /**
* Creates a new {@link FreedomExecutor} instance. * Creates a new {@link ExecutorProvider} instance.
*/ */
public FreedomExecutor(final Patchwork patchwork) public ExecutorProvider(final Patchwork patchwork)
{ {
syncExecutor = r -> Bukkit.getScheduler() syncExecutor = r -> Bukkit.getScheduler()
.runTask(patchwork, r); .runTask(patchwork, r);

View File

@ -21,8 +21,12 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.service; package fns.patchwork.provider;
import fns.patchwork.service.Service;
import fns.patchwork.service.ServiceSubscription;
import fns.patchwork.service.Task;
import fns.patchwork.service.TaskSubscription;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

View File

@ -0,0 +1,44 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.patchwork.registry;
import fns.patchwork.sql.SQL;
import java.util.Optional;
public class SQLRegistry
{
private SQL sql;
public SQLRegistry() {
this.sql = null;
}
public Optional<SQL> getSQL() {
return (sql == null) ? Optional.empty() : Optional.of(sql);
}
public void setSQL(final SQL sql) {
this.sql = sql;
}
}

View File

@ -25,7 +25,7 @@ package fns.patchwork.registry;
import fns.patchwork.service.Service; import fns.patchwork.service.Service;
import fns.patchwork.service.ServiceSubscription; import fns.patchwork.service.ServiceSubscription;
import fns.patchwork.service.SubscriptionProvider; import fns.patchwork.provider.SubscriptionProvider;
import fns.patchwork.service.Task; import fns.patchwork.service.Task;
import fns.patchwork.service.TaskSubscription; import fns.patchwork.service.TaskSubscription;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -23,6 +23,8 @@
package fns.patchwork.service; package fns.patchwork.service;
import fns.patchwork.provider.SubscriptionProvider;
/** /**
* Represents a ticking service. Services may be asynchronous or synchronous, however there are some restrictions: * Represents a ticking service. Services may be asynchronous or synchronous, however there are some restrictions:
* <ul> * <ul>

View File

@ -31,7 +31,7 @@ public interface SQL
{ {
CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args); CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args);
CompletableFuture<ResultSet> executeQuery(final String query, final Object... args); CompletableFuture<SQLResult> executeQuery(final String query, final Object... args);
CompletableFuture<Integer> executeUpdate(final String query, final Object... args); CompletableFuture<Integer> executeUpdate(final String query, final Object... args);

View File

@ -0,0 +1,348 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.patchwork.sql;
import fns.patchwork.utils.container.Pair;
import fns.patchwork.utils.logging.FNS4J;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
public class SQLResult
{
private final Map<Integer, Map<String, Object>> resultMap = new HashMap<>();
/**
* This constructor will create a new SQLResult object from the specified ResultSet.
* This will iterate through all rows and columns of the ResultSet and store them in a Map.
* The Map will contain keys of integers representing the row number, and values of Maps
* containing the column names and their values.
*
* @param resultSet The ResultSet to create the SQLResult object from.
*/
public SQLResult(final ResultSet resultSet)
{
try
{
final ResultSetMetaData metaData = resultSet.getMetaData();
final int columnCount = metaData.getColumnCount();
int rowIndex = 0;
while (resultSet.next())
{
rowIndex++;
final Map<String, Object> rowMap = new HashMap<>();
for (int columnIndex = 1; columnIndex <= columnCount; columnIndex++)
{
String columnName = metaData.getColumnName(columnIndex);
Object columnValue = resultSet.getObject(columnIndex);
rowMap.put(columnName, columnValue);
}
resultMap.put(rowIndex, rowMap);
}
}
catch (SQLException ex)
{
FNS4J.getLogger("Tyr").error(ex.getMessage());
}
}
/**
* This method will return a map of all rows and their columns and values.
*
* @return A Map containing all rows and their columns and values.
*/
public Map<Integer, Map<String, Object>> getResultMap()
{
return resultMap;
}
/**
* This method will return a map of all columns and their values from the specified row.
*
* @param rowIndex The row index to get the column names from.
* @return A Map containing all column names and their values from the specified row.
*/
public Map<String, Object> getRow(final int rowIndex)
{
return resultMap.get(rowIndex);
}
/**
* This method will return the value from the specified row and column.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @return The value from the specified row and column.
*/
public Object getValue(final int rowIndex, final String columnName)
{
return resultMap.get(rowIndex).get(columnName);
}
/**
* This method will return the first value from the first row of the result set.
*
* @return A Pair containing the column name and the stored value.
*/
public Pair<String, Object> getFirst()
{
return new Pair<>(resultMap.get(1).entrySet().iterator().next().getKey(),
resultMap.get(1).entrySet().iterator().next().getValue());
}
/**
* This method will return the first value from the specified row of the result set.
*
* @param rowIndex The row index to get the column name from.
* @return A Pair containing the column name and the stored value.
*/
public Pair<String, Object> getFirst(final int rowIndex)
{
return new Pair<>(resultMap.get(rowIndex).entrySet().iterator().next().getKey(),
resultMap.get(rowIndex).entrySet().iterator().next().getValue());
}
/**
* This method will return the last value from the first row of the result set.
*
* @return A Pair containing the column name and the stored value.
*/
public Pair<String, Object> getLast()
{
return new Pair<>(resultMap.get(1).entrySet().iterator().next().getKey(),
resultMap.get(1).entrySet().iterator().next().getValue());
}
/**
* This method will return the last value from the specified row of the result set.
*
* @param rowIndex The row index to get the column name from.
* @return A Pair containing the column name and the stored value.
*/
public Pair<String, Object> getLast(final int rowIndex)
{
return new Pair<>(resultMap.get(rowIndex).entrySet().iterator().next().getKey(),
resultMap.get(rowIndex).entrySet().iterator().next().getValue());
}
/**
* This method will attempt to retrieve the value from the specified row and column,
* and cast it to the specified class. This will throw a {@link ClassCastException} if the
* returned value is not an instance of the provided class.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @param clazz The class to cast the value to.
* @param <T> The expected type.
* @return The value from the specified row and column, cast to the specified class.
*/
public <T> T autoCast(final int rowIndex, final String columnName, final Class<T> clazz)
{
final Object value = resultMap.get(rowIndex).get(columnName);
if (!clazz.isInstance(value))
throw new ClassCastException("Cannot cast " + value.getClass().getName() + " to " + clazz.getName());
return clazz.cast(resultMap.get(rowIndex).get(columnName));
}
/**
* @param rowIndex The row index to get the column names from.
* @return A Set containing all column names from the specified row of the result set.
*/
public Set<String> getColumnNames(final int rowIndex)
{
return resultMap.get(rowIndex).keySet();
}
/**
* @return A Set containing all column names from the first row of the result set.
*/
public Set<String> getColumnNames()
{
return resultMap.get(1).keySet();
}
/**
* This method will apply the specified consumer to all rows of the result set.
*
* @param columnConsumer The consumer to apply to all rows of the result set.
*/
public void accept(final Consumer<Map<String, Object>> columnConsumer)
{
this.resultMap.forEach((integer, map) -> columnConsumer.accept(map));
}
/**
* Checks to see if the result set contains the specified row number.
* Best used in a for loop, using {@link #rowCount()} as the upper bound.
*
* @param rowIndex The row index to check.
* @return True if the result set contains the specified row number, false otherwise.
*/
public boolean hasNext(final int rowIndex)
{
return this.resultMap.containsKey(rowIndex + 1);
}
/**
* Checks to see if the result set has the first row.
* If row 1 doesn't exist, it's safe to say the result set is empty.
*
* @return True if the result set has row 1, false otherwise.
*/
public boolean hasNext()
{
return this.resultMap.containsKey(1);
}
/**
* @return The number of rows in the result set.
*/
public int rowCount()
{
return this.resultMap.size();
}
/**
* @param rowIndex The row index from which to count columns.
* @return The number of columns in the specified row.
*/
public int columnCount(final int rowIndex)
{
return this.resultMap.get(rowIndex).size();
}
/**
* Retrieves a String value from the specified row and column.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @return The String value from the specified row and column.
* @see #autoCast(int, String, Class)
*/
public String getString(final int rowIndex, final String columnName)
{
return autoCast(rowIndex, columnName, String.class);
}
/**
* This method will attempt to retrieve a String value from the specified column within the first row of the
* result set.
*
* @param columnName The column name to get the value from.
* @return The String value from the specified column within the first row of the result set.
* @see #getString(int, String)
*/
public String getString(final String columnName)
{
return getString(1, columnName);
}
/**
* Retrieves an Integer value from the specified row and column.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @return The Integer value from the specified row and column.
* @see #autoCast(int, String, Class)
*/
public int getInteger(final int rowIndex, final String columnName)
{
return autoCast(rowIndex, columnName, Integer.class);
}
/**
* This method will attempt to retrieve an Integer value from the specified column within the first row of the
* result set.
*
* @param columnName The column name to get the value from.
* @return The Integer value from the specified column within the first row of the result set.
* @see #getInteger(int, String)
*/
public int getInteger(final String columnName)
{
return getInteger(1, columnName);
}
/**
* Retrieves a Long value from the specified row and column.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @return The Long value from the specified row and column.
* @see #autoCast(int, String, Class)
*/
public long getLong(final int rowIndex, final String columnName)
{
return autoCast(rowIndex, columnName, Long.class);
}
/**
* This method will attempt to retrieve a Long value from the specified column within the first row of the
* result set.
*
* @param columnName The column name to get the value from.
* @return The Long value from the specified column within the first row of the result set.
* @see #getLong(int, String)
*/
public long getLong(final String columnName)
{
return getLong(1, columnName);
}
/**
* Retrieves a Double value from the specified row and column.
*
* @param rowIndex The row index to get the column name from.
* @param columnName The column name to get the value from.
* @return The Double value from the specified row and column.
* @see #autoCast(int, String, Class)
*/
public boolean getBoolean(final int rowIndex, final String columnName)
{
return autoCast(rowIndex, columnName, Boolean.class);
}
/**
* This method will attempt to retrieve a Boolean value from the specified column within the first row of the
* result set.
*
* @param columnName The column name to get the value from.
* @return The Boolean value from the specified column within the first row of the result set.
* @see #getBoolean(int, String)
*/
public boolean getBoolean(final String columnName)
{
return getBoolean(1, columnName);
}
}

View File

@ -16,11 +16,10 @@ bukkit {
} }
dependencies { dependencies {
compileOnly project(":Patchwork") compileOnly project(path: ":Patchwork")
compileOnly project(":Datura") compileOnly project(path: ":Datura")
library 'com.hierynomus:sshj:0.28.0' library 'com.j256.two-factor-auth:two-factor-auth:1.3'
library 'org.bouncycastle:bcprov-jdk18on:1.76'
testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter'

View File

@ -0,0 +1,48 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.tyr;
import fns.datura.Datura;
import fns.patchwork.base.Shortcuts;
import fns.patchwork.sql.SQL;
import fns.patchwork.utils.logging.FNS4J;
public class Tyr
{
public void onEnable()
{
final SQL sql = Shortcuts.provideModule(Datura.class).getSQL();
sql.createTable("sessionData",
"user VARCHAR(16) NOT NULL PRIMARY KEY, secretKey VARCHAR(64) NOT NULL;")
.whenCompleteAsync((result, throwable) ->
{
if (throwable != null)
FNS4J.getLogger("Tyr")
.error(throwable.getMessage());
}, Shortcuts.getExecutors()
.getAsync());
}
}

View File

@ -0,0 +1,93 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.tyr.data;
import fns.patchwork.base.Shortcuts;
import fns.patchwork.utils.logging.FNS4J;
import fns.tyr.oauth.Identity;
import java.sql.SQLException;
public class SQLEntry
{
private final Identity identity;
public SQLEntry(final Identity identity)
{
this.identity = identity;
}
public static SQLEntry load(final String username)
{
return Shortcuts.getSQL()
.map(c ->
c.executeQuery("SELECT * FROM sessionData WHERE user = ?;", username)
.thenApplyAsync(result ->
{
SQLEntry entry = null;
try
{
if (result.next())
{
final String user = result.getString("user");
final String secretKey = result.getString("secretKey");
final Identity i = new Identity(user, secretKey);
entry = new SQLEntry(i);
FNS4J.getLogger("Tyr")
.info("Loaded entry for " + username);
}
else
{
entry = new SQLEntry(Identity.of(username));
FNS4J.getLogger("Tyr")
.info("Created a new entry for " + username);
}
}
catch (SQLException ex)
{
FNS4J.getLogger("Tyr").error(ex.getMessage());
}
return entry;
}, Shortcuts.getExecutors()
.getAsync())
.join())
.orElseThrow(() -> new IllegalStateException("SQL is not initialized!"));
}
public void save()
{
Shortcuts.getSQL()
.orElseThrow(() -> new IllegalStateException("SQL is not available!"))
.executeUpdate("INSERT INTO sessionData (user, secretKey) VALUES (?, ?);",
this.identity.username(),
this.identity.secretKey())
.whenCompleteAsync((result, throwable) ->
{
if (throwable != null)
FNS4J.getLogger("Tyr").error(throwable.getMessage());
}, Shortcuts.getExecutors()
.getAsync());
}
}

View File

@ -0,0 +1,31 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.tyr.oauth;
public record Identity(String username, String secretKey)
{
public static Identity of(final String username) {
return new Identity(username, TOTP.createSecretKey());
}
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.tyr.oauth;
import fns.patchwork.base.Shortcuts;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
public class OAuth2
{
private final Set<Identity> identitySet;
public OAuth2()
{
this.identitySet = new HashSet<>();
}
public void addIdentity(Identity identity)
{
this.identitySet.add(identity);
}
public void removeIdentity(Identity identity)
{
this.identitySet.remove(identity);
}
public Optional<Identity> getIdentity(final String username)
{
return this.identitySet.stream()
.filter(identity -> identity.username().equals(username))
.findFirst();
}
public void loadAll()
{
Shortcuts.getSQL()
.ifPresent(sql -> sql.executeQuery("SELECT * FROM sessionData;")
.thenAcceptAsync(result ->
{
for (int i = 1; i < result.rowCount(); i++)
{
final String username = result.getString(i,
"user");
final String secretKey = result.getString(i,
"secretKey");
this.addIdentity(
new Identity(username, secretKey));
}
}));
}
}

View File

@ -0,0 +1,63 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.tyr.oauth;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
import fns.patchwork.utils.logging.FNS4J;
import java.security.GeneralSecurityException;
/**
* User-friendly version of TimeBasedOneTimePasswordUtil.
*/
public final class TOTP
{
private TOTP()
{
throw new AssertionError("This class cannot be instantiated.");
}
public static String createSecretKey()
{
return TimeBasedOneTimePasswordUtil.generateBase32Secret(32);
}
public static String createQRCode(final String username, final String secretKey)
{
return TimeBasedOneTimePasswordUtil.qrImageUrl(username, secretKey);
}
public static boolean verify(final String secretKey, final int userCode)
{
try
{
int vCode = TimeBasedOneTimePasswordUtil.generateCurrentNumber(secretKey);
return vCode == userCode;
}
catch (GeneralSecurityException ex)
{
FNS4J.getLogger("Tyr").error("Failed to verify TOTP code: " + ex.getMessage());
return false;
}
}
}