mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-12-25 16:18:13 +01:00
Compare commits
2 Commits
floodgate-
...
add-postgr
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d9f3cb4a | |||
| 56e43fb146 |
12
README.md
12
README.md
@@ -60,13 +60,13 @@ Possible values: `Premium`, `Cracked`, `Unknown`
|
||||
|
||||
## Requirements
|
||||
|
||||
* Java 17+
|
||||
* Java 17+ (Recommended)
|
||||
* Server software in offlinemode:
|
||||
* Spigot (or a fork e.g. Paper) 1.8.8+
|
||||
* Protocol plugin:
|
||||
* [ProtocolLib 5.0+](https://www.spigotmc.org/resources/protocollib.1997/) or
|
||||
* [ProtocolLib 5.1+](https://www.spigotmc.org/resources/protocollib.1997/) or
|
||||
* [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/)
|
||||
* Latest BungeeCord (or a fork e.g. Waterfall)
|
||||
* Latest BungeeCord (or a fork e.g. Waterfall) or Velocity
|
||||
* An auth plugin.
|
||||
|
||||
### Supported auth plugins
|
||||
@@ -117,10 +117,10 @@ Install the plugin on both platforms, that is proxy (BungeeCord or Velocity) and
|
||||
4. Activate ip forwarding in your proxy config
|
||||
5. Check your database settings in the config of FastLogin on your proxy
|
||||
* The proxies only ship with a limited set of drivers where Spigot supports more. Therefore, these are supported:
|
||||
* BungeeCord: `com.mysql.jdbc.Driver` for MySQL/MariaDB
|
||||
* Velocity: `fastlogin.mariadb.jdbc.Driver` for MySQL/MariaDB
|
||||
* BungeeCord: `com.mysql.jdbc.Driver` for MySQL/MariaDB/PostgreSQL
|
||||
* Velocity: `fastlogin.mariadb.jdbc.Driver` for MySQL/MariaDB/PostgreSQL
|
||||
* Note the embedded file storage SQLite is not available
|
||||
* MySQL/MariaDB requires an external database server running. Check your server provider if there is one available
|
||||
* MySQL/MariaDB/PostgreSQL requires an external database server running. Check your server provider if there is one available
|
||||
or install one.
|
||||
6. Set proxy and Spigot in offline mode by setting the value `onlinemode` in your `config.yml` to false
|
||||
7. You should *always* configure the firewall for your Spigot server so that it's only accessible through your proxy
|
||||
|
||||
@@ -39,7 +39,7 @@ public class BukkitScheduler extends AsyncScheduler {
|
||||
public BukkitScheduler(Plugin plugin, Logger logger) {
|
||||
super(logger, command -> Bukkit.getScheduler().runTaskAsynchronously(plugin, command));
|
||||
|
||||
syncExecutor = r -> Bukkit.getScheduler().runTask(plugin, r);
|
||||
syncExecutor = task -> Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
public Executor getSyncExecutor() {
|
||||
|
||||
@@ -177,7 +177,7 @@ public class ProtocolLibListener extends PacketAdapter {
|
||||
ClientPublicKey clientPublicKey, byte[] expectedToken) {
|
||||
try {
|
||||
if (new MinecraftVersion(1, 19, 0).atOrAbove()
|
||||
&& !(new MinecraftVersion(1, 19, 3).atOrAbove())) {
|
||||
&& !new MinecraftVersion(1, 19, 3).atOrAbove()) {
|
||||
Either<byte[], ?> either = packet.getSpecificModifier(Either.class).read(0);
|
||||
if (clientPublicKey == null) {
|
||||
Optional<byte[]> left = either.left();
|
||||
|
||||
@@ -71,43 +71,25 @@ public class ConnectListener implements Listener {
|
||||
private static final String UUID_FIELD_NAME = "uniqueId";
|
||||
protected static final MethodHandle UNIQUE_ID_SETTER;
|
||||
|
||||
private static final String REWRITE_ID_NAME = "rewriteId";
|
||||
protected static final MethodHandle REWRITE_ID_SETTER;
|
||||
|
||||
static {
|
||||
MethodHandle uniqueIdHandle = null;
|
||||
MethodHandle rewriterHandle = null;
|
||||
MethodHandle setHandle = null;
|
||||
try {
|
||||
Lookup lookup = MethodHandles.lookup();
|
||||
|
||||
// test for implementation class availability
|
||||
Class.forName("net.md_5.bungee.connection.InitialHandler");
|
||||
uniqueIdHandle = getHandlerSetter(lookup, UUID_FIELD_NAME);
|
||||
try {
|
||||
rewriterHandle = getHandlerSetter(lookup, REWRITE_ID_NAME);
|
||||
} catch (NoSuchFieldException noSuchFieldEx) {
|
||||
Logger logger = LoggerFactory.getLogger(ConnectListener.class);
|
||||
logger.error(
|
||||
"Rewrite field not found. Setting only legacy BungeeCord field"
|
||||
);
|
||||
}
|
||||
|
||||
Field uuidField = InitialHandler.class.getDeclaredField(UUID_FIELD_NAME);
|
||||
uuidField.setAccessible(true);
|
||||
setHandle = lookup.unreflectSetter(uuidField);
|
||||
} catch (ReflectiveOperationException reflectiveOperationException) {
|
||||
Logger logger = LoggerFactory.getLogger(ConnectListener.class);
|
||||
logger.error(
|
||||
"Cannot find Bungee UUID field implementation; Disabling premium UUID and skin won't work.",
|
||||
"Cannot find Bungee initial handler; Disabling premium UUID and skin won't work.",
|
||||
reflectiveOperationException
|
||||
);
|
||||
}
|
||||
|
||||
UNIQUE_ID_SETTER = uniqueIdHandle;
|
||||
REWRITE_ID_SETTER = rewriterHandle;
|
||||
}
|
||||
|
||||
private static MethodHandle getHandlerSetter(Lookup lookup, String fieldName)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
Field uuidField = InitialHandler.class.getDeclaredField(fieldName);
|
||||
uuidField.setAccessible(true);
|
||||
return lookup.unreflectSetter(uuidField);
|
||||
UNIQUE_ID_SETTER = setHandle;
|
||||
}
|
||||
|
||||
private final FastLoginBungee plugin;
|
||||
@@ -197,12 +179,6 @@ public class ConnectListener implements Listener {
|
||||
// So we have to do it with reflection
|
||||
UNIQUE_ID_SETTER.invokeExact(connection, offlineUUID);
|
||||
|
||||
// if available set rewrite id to forward the UUID for newer BungeeCord versions since
|
||||
// https://github.com/SpigotMC/BungeeCord/commit/1be25b6c74ec2be4b15adf8ca53a0497f01e2afe
|
||||
if (REWRITE_ID_SETTER != null) {
|
||||
REWRITE_ID_SETTER.invokeExact(connection, offlineUUID);
|
||||
}
|
||||
|
||||
String format = "Overridden UUID from {} to {} (based of {}) on {}";
|
||||
plugin.getLog().info(format, oldPremiumId, offlineUUID, username, connection);
|
||||
} catch (Exception ex) {
|
||||
|
||||
@@ -107,7 +107,7 @@ public class TickingRateLimiter implements RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
private static class TimeRecord implements Comparable<Long> {
|
||||
private static class TimeRecord implements Comparable<TimeRecord> {
|
||||
|
||||
private final long firstMinuteRecord;
|
||||
private final long expireTime;
|
||||
@@ -131,9 +131,9 @@ public class TickingRateLimiter implements RateLimiter {
|
||||
return firstMinuteRecord + expireTime <= now;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Long other) {
|
||||
public int compareTo(long other) {
|
||||
if (other < firstMinuteRecord) {
|
||||
// other is earlier
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -143,5 +143,10 @@ public class TickingRateLimiter implements RateLimiter {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TimeRecord other) {
|
||||
return compareTo(other.firstMinuteRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.github.games647.fastlogin.core.antibot.TickingRateLimiter;
|
||||
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
|
||||
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
|
||||
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
|
||||
import com.github.games647.fastlogin.core.storage.PostgreSQLStorage;
|
||||
import com.github.games647.fastlogin.core.storage.MySQLStorage;
|
||||
import com.github.games647.fastlogin.core.storage.SQLStorage;
|
||||
import com.github.games647.fastlogin.core.storage.SQLiteStorage;
|
||||
@@ -230,6 +231,24 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
|
||||
|
||||
if (type.contains("sqlite")) {
|
||||
storage = new SQLiteStorage(plugin, database, databaseConfig);
|
||||
} else if (type.contains("postgresql")) {
|
||||
String host = config.get("host", "");
|
||||
int port = config.get("port", 3306);
|
||||
boolean useSSL = config.get("useSSL", false);
|
||||
|
||||
if (useSSL) {
|
||||
boolean publicKeyRetrieval = config.getBoolean("allowPublicKeyRetrieval", false);
|
||||
String rsaPublicKeyFile = config.getString("ServerRSAPublicKeyFile");
|
||||
String sslMode = config.getString("sslMode", "Required");
|
||||
|
||||
databaseConfig.addDataSourceProperty("allowPublicKeyRetrieval", publicKeyRetrieval);
|
||||
databaseConfig.addDataSourceProperty("serverRSAPublicKeyFile", rsaPublicKeyFile);
|
||||
databaseConfig.addDataSourceProperty("sslMode", sslMode);
|
||||
}
|
||||
|
||||
databaseConfig.setUsername(config.get("username", ""));
|
||||
databaseConfig.setPassword(config.getString("password"));
|
||||
storage = new PostgreSQLStorage(plugin, type, host, port, database, databaseConfig, useSSL);
|
||||
} else {
|
||||
String host = config.get("host", "");
|
||||
int port = config.get("port", 3306);
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.geysermc.floodgate.api.player.FloodgatePlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -61,9 +62,9 @@ public abstract class FloodgateManagement<P extends C, C, L extends LoginSession
|
||||
this.username = getName(player);
|
||||
|
||||
//load values from config.yml
|
||||
autoLoginFloodgate = core.getConfig().get("autoLoginFloodgate").toString().toLowerCase();
|
||||
autoRegisterFloodgate = core.getConfig().get("autoRegisterFloodgate").toString().toLowerCase();
|
||||
allowNameConflict = core.getConfig().get("allowFloodgateNameConflict").toString().toLowerCase();
|
||||
autoLoginFloodgate = core.getConfig().getString("autoLoginFloodgate").toLowerCase(Locale.ROOT);
|
||||
autoRegisterFloodgate = core.getConfig().getString("autoRegisterFloodgate").toLowerCase(Locale.ROOT);
|
||||
allowNameConflict = core.getConfig().getString("allowFloodgateNameConflict").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,6 +61,7 @@ public abstract class LoginSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user needs registration once login is successful
|
||||
* @return This value is always false if we authenticate the player with a cracked authentication
|
||||
*/
|
||||
public synchronized boolean needsRegistration() {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015-2023 games647 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 com.github.games647.fastlogin.core.storage;
|
||||
|
||||
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
|
||||
public class PostgreSQLStorage extends SQLStorage {
|
||||
|
||||
private static final String JDBC_PROTOCOL = "jdbc:";
|
||||
|
||||
public PostgreSQLStorage(PlatformPlugin<?> plugin, String driver, String host, int port, String database,
|
||||
HikariConfig config, boolean useSSL) {
|
||||
super(plugin.getLog(), plugin.getName(), plugin.getThreadFactory(),
|
||||
setParams(config, driver, host, port, database, useSSL));
|
||||
}
|
||||
|
||||
private static HikariConfig setParams(HikariConfig config,
|
||||
String driver, String host, int port, String database,
|
||||
boolean useSSL) {
|
||||
// Require SSL on the server if requested in config - this will also verify certificate
|
||||
// Those values are deprecated in favor of sslMode
|
||||
config.addDataSourceProperty("useSSL", useSSL);
|
||||
config.addDataSourceProperty("requireSSL", useSSL);
|
||||
|
||||
// adding paranoid, hides hostname, username, version and so
|
||||
// could be useful for hiding server details
|
||||
config.addDataSourceProperty("paranoid", true);
|
||||
|
||||
config.setJdbcUrl(JDBC_PROTOCOL + buildJDBCUrl(driver, host, port, database));
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static String buildJDBCUrl(String driver, String host, int port, String database) {
|
||||
return "postgresql://" + host + ':' + port + '/' + database;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCreateTableStmt() {
|
||||
// PostgreSQL has a different syntax for id column
|
||||
return CREATE_TABLE_STMT
|
||||
.replace("`", "\"")
|
||||
.replace("INTEGER PRIMARY KEY AUTO_INCREMENT", "SERIAL PRIMARY KEY");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAddFloodgateColumnStmt() {
|
||||
// PostgreSQL has a different syntax
|
||||
return ADD_FLOODGATE_COLUMN_STMT
|
||||
.replace("`", "\"")
|
||||
.replace("INTEGER(3)", "INTEGER");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLoadByNameStmt() {
|
||||
return LOAD_BY_NAME_STMT
|
||||
.replace("`", "\"");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLoadByUuidStmt() {
|
||||
return LOAD_BY_UUID_STMT
|
||||
.replace("`", "\"");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInsertProfileStmt() {
|
||||
return INSERT_PROFILE_STMT
|
||||
.replace("`", "\"");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getUpdateProfileStmt() {
|
||||
return UPDATE_PROFILE_STMT
|
||||
.replace("`", "\"");
|
||||
}
|
||||
}
|
||||
@@ -61,14 +61,14 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
protected static final String ADD_FLOODGATE_COLUMN_STMT = "ALTER TABLE `" + PREMIUM_TABLE
|
||||
+ "` ADD COLUMN `Floodgate` INTEGER(3)";
|
||||
|
||||
protected static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE
|
||||
protected static final String LOAD_BY_NAME_STMT = "SELECT * FROM `" + PREMIUM_TABLE
|
||||
+ "` WHERE `Name`=? LIMIT 1";
|
||||
protected static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE
|
||||
protected static final String LOAD_BY_UUID_STMT = "SELECT * FROM `" + PREMIUM_TABLE
|
||||
+ "` WHERE `UUID`=? LIMIT 1";
|
||||
protected static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE
|
||||
protected static final String INSERT_PROFILE_STMT = "INSERT INTO `" + PREMIUM_TABLE
|
||||
+ "` (`UUID`, `Name`, `Premium`, `Floodgate`, `LastIp`) " + "VALUES (?, ?, ?, ?, ?) ";
|
||||
// limit not necessary here, because it's unique
|
||||
protected static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE
|
||||
protected static final String UPDATE_PROFILE_STMT = "UPDATE `" + PREMIUM_TABLE
|
||||
+ "` SET `UUID`=?, `Name`=?, `Premium`=?, `Floodgate`=?, `LastIp`=?, "
|
||||
+ "`LastLogin`=CURRENT_TIMESTAMP WHERE `UserID`=?";
|
||||
|
||||
@@ -97,7 +97,7 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
// add Floodgate column
|
||||
DatabaseMetaData md = con.getMetaData();
|
||||
if (isColumnMissing(md, "Floodgate")) {
|
||||
stmt.executeUpdate(ADD_FLOODGATE_COLUMN_STMT);
|
||||
stmt.executeUpdate(getAddFloodgateColumnStmt());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
@Override
|
||||
public StoredProfile loadProfile(String name) {
|
||||
try (Connection con = dataSource.getConnection();
|
||||
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_NAME)
|
||||
PreparedStatement loadStmt = con.prepareStatement(getLoadByNameStmt())
|
||||
) {
|
||||
loadStmt.setString(1, name);
|
||||
|
||||
@@ -130,7 +130,7 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
@Override
|
||||
public StoredProfile loadProfile(UUID uuid) {
|
||||
try (Connection con = dataSource.getConnection();
|
||||
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)) {
|
||||
PreparedStatement loadStmt = con.prepareStatement(getLoadByUuidStmt())) {
|
||||
loadStmt.setString(1, UUIDAdapter.toMojangId(uuid));
|
||||
|
||||
try (ResultSet resultSet = loadStmt.executeQuery()) {
|
||||
@@ -147,7 +147,10 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
if (resultSet.next()) {
|
||||
long userId = resultSet.getInt("UserID");
|
||||
|
||||
UUID uuid = Optional.ofNullable(resultSet.getString("UUID")).map(UUIDAdapter::parseId).orElse(null);
|
||||
UUID uuid = Optional.ofNullable(resultSet.getString("UUID"))
|
||||
.map(String::trim)
|
||||
.map(UUIDAdapter::parseId)
|
||||
.orElse(null);
|
||||
|
||||
String name = resultSet.getString("Name");
|
||||
boolean premium = resultSet.getBoolean("Premium");
|
||||
@@ -177,7 +180,7 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
playerProfile.getSaveLock().lock();
|
||||
try {
|
||||
if (playerProfile.isSaved()) {
|
||||
try (PreparedStatement saveStmt = con.prepareStatement(UPDATE_PROFILE)) {
|
||||
try (PreparedStatement saveStmt = con.prepareStatement(getUpdateProfileStmt())) {
|
||||
saveStmt.setString(1, uuid);
|
||||
saveStmt.setString(2, playerProfile.getName());
|
||||
saveStmt.setBoolean(3, playerProfile.isPremium());
|
||||
@@ -188,7 +191,8 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
saveStmt.execute();
|
||||
}
|
||||
} else {
|
||||
try (PreparedStatement saveStmt = con.prepareStatement(INSERT_PROFILE, RETURN_GENERATED_KEYS)) {
|
||||
try (PreparedStatement saveStmt = con.prepareStatement(getInsertProfileStmt(),
|
||||
RETURN_GENERATED_KEYS)) {
|
||||
saveStmt.setString(1, uuid);
|
||||
|
||||
saveStmt.setString(2, playerProfile.getName());
|
||||
@@ -214,13 +218,37 @@ public abstract class SQLStorage implements AuthStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite has a slightly different syntax, so this will be overridden by SQLiteStorage
|
||||
* SQLite and PostgreSQL have a slightly different syntax, so this will be overridden by SQLiteStorage and so on...
|
||||
* @return An SQL Statement to create the `premium` table
|
||||
*/
|
||||
protected String getCreateTableStmt() {
|
||||
return CREATE_TABLE_STMT;
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL has a slightly different syntax, so this will be overridden by PostgreSQLStorage
|
||||
* @return An SQL Statement to create the `premium` table
|
||||
*/
|
||||
protected String getAddFloodgateColumnStmt() {
|
||||
return ADD_FLOODGATE_COLUMN_STMT;
|
||||
}
|
||||
|
||||
protected String getLoadByNameStmt() {
|
||||
return LOAD_BY_NAME_STMT;
|
||||
}
|
||||
|
||||
protected String getLoadByUuidStmt() {
|
||||
return LOAD_BY_UUID_STMT;
|
||||
}
|
||||
|
||||
protected String getInsertProfileStmt() {
|
||||
return INSERT_PROFILE_STMT;
|
||||
}
|
||||
|
||||
protected String getUpdateProfileStmt() {
|
||||
return UPDATE_PROFILE_STMT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
dataSource.close();
|
||||
|
||||
@@ -289,6 +289,15 @@ database: '{pluginDir}/FastLogin.db'
|
||||
#username: 'myUser'
|
||||
#password: 'myPassword'
|
||||
|
||||
# PostgreSQL
|
||||
# If you want to enable it, uncomment only the lines below; this not this line.
|
||||
#driver: 'postgresql'
|
||||
#host: '127.0.0.1'
|
||||
#port: 5432
|
||||
#database: 'fastlogin'
|
||||
#username: 'myUser'
|
||||
#password: 'myPassword'
|
||||
|
||||
# Advanced Connection Pool settings in seconds
|
||||
#timeout: 30
|
||||
#lifetime: 30
|
||||
|
||||
Reference in New Issue
Block a user