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.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.net.URLDecoder import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.concurrent.CompletableFuture import kotlin.io.path.inputStream class WwwAvatar : SimpleChannelInboundHandler<FullHttpRequest>(), Logger { private var avatarIP = "" private var avatarEstimatedID = "" 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") val avatarID = params.getOrDefault("avatarID", "") when (data.path()) { "/qwilight/www/note" -> { val viewUnit = params.getOrDefault("viewUnit", "20").toInt() if (viewUnit > 20) { send400(ctx) } else { DB.getNote( params.getOrDefault("want", ""), params.getOrDefault("src", "0").toInt(), params.getOrDefault("fit", "0").toInt(), params.getOrDefault("page", "1").toInt(), viewUnit ).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 handled = null val comments = null }) } else { if (noteID.isEmpty()) { send400(ctx) } else { val commentID = params.getOrDefault("commentID", "") if (commentID.isEmpty()) { val viewUnit = params.getOrDefault("viewUnit", "-1").toInt() val isUbuntu = params.getOrDefault("isUbuntu", "false").toBoolean() val ubuntuID = params.getOrDefault("ubuntuID", "") DB.getComment( noteID, Utility.getDefaultAvatarID(avatarID), language, viewUnit, isUbuntu, ubuntuID, this ).thenAccept { send(ctx, it) } } else { XZCompressorInputStream( TwilightComponent.COMMENT_ENTRY_PATH.resolve( Utility.getNoteID512(noteID) ).resolve("$commentID.xz") .inputStream() ).use { send(ctx, IOUtils.toByteArray(it)) } } } } } "/qwilight/www/avatar" -> { val want = params.getOrDefault("want", "") if (want.isEmpty()) { send400(ctx) }else { DB.getAvatar(want) .thenAccept { if (it != null) { send(ctx, it) } else { send204(ctx) } } } } "/qwilight/www/avatar/favorites/4K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_4, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/6K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_6, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/5K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_5_1, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/7K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_7_1, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/9K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_9, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/10K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_10_2, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/14K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_14_2, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/24K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_24_2, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/favorites/48K" -> DB.getAvatarFavorites( Component.InputMode.INPUT_MODE_48_4, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/4K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_4, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/6K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_6, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/5K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_5_1, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/7K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_7_1, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/9K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_9, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/10K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_10_2, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/14K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_14_2, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/24K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_24_2, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/lasts/48K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_48_4, avatarID) .thenAccept { send(ctx, it) } "/qwilight/www/avatar/ability/5K" -> DB.getAvatarAbility( Component.InputMode.INPUT_MODE_5_1, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/ability/7K" -> DB.getAvatarAbility( Component.InputMode.INPUT_MODE_7_1, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/ability/9K" -> DB.getAvatarAbility( Component.InputMode.INPUT_MODE_9, avatarID ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/wwwLevels" -> DB.getAvatarWwwLevels(avatarID).thenAccept { send(ctx, it) } "/qwilight/www/avatar/handled" -> DB.getAvatarHandled( avatarID, params.getOrDefault("levelName", "") ).thenAccept { send(ctx, it) } "/qwilight/www/avatar/levelVS" -> { val targetID = params.getOrDefault("targetID", "") val levelName = params.getOrDefault("levelName", "") if (avatarID.isEmpty() || targetID.isEmpty() || levelName.isEmpty() || avatarID == targetID) { send400(ctx) } else { DB.getAvatarLevelVS(avatarID, targetID, levelName).thenAccept { send(ctx, it) } } } "/qwilight/www/hall/totalTotal" -> DB.getHallTotalTotal().thenAccept { send(ctx, it) } "/qwilight/www/hall/atTotal" -> DB.getHallAtTotal().thenAccept { send(ctx, it) } "/qwilight/www/hall/totalTop" -> DB.getHallTotalTop().thenAccept { send(ctx, it) } "/qwilight/www/hall/atTop" -> DB.getHallAtTop().thenAccept { send(ctx, it) } "/qwilight/www/hall/totalStand" -> DB.getHallTotalStand().thenAccept { send(ctx, it) } "/qwilight/www/hall/atStand" -> DB.getHallAtStand().thenAccept { send(ctx, it) } "/qwilight/www/hall/totalBand" -> DB.getHallTotalBand().thenAccept { send(ctx, it) } "/qwilight/www/hall/atBand" -> DB.getHallAtBand().thenAccept { send(ctx, it) } "/qwilight/www/hall/ability/5K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_5_1) .thenAccept { send(ctx, it) } "/qwilight/www/hall/ability/7K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_7_1) .thenAccept { send(ctx, it) } "/qwilight/www/hall/ability/9K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_9) .thenAccept { send(ctx, it) } "/qwilight/www/hall/level" -> DB.getHallLevel().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(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 avatars = DB.getClearedAvatars(levelID) 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(Utility.getDefaultAvatarID(avatarID), language).thenAccept { if (it != null) { send(ctx, it) } else { send204(ctx) } } "/qwilight/www/titles" -> DB.getTitles( Utility.getDefaultAvatarID(avatarID), language ).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( Utility.getDefaultAvatarID(avatarID) ).thenAccept { send(ctx, it) } "/qwilight/www/drawing" -> { val drawingVariety = params.getOrDefault("drawingVariety", "") val abilityClass5K = params.getOrDefault("System/Ability Class/abilityClass5K", "") val abilityClass7K = params.getOrDefault("System/Ability Class/abilityClass7K", "") val abilityClass9K = params.getOrDefault("System/Ability Class/abilityClass9K", "") val levelID = params.getOrDefault("levelID", "") if (drawingVariety.isNotEmpty()) { when (drawingVariety) { "0" -> { fun sendAvatarDrawing(avatarID: String): CompletableFuture<Void> { return 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())) } } } if (avatarID.startsWith("*")) { val avatarDrawing = ValveSystem.getDrawing(Utility.getDefaultAvatarID(avatarID)) if (avatarDrawing != null) { send(ctx, avatarDrawing) } else { sendAvatarDrawing(avatarID) } } else if (avatarID.startsWith("$")) { if (PlatformIDSystem.hasAvatarID(avatarID)) { sendAvatarDrawing(avatarID) } else { val avatarDrawing = PlatformSystem.getDrawing(avatarID) if (avatarDrawing != null) { send(ctx, avatarDrawing) } else { sendAvatarDrawing(avatarID) } } } else { sendAvatarDrawing(avatarID) } } "2" -> DB.getAvatarEdge(Utility.getDefaultAvatarID(avatarID)).thenAccept { send(ctx, EdgeSystem.getDrawing(it)) } else -> send400(ctx) } } else if (abilityClass5K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K, abilityClass5K.toDouble() ) ) } else if (abilityClass7K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K, abilityClass7K.toDouble() ) ) } else if (abilityClass9K.isNotEmpty()) { send( ctx, AbilityClassSystem.getAbilityClass( AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K, abilityClass9K.toDouble() ) ) } else if (levelID.isNotEmpty()) { send(ctx, LevelSystem.getLevelIDDrawing(levelID)) } 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/toil" -> { 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 noteFileData = ByteBufUtil.getBytes(msg.content()) var isBanned = false BaseCompiler.handleCompile(noteFileData, -1).forEach { if (it.isBanned || BannedNote.isBanned(it.noteID)) { isBanned = true } else { logFuture { Files.write( TwilightComponent.NOTE_ENTRY_PATH.resolve(Utility.getID512(noteFileData)), noteFileData ) DB.setNote( it.noteID, Utility.getID128(noteFileData), Utility.getID256(noteFileData), it ) } } } if (isBanned) { send403(ctx) } else { 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.getFaultText(e)) } } }