diff --git a/Server/src/main/java/core/game/content/global/report/AbuseReport.java b/Server/src/main/java/core/game/content/global/report/AbuseReport.java index ab7594032..c639aa11a 100644 --- a/Server/src/main/java/core/game/content/global/report/AbuseReport.java +++ b/Server/src/main/java/core/game/content/global/report/AbuseReport.java @@ -1,6 +1,7 @@ package core.game.content.global.report; import core.game.node.entity.player.Player; +import discord.Discord; import rs09.game.system.command.CommandMapping; /** @@ -50,6 +51,7 @@ public final class AbuseReport { CommandMapping.INSTANCE.get("mute").attemptHandling(player, new String[] {"mute", victim, "48h"}); } player.getPacketDispatch().sendMessage("Thank-you, your abuse report has been received."); + Discord.INSTANCE.postPlayerAlert(victim, "Abuse Report - " + rule.name()); } /** diff --git a/Server/src/main/kotlin/discord/Discord.kt b/Server/src/main/kotlin/discord/Discord.kt index a4e73b632..33f980433 100644 --- a/Server/src/main/kotlin/discord/Discord.kt +++ b/Server/src/main/kotlin/discord/Discord.kt @@ -1,6 +1,7 @@ package discord import api.getItemName +import core.game.node.entity.player.Player import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.json.simple.JSONArray @@ -24,7 +25,7 @@ object Discord { GlobalScope.launch { val offer = encodeOfferJson(isSale, itemId, value, qty, user) try { - sendJsonPost(offer) + sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) } catch (e: Exception) { e.printStackTrace() } @@ -36,7 +37,18 @@ object Discord { GlobalScope.launch { val offer = encodeUpdateJson(isSale, itemId, value, amtLeft) try { - sendJsonPost(offer) + sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun postPlayerAlert(player: String, type: String) { + GlobalScope.launch { + val alert = encodeUserAlert(type, player) + try { + sendJsonPost(ServerConstants.DISCORD_MOD_WEBHOOK, alert) } catch (e: Exception) { e.printStackTrace() } @@ -88,6 +100,23 @@ object Discord { return obj.toJSONString() } + private fun encodeUserAlert(type: String, player: String) : String { + val obj = JSONObject() + val embeds = JSONArray() + val embed = JSONObject() + + val fields = arrayOf( + EmbedField("Player", player, false), + EmbedField("Type", type, false) + ) + + embed["title"] = "Player Alert" + embed["fields"] = getFields(fields) + embeds.add(embed) + obj["embeds"] = embeds + return obj.toJSONString() + } + private fun getFields(fields: Array): JSONArray { val arr = JSONArray() @@ -110,8 +139,8 @@ object Discord { return obj } - private fun sendJsonPost(data: String) { - val conn = URL(ServerConstants.DISCORD_GE_WEBHOOK).openConnection() as HttpURLConnection + private fun sendJsonPost(url: String = ServerConstants.DISCORD_GE_WEBHOOK, data: String) { + val conn = URL(url).openConnection() as HttpURLConnection conn.doOutput = true conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") diff --git a/Server/src/main/kotlin/rs09/ServerConstants.kt b/Server/src/main/kotlin/rs09/ServerConstants.kt index 250f6e24e..62be424cc 100644 --- a/Server/src/main/kotlin/rs09/ServerConstants.kt +++ b/Server/src/main/kotlin/rs09/ServerConstants.kt @@ -233,6 +233,9 @@ class ServerConstants { @JvmField var DISCORD_GE_WEBHOOK = "" + @JvmField + var DISCORD_MOD_WEBHOOK = "" + @JvmField var PRELOAD_MAP = false diff --git a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt index 0ae6cf706..82e8be15d 100644 --- a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt +++ b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt @@ -10,6 +10,7 @@ import core.game.world.map.Location import core.game.world.map.RegionManager import core.game.world.map.path.Pathfinder import core.game.world.update.flag.context.Graphics +import discord.Discord import rs09.game.content.ame.events.MysteriousOldManNPC import rs09.game.content.global.WeightBasedTable import rs09.tools.secondsToTicks @@ -104,6 +105,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { player.properties.teleportLocation = Location.create(3212, 9620, 0) } player.graphics(SMOKE_GRAPHICS) + Discord.postPlayerAlert(player.username, "Ignored Random") } override fun clear() { diff --git a/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt b/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt index 7a93125b4..5c4db9bab 100644 --- a/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt +++ b/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt @@ -7,6 +7,8 @@ import rs09.game.world.GameWorld import core.game.world.map.Location import rs09.game.world.repository.Repository import core.plugin.Initializable +import rs09.ServerStore +import rs09.ServerStore.Companion.addToList import rs09.game.system.command.Privilege import java.util.concurrent.TimeUnit @@ -84,6 +86,59 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ * ============================================================================================================= */ + /** + * Ban all players on a given IP + * ============================================================================================================= + */ + define("ipban", Privilege.ADMIN, "::ipban IP TIME", "Bans all players on the given ip. Time format: INTd/s/m/h ex: 30d for 30 days."){ player, args -> + val ip = args[1] + val durationString = args[2] + val durationTokens = durationString.toCharArray() + var intToken = "" + var durationMillis = 0L + var durationUnit: TimeUnit = TimeUnit.NANOSECONDS + for(token in durationTokens){ + if(token.toString().toIntOrNull() != null) intToken += token + else { + val durationInt: Int = (intToken.toIntOrNull() ?: -1).also { if(it == -1) reject(player, "Invalid duration: $intToken") } + durationUnit = when(token) { + 'd' -> TimeUnit.DAYS + 's' -> TimeUnit.SECONDS + 'm' -> TimeUnit.MINUTES + 'h' -> TimeUnit.HOURS + else -> TimeUnit.SECONDS + } + durationMillis = durationUnit.toMillis(durationInt.toLong()) + } + } + + val playersToBan = GameWorld.accountStorage.getUsernamesWithIP(ip) + if (playersToBan.isEmpty()) { + reject(player, "No accounts found on IP $ip") + } + + for (p in playersToBan) { + val playerToKick = Repository.getPlayerByName(p) + playerToKick?.details?.accountInfo?.banEndTime = System.currentTimeMillis() + durationMillis + playerToKick?.clear(true) + GameWorld.Pulser.submit(object : Pulse(2) { + override fun pulse(): Boolean { + val info = GameWorld.accountStorage.getAccountInfo(p) + info.banEndTime = System.currentTimeMillis() + durationMillis + GameWorld.accountStorage.update(info) + return true + } + }) + } + + ServerStore.getArchive("flagged-ips").addToList("ips", ip) + + notify(player, "Banned all accounts on $ip for $intToken ${durationUnit.name.toLowerCase()}.") + } + /** + * ============================================================================================================= + */ + /** * Mute a player * ============================================================================================================= diff --git a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt index a063f4506..a05d268ff 100644 --- a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt +++ b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt @@ -124,6 +124,7 @@ object ServerConfigParser { ServerConstants.USE_AUTH = data.getBoolean("server.use_auth", true) ServerConstants.PERSIST_ACCOUNTS = data.getBoolean("server.persist_accounts", true) ServerConstants.DAILY_ACCOUNT_LIMIT = data.getLong("server.daily_accounts_per_ip", 3L).toInt() + ServerConstants.DISCORD_MOD_WEBHOOK = data.getString("server.moderation_webhook", "") } diff --git a/Server/src/main/kotlin/rs09/net/packet/in/Login.kt b/Server/src/main/kotlin/rs09/net/packet/in/Login.kt index b651e4b47..9fdaf0d6d 100644 --- a/Server/src/main/kotlin/rs09/net/packet/in/Login.kt +++ b/Server/src/main/kotlin/rs09/net/packet/in/Login.kt @@ -10,6 +10,7 @@ import core.game.node.entity.player.info.login.LoginType import core.net.Constants import core.net.IoSession import core.tools.StringUtils +import discord.Discord import proto.management.JoinClanRequest import proto.management.PlayerStatusUpdate import proto.management.RequestContactInfo @@ -124,6 +125,12 @@ object Login { details.session = session details.info.translate(UIDInfo(details.ipAddress, "DEPRECATED", "DEPRECATED", "DEPRECATED")) + val archive = ServerStore.getArchive("flagged-ips") + val flaggedIps = archive.getList("ips") + if (flaggedIps.contains(details.ipAddress)) { + Discord.postPlayerAlert(details.username, "Login from flagged IP ${details.ipAddress}") + } + if (checkAccountLimit(details.ipAddress, details.username)) { val player = Player(details) if (Repository.getPlayerByName(player.name) == null) { diff --git a/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt index 9cbbe0361..adf5a4853 100644 --- a/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt +++ b/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt @@ -5,6 +5,7 @@ import rs09.auth.UserAccountInfo interface AccountStorageProvider { fun checkUsernameTaken(username: String): Boolean fun getAccountInfo(username: String): UserAccountInfo + fun getUsernamesWithIP(ip: String) : List fun store(info: UserAccountInfo) fun update(info: UserAccountInfo) fun remove(info: UserAccountInfo) diff --git a/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt index 65923dc47..2e5d6fd88 100644 --- a/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt +++ b/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt @@ -28,4 +28,8 @@ class InMemoryStorageProvider : AccountStorageProvider { override fun getOnlineFriends(username: String): List { return ArrayList() } + + override fun getUsernamesWithIP(ip: String): List { + return ArrayList() + } } diff --git a/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt index dc42ba1ff..5fa6bda8b 100644 --- a/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt +++ b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt @@ -177,9 +177,24 @@ class SQLStorageProvider : AccountStorageProvider { return friends } + override fun getUsernamesWithIP(ip: String): List { + val conn = getConnection() + val res = ArrayList() + conn.use { + val query = it.prepareStatement(accountsByIPQuery) + query.setString(1, ip) + val r = query.executeQuery() + while (r.next()) { + res.add(r.getString(1)) + } + } + return res + } + companion object { private const val usernameQuery = "SELECT username FROM members WHERE username = ?;" private const val removeInfoQuery = "DELETE FROM members WHERE username = ?;" + private const val accountsByIPQuery = "SELECT username FROM members WHERE lastGameIp = ?;" private const val accountInfoQuery = "SELECT " + "username," + "password," +