|
| 1 | +/* |
| 2 | + * Anvil - AnvilPowered |
| 3 | + * Copyright (C) 2020-2021 |
| 4 | + * |
| 5 | + * This program is free software: you can redistribute it and/or modify |
| 6 | + * it under the terms of the GNU Lesser General Public License as published by |
| 7 | + * the Free Software Foundation, either version 3 of the License, or |
| 8 | + * (at your option) any later version. |
| 9 | + * |
| 10 | + * This program is distributed in the hope that it will be useful, |
| 11 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | + * GNU Lesser General Public License for more details. |
| 14 | + * |
| 15 | + * You should have received a copy of the GNU Lesser General Public License |
| 16 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 17 | + */ |
| 18 | + |
| 19 | +package org.anvilpowered.anvil.bungee.metric |
| 20 | + |
| 21 | +import com.google.gson.JsonArray |
| 22 | +import com.google.gson.JsonObject |
| 23 | +import com.google.inject.Inject |
| 24 | +import java.io.BufferedReader |
| 25 | +import java.io.BufferedWriter |
| 26 | +import java.io.ByteArrayOutputStream |
| 27 | +import java.io.DataOutputStream |
| 28 | +import java.io.File |
| 29 | +import java.io.FileReader |
| 30 | +import java.io.FileWriter |
| 31 | +import java.io.IOException |
| 32 | +import java.io.InputStreamReader |
| 33 | +import java.lang.reflect.InvocationTargetException |
| 34 | +import java.net.URL |
| 35 | +import java.nio.charset.StandardCharsets |
| 36 | +import java.util.UUID |
| 37 | +import java.util.concurrent.TimeUnit |
| 38 | +import java.util.zip.GZIPOutputStream |
| 39 | +import javax.net.ssl.HttpsURLConnection |
| 40 | +import net.md_5.bungee.api.ProxyServer |
| 41 | +import net.md_5.bungee.api.plugin.Plugin |
| 42 | +import net.md_5.bungee.config.Configuration |
| 43 | +import net.md_5.bungee.config.ConfigurationProvider |
| 44 | +import net.md_5.bungee.config.YamlConfiguration |
| 45 | +import org.anvilpowered.anvil.api.Environment |
| 46 | +import org.anvilpowered.anvil.common.metric.MetricService |
| 47 | +import org.slf4j.Logger |
| 48 | + |
| 49 | +class BungeeMetricService @Inject constructor( |
| 50 | + private val logger: Logger |
| 51 | +) : MetricService { |
| 52 | + |
| 53 | + private lateinit var environment: Environment |
| 54 | + private var serviceId = 0 |
| 55 | + private var enabled = false |
| 56 | + private var serverUUID: String? = null |
| 57 | + private var logFailedRequests = false |
| 58 | + private var logSentData = false |
| 59 | + private var logResponseStatusText = false |
| 60 | + private val knownMetricsInstances: MutableList<Any> = ArrayList() |
| 61 | + |
| 62 | + override fun initialize(env: Environment) { |
| 63 | + val bungeeId: Int? = env.pluginInfo.metricIds["bungeecord"] |
| 64 | + requireNotNull(bungeeId) { "Could not find a valid Metrics Id for BungeeCord. Please check your PluginInfo" } |
| 65 | + initialize(env, bungeeId) |
| 66 | + } |
| 67 | + |
| 68 | + private fun initialize(environment: Environment, serviceId: Int) { |
| 69 | + this.environment = environment |
| 70 | + this.serviceId = serviceId |
| 71 | + try { |
| 72 | + loadConfig() |
| 73 | + } catch (e: IOException) { |
| 74 | + logger.error("Failed to load bStats config!", e) |
| 75 | + return |
| 76 | + } |
| 77 | + |
| 78 | + if (!enabled) { |
| 79 | + return |
| 80 | + } |
| 81 | + |
| 82 | + val usedMetricsClass = getFirstBStatsClass() ?: return |
| 83 | + if (usedMetricsClass == javaClass) { |
| 84 | + linkMetrics(this) |
| 85 | + startSubmitting() |
| 86 | + } else { |
| 87 | + val logMsg = "Failed to link to first metrics class ${usedMetricsClass.name}" |
| 88 | + try { |
| 89 | + usedMetricsClass.getMethod("linkMetrics", Any::class.java).invoke(null, this) |
| 90 | + } catch (e: NoSuchMethodException) { |
| 91 | + if (logFailedRequests) { |
| 92 | + logger.error(logMsg, e) |
| 93 | + } |
| 94 | + } catch (e: IllegalAccessException) { |
| 95 | + if (logFailedRequests) { |
| 96 | + logger.error(logMsg, e) |
| 97 | + } |
| 98 | + } catch (e: InvocationTargetException) { |
| 99 | + if (logFailedRequests) { |
| 100 | + logger.error(logMsg, e) |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + private fun linkMetrics(metrics: Any) = knownMetricsInstances.add(metrics) |
| 107 | + |
| 108 | + private fun startSubmitting() { |
| 109 | + val initialDelay = (1000 * 60 * (3 + Math.random() * 3)).toLong() |
| 110 | + val secondDelay = (1000 * 60 * (Math.random() * 30)).toLong() |
| 111 | + val plugin = environment.plugin as Plugin |
| 112 | + ProxyServer.getInstance().scheduler.schedule(plugin, { submitData() }, initialDelay, TimeUnit.MILLISECONDS) |
| 113 | + ProxyServer.getInstance().scheduler.schedule( |
| 114 | + plugin, { submitData() }, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS |
| 115 | + ) |
| 116 | + } |
| 117 | + |
| 118 | + private fun getServerData(): JsonObject { |
| 119 | + val proxyInstance = ProxyServer.getInstance() |
| 120 | + val data = JsonObject() |
| 121 | + data.addProperty("serverUUID", serverUUID) |
| 122 | + data.addProperty("playerAmount", proxyInstance.onlineCount) |
| 123 | + data.addProperty("managedServers", proxyInstance.servers.size) |
| 124 | + data.addProperty("onlineMode", if (proxyInstance.config.isOnlineMode) 1 else 0) |
| 125 | + data.addProperty("bungeecordVersion", proxyInstance.version) |
| 126 | + data.addProperty("javaVersion", System.getProperty("java.version")) |
| 127 | + data.addProperty("osName", System.getProperty("os.name")) |
| 128 | + data.addProperty("osArch", System.getProperty("os.arch")) |
| 129 | + data.addProperty("osVersion", System.getProperty("os.version")) |
| 130 | + data.addProperty("coreCount", Runtime.getRuntime().availableProcessors()) |
| 131 | + return data |
| 132 | + } |
| 133 | + |
| 134 | + private fun submitData() { |
| 135 | + val data: JsonObject = getServerData() |
| 136 | + val pluginData = JsonArray() |
| 137 | + for (metrics in knownMetricsInstances) { |
| 138 | + try { |
| 139 | + val plugin = metrics.javaClass.getMethod("getPluginData").invoke(metrics) |
| 140 | + if (plugin is JsonObject) { |
| 141 | + pluginData.add(plugin) |
| 142 | + } |
| 143 | + } catch (ignored: Exception) { |
| 144 | + } |
| 145 | + } |
| 146 | + data.add("plugins", pluginData) |
| 147 | + try { |
| 148 | + sendData(data) |
| 149 | + } catch (e: Exception) { |
| 150 | + if (logFailedRequests) { |
| 151 | + logger.error("Could not submit plugin stats!", e) |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + @Throws(IOException::class) |
| 157 | + private fun loadConfig() { |
| 158 | + val bStatsFolder = File("plugins/bStats") |
| 159 | + check(bStatsFolder.mkdirs()) { "Could not create the config directory for bStats!" } |
| 160 | + val configFile = File(bStatsFolder, "config.yml") |
| 161 | + check(configFile.mkdirs()) { "Could not create the config file for bStats!" } |
| 162 | + |
| 163 | + writeFile( |
| 164 | + configFile, |
| 165 | + "#bStats collects some data for plugin authors like how many servers are using their plugins.", |
| 166 | + "#To honor their work, you should not disable it.", |
| 167 | + "#This has nearly no effect on the server performance!", |
| 168 | + "#Check out https://bStats.org/ to learn more :)", |
| 169 | + "enabled: true", |
| 170 | + "serverUuid: \"" + UUID.randomUUID() + "\"", |
| 171 | + "logFailedRequests: false", |
| 172 | + "logSentData: false", |
| 173 | + "logResponseStatusText: false" |
| 174 | + ) |
| 175 | + |
| 176 | + val configuration: Configuration = ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(configFile) |
| 177 | + |
| 178 | + enabled = configuration.getBoolean("enabled", true) |
| 179 | + serverUUID = configuration.getString("serverUuid") |
| 180 | + logFailedRequests = configuration.getBoolean("logFailedRequests", false) |
| 181 | + logSentData = configuration.getBoolean("logSentData", false) |
| 182 | + logResponseStatusText = configuration.getBoolean("logResponseStatusText", false) |
| 183 | + } |
| 184 | + |
| 185 | + private fun getFirstBStatsClass(): Class<*>? { |
| 186 | + val bStatsFolder = File("plugins/bStats") |
| 187 | + bStatsFolder.mkdirs() |
| 188 | + check(bStatsFolder.mkdirs()) { "Could not create the bStats config folder!" } |
| 189 | + val tempFile = File(bStatsFolder, "temp.txt") |
| 190 | + return try { |
| 191 | + val className = readFile(tempFile) |
| 192 | + if (className != null) { |
| 193 | + try { |
| 194 | + return Class.forName(className) |
| 195 | + } catch (ignored: ClassNotFoundException) { |
| 196 | + } |
| 197 | + } |
| 198 | + writeFile(tempFile, javaClass.name) |
| 199 | + javaClass |
| 200 | + } catch (e: IOException) { |
| 201 | + if (logFailedRequests) { |
| 202 | + logger.error("Failed to get first bStats class!", e) |
| 203 | + } |
| 204 | + null |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + @Throws(IOException::class) |
| 209 | + private fun readFile(file: File): String? { |
| 210 | + return if (!file.exists()) { |
| 211 | + null |
| 212 | + } else { |
| 213 | + BufferedReader(FileReader(file)).use { it.readLine() } |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + @Throws(IOException::class) |
| 218 | + private fun writeFile(file: File, vararg lines: String) { |
| 219 | + BufferedWriter(FileWriter(file)).use { |
| 220 | + for (line in lines) { |
| 221 | + it.write(line) |
| 222 | + it.newLine() |
| 223 | + } |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + private fun sendData(data: JsonObject?) { |
| 228 | + requireNotNull(data) { "Data cannot be null" } |
| 229 | + if (logSentData) { |
| 230 | + logger.info("Sending data to bStats: $data") |
| 231 | + } |
| 232 | + val connection: HttpsURLConnection = URL("https://bStats.org/submitData/bungeecord").openConnection() as HttpsURLConnection |
| 233 | + val compressedData = compress(data.toString()) |
| 234 | + |
| 235 | + connection.requestMethod = "POST" |
| 236 | + connection.addRequestProperty("Accept", "application/json") |
| 237 | + connection.addRequestProperty("Connection", "close") |
| 238 | + connection.addRequestProperty("Content-Encoding", "gzip") |
| 239 | + connection.addRequestProperty("Content-Length", compressedData!!.size.toString()) |
| 240 | + connection.setRequestProperty("Content-Type", "application/json") |
| 241 | + connection.setRequestProperty("User-Agent", "MC-Server/1") |
| 242 | + |
| 243 | + connection.doOutput = true |
| 244 | + DataOutputStream(connection.outputStream).use { outputStream -> outputStream.write(compressedData) } |
| 245 | + val builder = StringBuilder() |
| 246 | + BufferedReader(InputStreamReader(connection.inputStream)).use { bufferedReader -> |
| 247 | + var line: String? |
| 248 | + while (bufferedReader.readLine().also { line = it } != null) { |
| 249 | + builder.append(line) |
| 250 | + } |
| 251 | + } |
| 252 | + if (logResponseStatusText) { |
| 253 | + logger.info("Sent data to bStats and received response: $builder") |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + private fun compress(str: String?): ByteArray? { |
| 258 | + if (str == null) { |
| 259 | + return null |
| 260 | + } |
| 261 | + val outputStream = ByteArrayOutputStream() |
| 262 | + GZIPOutputStream(outputStream).use { gzip -> gzip.write(str.toByteArray(StandardCharsets.UTF_8)) } |
| 263 | + return outputStream.toByteArray() |
| 264 | + } |
| 265 | +} |
0 commit comments