/* Mangadex@Home Copyright (c) 2020, MangaDex Network This file is part of MangaDex@Home. MangaDex@Home is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. MangaDex@Home is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this MangaDex@Home. If not, see . */ package mdnet.server import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import mdnet.data.Token import mdnet.logging.info import mdnet.security.TweetNaclFast import org.http4k.core.Filter import org.http4k.core.HttpHandler import org.http4k.core.Response import org.http4k.core.Status import org.http4k.lens.LensFailure import org.http4k.lens.Path import org.slf4j.LoggerFactory import java.time.OffsetDateTime import java.util.Base64 class TokenVerifier(tokenKey: ByteArray, private val isDisabled: Boolean) : Filter { private val box = TweetNaclFast.SecretBox(tokenKey) override fun invoke(next: HttpHandler): HttpHandler { return then@{ if (isDisabled) { return@then next(it) } val chapterHash = Path.of("chapterHash")(it) val cleanedUri = it.uri.path.replaceBefore("/data", "/{token}") val tokenArr = try { val toDecode = try { Path.of("token")(it) } catch (e: LensFailure) { LOGGER.info(e) { "Request for $cleanedUri rejected for missing token" } return@then Response(Status.FORBIDDEN).body("Token is missing") } Base64.getUrlDecoder().decode(toDecode) } catch (e: IllegalArgumentException) { LOGGER.info(e) { "Request for $cleanedUri rejected for non-base64 token" } return@then Response(Status.FORBIDDEN).body("Token is invalid base64") } if (tokenArr.size < 24) { LOGGER.info { "Request for $cleanedUri rejected for invalid token" } return@then Response(Status.FORBIDDEN) } val token = try { JACKSON.readValue( box.open(tokenArr.sliceArray(24 until tokenArr.size), tokenArr.sliceArray(0 until 24)).apply { if (this == null) { LOGGER.info { "Request for $cleanedUri rejected for invalid token" } return@then Response(Status.FORBIDDEN) } } ) } catch (e: JsonProcessingException) { LOGGER.info(e) { "Request for $cleanedUri rejected for invalid token" } return@then Response(Status.FORBIDDEN).body("Token is invalid") } if (OffsetDateTime.now().isAfter(token.expires)) { LOGGER.info { "Request for $cleanedUri rejected for expired token" } return@then Response(Status.GONE).body("Token has expired") } if (token.hash != chapterHash) { LOGGER.info { "Request for $cleanedUri rejected for inapplicable token" } return@then Response(Status.FORBIDDEN).body("Token is inapplicable for the image") } return@then next(it) } } companion object { private val LOGGER = LoggerFactory.getLogger(TokenVerifier::class.java) private val JACKSON: ObjectMapper = jacksonObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .registerModule(JavaTimeModule()) } }