package net.taehui.twilight.www import com.fasterxml.jackson.databind.ObjectMapper import io.netty.buffer.ByteBufUtil import io.netty.buffer.Unpooled import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.SimpleChannelInboundHandler import io.netty.handler.codec.http.* import io.netty.util.CharsetUtil import net.taehui.twilight.* import net.taehui.twilight.system.* import org.apache.commons.codec.binary.Hex import org.apache.commons.compress.compressors.xz.XZCompressorInputStream import org.apache.commons.io.IOUtils import org.apache.hc.client5.http.classic.methods.HttpGet import org.apache.hc.client5.http.impl.classic.HttpClients import org.slf4j.LoggerFactory import java.io.IOException import java.net.URLDecoder import java.nio.charset.StandardCharsets import java.nio.file.Files import java.security.MessageDigest import java.util.concurrent.CompletableFuture import kotlin.io.path.inputStream class WwwAvatar : SimpleChannelInboundHandler<FullHttpRequest>(), Logger { private var avatarIP = "" private var avatarEstimatedID = "" private fun getDefaultDrawingIf(drawing: ByteArray? = null): CompletableFuture<ByteArray?> { return if (drawing != null) CompletableFuture.completedFuture(drawing) else logValueFuture { try { HttpClients.createDefault().use { val dataGet = HttpGet(Configure.www.taehui + "/avatar/drawing") dataGet.setHeader("X-Real-IP", avatarIP) it.execute(dataGet, HCDataHandler()) } } catch (e: IOException) { logFault(e) null } } } fun send(handler: ChannelHandlerContext, text: Any?) { val r = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(ObjectMapper().writeValueAsString(text), CharsetUtil.UTF_8) ) val hs = r.headers() hs[HttpHeaderNames.CONTENT_TYPE] = "application/json" hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8 handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE) } fun send(handler: ChannelHandlerContext, text: Double) { val r = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(text.toString(), CharsetUtil.UTF_8) ) val hs = r.headers() hs[HttpHeaderNames.CONTENT_TYPE] = "text/plain" hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8 handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE) } fun send(handler: ChannelHandlerContext, text: String) { val r = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer( text.toByteArray( StandardCharsets.UTF_8 ) ) ) val hs = r.headers() hs[HttpHeaderNames.CONTENT_TYPE] = "text/plain" hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8 handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE) } fun send(handler: ChannelHandlerContext, data: ByteArray?) { if (data != null) { handler.writeAndFlush( DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(data) ) ).addListener( ChannelFutureListener.CLOSE ) } else { send204(handler) } } private fun send204(handler: ChannelHandlerContext) { handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT)).addListener( ChannelFutureListener.CLOSE ) } private fun send400(handler: ChannelHandlerContext) { handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)) .addListener( ChannelFutureListener.CLOSE ) } private fun send403(handler: ChannelHandlerContext) { handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN)).addListener( ChannelFutureListener.CLOSE ) } private fun send404(handler: ChannelHandlerContext) { handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)).addListener( ChannelFutureListener.CLOSE ) } private val loggerID: String get() { val avatarID = if (avatarEstimatedID.isNotEmpty()) "$avatarEstimatedID?" else "" return if (avatarID.isNotEmpty()) "$avatarIP (${avatarID})" else avatarIP } public override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) { avatarIP = msg.headers()["X-Real-IP"] ?: "localhost" avatarEstimatedID = AvatarIPSystem.getAvatarID(avatarIP) val mode = msg.method() if (avatarIP != "localhost") { logInfo("$mode ${URLDecoder.decode(msg.uri(), StandardCharsets.UTF_8)}") } val data = QueryStringDecoder(msg.uri()) when (mode) { HttpMethod.GET -> { val params = data.parameters().map { Pair(it.key, it.value[0]) }.toMap() val language = params.getOrDefault("language", "en-US") when (data.path()) { "/qwilight/www/note" -> DB.getNote(params).thenAccept { send(ctx, it) } "/qwilight/www/comment" -> { val noteID = params.getOrDefault("noteID", "") if (BannedNote.isBanned(noteID)) { send(ctx, object { val favor = null val totalFavor = 0 val comments = null }) } else { val avatarID = params.getOrDefault("avatarID", "") val commentID = params.getOrDefault("commentID", "") val target = params.getOrDefault("target", "false").toBoolean() if (noteID.isEmpty()) { send400(ctx) } else { if (commentID.isEmpty()) { DB.getComment(noteID, avatarID, language, target, this).thenAccept { send(ctx, it) } } else { XZCompressorInputStream( TwilightComponent.COMMENT_ENTRY_PATH.resolve( noteID.substring( 0, noteID.indexOf(':') ) ).resolve("$commentID.xz") .inputStream() ).use { send(ctx, IOUtils.toByteArray(it)) } } } } } "/qwilight/www/avatar" -> DB.getAvatar(params).thenAccept { if (it != null) { send(ctx, it) } else { send204(ctx) } } "/qwilight/www/wow" -> DB.getWow().thenAccept { send(ctx, it) } "/qwilight/www/etc" -> { DB.getEtc(language).thenAccept { send(ctx, it) } } "/qwilight/www/level" -> { val levelName = params.getOrDefault("levelName", "") if (levelName.isEmpty()) { val levelID = params.getOrDefault("levelID", "") if (levelID.isEmpty()) { if (params.containsKey("avatarID")) { send(ctx, LevelSystem.getLevelNames(params.getOrDefault("avatarID", ""))) } else { val avatarIDMe = params.getOrDefault("avatarIDMe", "") if (avatarIDMe.isEmpty()) { send(ctx, object { val avatars = LevelSystem.avatars val levelIDs = emptyArray<String>() }) } else { DB.getClearedLevelIDs( avatarIDMe ).thenAccept { send(ctx, object { val avatars = LevelSystem.avatars val levelIDs = it }) } } } } else { LevelSystem.getLevelItem(levelID)?.let { levelItem -> DB.getLevelNote(levelItem).thenAccept { send( ctx, object { val levelNote = it val stand = levelItem.stand val point = levelItem.point val band = levelItem.band val judgments = levelItem.judgments val autoMode = levelItem.autoMode val noteSaltMode = levelItem.noteSaltMode val audioMultiplier = levelItem.audioMultiplier val faintNoteMode = levelItem.faintNoteMode val judgmentMode = levelItem.judgmentMode val hitPointsMode = levelItem.hitPointsMode val noteMobilityMode = levelItem.noteMobilityMode val longNoteMode = levelItem.longNoteMode val inputFavorMode = levelItem.inputFavorMode val noteModifyMode = levelItem.noteModifyMode val bpmMode = levelItem.bpmMode val waveMode = levelItem.waveMode val setNoteMode = levelItem.setNoteMode val lowestJudgmentConditionMode = levelItem.lowestJudgmentConditionMode val allowPause = levelItem.allowPause val titles = TitleSystem.getTitles(levelID, language) val edgeIDs = EdgeSystem.getEdgeIDs(levelID) } ) } } } } else { val levelGroup = LevelSystem.getLevelGroup(levelName) if (levelGroup != null) { DB.getLevels( levelGroup ).thenAccept { send(ctx, it) } } } } "/qwilight/www/vote" -> { val voteName = params.getOrDefault("voteName", "") if (voteName.isEmpty()) { send(ctx, VoteSystem.voteNames) } else { send(ctx, VoteSystem.getVoteItems(voteName)) } } "/qwilight/www/sites" -> send(ctx, SiteHandler.getCalledSites()) "/qwilight/www/defaultNoteDate" -> { val defaultNoteDate = Configure.defaultNoteDate if (defaultNoteDate > params.getOrDefault("date", "0").toLong()) { send(ctx, object { val date = defaultNoteDate }) } else { send204(ctx) } } "/qwilight/www/defaultUIDate" -> { val defaultUIDate = Configure.defaultUIDate if (defaultUIDate > params.getOrDefault("date", "0").toLong()) { send(ctx, object { val date = defaultUIDate }) } else { send204(ctx) } } "/qwilight/www/title" -> DB.getTitle(params).thenAccept { if (it != null) { send(ctx, it) } else { send204(ctx) } } "/qwilight/www/titles" -> DB.getTitles( params ).thenAccept { send(ctx, it) } "/qwilight/www/edge" -> { val drawing = EdgeSystem.getDrawing(params.getOrDefault("edgeID", "")) if (drawing != null) { send(ctx, drawing) } else { send204(ctx) } } "/qwilight/www/edges" -> DB.getEdgeIDs( params ).thenAccept { send(ctx, it) } "/qwilight/www/drawing" -> { val avatarID = Utility.getDefaultAvatarID(params.getOrDefault("avatarID", "")) val drawingVariety = params.getOrDefault("drawingVariety", "") val abilityClass5K = params.getOrDefault("abilityClass5K", "") val abilityClass7K = params.getOrDefault("abilityClass7K", "") val abilityClass9K = params.getOrDefault("abilityClass9K", "") if (drawingVariety.isNotEmpty()) { if (avatarID.isEmpty()) { when (drawingVariety) { "0" -> getDefaultDrawingIf().thenAccept { send(ctx, it) } "2" -> send(ctx, EdgeSystem.getDrawing("Default")) else -> send400(ctx) } } else if (avatarID.startsWith("*")) { when (drawingVariety) { "0" -> getDefaultDrawingIf(ValveSystem.getDrawing(avatarID)).thenAccept { send( ctx, it ) } "2" -> send(ctx, EdgeSystem.getDrawing("Default")) else -> send400(ctx) } } else if (avatarID.startsWith("$")) { when (drawingVariety) { "0" -> getDefaultDrawingIf(PlatformSystem.getDrawing(avatarID)).thenAccept { send( ctx, it ) } "2" -> send(ctx, EdgeSystem.getDrawing("Default")) else -> send400(ctx) } } else { when (drawingVariety) { "0" -> logFuture { HttpClients.createDefault().use { val dataGet = HttpGet( Configure.www.taehui + "/avatar/drawing/" + Utility.getDefaultAvatarID( avatarID ) ) dataGet.setHeader("X-Real-IP", avatarIP) send(ctx, it.execute(dataGet, HCDataHandler())) } } "2" -> logFuture { val edge = DB.getAvatarEdge(Utility.getDefaultAvatarID(avatarID)) send(ctx, EdgeSystem.getDrawing(edge)) } else -> send400(ctx) } } } else if (abilityClass5K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.ABILITY_CLASS_5K, abilityClass5K.toDouble() ) ) } else if (abilityClass7K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.ABILITY_CLASS_7K, abilityClass7K.toDouble() ) ) } else if (abilityClass9K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.ABILITY_CLASS_9K, abilityClass9K.toDouble() ) ) } else { send400(ctx) } } else -> send404(ctx) } } HttpMethod.POST -> { when (data.path()) { "/qwilight/www/fault" -> { QwilightLogging(loggerID).logInfo( msg.content().toString(StandardCharsets.UTF_8) ) ctx.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED)) .addListener( ChannelFutureListener.CLOSE ) } "/qwilight/www/note" -> { val noteFileContents = ByteBufUtil.getBytes(msg.content()) val hashComputer512 = MessageDigest.getInstance("SHA-512") hashComputer512.update(noteFileContents) val hashComputer128 = MessageDigest.getInstance("MD5") hashComputer128.update(noteFileContents) val hashComputer256 = MessageDigest.getInstance("SHA-256") hashComputer256.update(noteFileContents) val noteID512 = Hex.encodeHexString(hashComputer512.digest()) val noteID128 = Hex.encodeHexString(hashComputer128.digest()) val noteID256 = Hex.encodeHexString(hashComputer256.digest()) val targetComputing = BaseCompiler.handleCompile(noteFileContents) if (targetComputing.isBanned || BannedNote.isBanned(noteID512)) { send403(ctx) } else { logFuture { Files.write(TwilightComponent.NOTE_ENTRY_PATH.resolve(noteID512), noteFileContents) DB.setNote( "$noteID512:0", noteID128, noteID256, targetComputing ) send204(ctx) } } } else -> { send404(ctx) } } } else -> { ctx.writeAndFlush( DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED ) ) .addListener( ChannelFutureListener.CLOSE ) } } } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { logFault(cause) } override fun logInfo(toNotify: String) { LoggerFactory.getLogger(javaClass).info("[{}] {}", loggerID, toNotify) } override fun logFault(e: Throwable) { if (Utility.isValidFault(e)) { LoggerFactory.getLogger(javaClass).error("[{}] {}", avatarIP, Utility.getFault(e)) } } }