mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
Merge branch 'testing' into 'master'
Cut 2.0.1 See merge request mangadex-pub/mangadex_at_home!87
This commit is contained in:
commit
52f23b0f53
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -17,6 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
## [2.0.1] - 2021-05-27
|
||||||
|
### Added
|
||||||
|
- [2021-05-27] Added SNI check to prevent people from simply scanning nodes [@carbotaniuman].
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [2021-05-21] Update metrics and fix cache directory leak [@carbotaniuman].
|
||||||
|
- [2021-05-21] Change headers to be wildcards [@carbotaniuman].
|
||||||
|
- [2021-05-27] Make sending the `Server` header configurable but off by default [@carbotaniuman].
|
||||||
|
|
||||||
## [2.0.0] - 2021-03-11
|
## [2.0.0] - 2021-03-11
|
||||||
### Changed
|
### Changed
|
||||||
- [2021-03-11] Switch back to HTTP/1.1 [@carbotaniuman].
|
- [2021-03-11] Switch back to HTTP/1.1 [@carbotaniuman].
|
||||||
|
@ -388,6 +397,7 @@ This release contains many breaking changes! Of note are the changes to the cach
|
||||||
- [2020-06-11] Tweaked logging configuration to reduce log file sizes by [@carbotaniuman].
|
- [2020-06-11] Tweaked logging configuration to reduce log file sizes by [@carbotaniuman].
|
||||||
|
|
||||||
[Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0...HEAD
|
[Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0...HEAD
|
||||||
|
[2.0.1]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0...2.0.1
|
||||||
[2.0.0]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc14...2.0.0
|
[2.0.0]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc14...2.0.0
|
||||||
[2.0.0-rc14]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc13...2.0.0-rc14
|
[2.0.0-rc14]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc13...2.0.0-rc14
|
||||||
[2.0.0-rc13]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc12...2.0.0-rc13
|
[2.0.0-rc13]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc12...2.0.0-rc13
|
||||||
|
|
|
@ -21,7 +21,7 @@ package mdnet
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val CLIENT_BUILD = 30
|
const val CLIENT_BUILD = 31
|
||||||
|
|
||||||
@JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
@JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
||||||
|
|
||||||
|
|
|
@ -67,26 +67,8 @@ class Main : Runnable {
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!Files.isDirectory(databaseFolder) || Files.isRegularFile(databaseFolder.resolveSibling(databaseFolder.fileName.toString() + ".mv.db"))) {
|
if (!Files.isDirectory(databaseFolder)) {
|
||||||
println()
|
throw IllegalArgumentException("Expected $databaseFolder to be a folder, was a file")
|
||||||
println()
|
|
||||||
println(
|
|
||||||
"""the --database option now takes in the folder with the database file!
|
|
||||||
|(it previously took in the path to the file without any extensions)
|
|
||||||
|if you are using docker update your docker mount settings!
|
|
||||||
|if you are not, manually move update your --database args!
|
|
||||||
|note: the database file itself should be named metadata.{extension}
|
|
||||||
|where {extension} can be `.db` or `.mv.db`
|
|
||||||
|
|
|
||||||
|If this is your first time seeing this message, please check out the support
|
|
||||||
|channel as things HAVE changed. Failure to do so WILL require
|
|
||||||
|a cache wipe.
|
|
||||||
""".trimMargin()
|
|
||||||
)
|
|
||||||
println()
|
|
||||||
println()
|
|
||||||
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.isRegularFile(databaseFolder)) {
|
if (Files.isRegularFile(databaseFolder)) {
|
||||||
|
|
|
@ -264,6 +264,7 @@ class ServerManager(
|
||||||
storage,
|
storage,
|
||||||
remoteSettings,
|
remoteSettings,
|
||||||
settings.serverSettings,
|
settings.serverSettings,
|
||||||
|
settings.devSettings,
|
||||||
settings.metricsSettings,
|
settings.metricsSettings,
|
||||||
statistics,
|
statistics,
|
||||||
registry,
|
registry,
|
||||||
|
|
|
@ -30,6 +30,8 @@ import io.netty.handler.codec.DecoderException
|
||||||
import io.netty.handler.codec.http.HttpObjectAggregator
|
import io.netty.handler.codec.http.HttpObjectAggregator
|
||||||
import io.netty.handler.codec.http.HttpServerCodec
|
import io.netty.handler.codec.http.HttpServerCodec
|
||||||
import io.netty.handler.codec.http.HttpServerKeepAliveHandler
|
import io.netty.handler.codec.http.HttpServerKeepAliveHandler
|
||||||
|
import io.netty.handler.ssl.SniCompletionEvent
|
||||||
|
import io.netty.handler.ssl.SniHandler
|
||||||
import io.netty.handler.ssl.SslContextBuilder
|
import io.netty.handler.ssl.SslContextBuilder
|
||||||
import io.netty.handler.stream.ChunkedWriteHandler
|
import io.netty.handler.stream.ChunkedWriteHandler
|
||||||
import io.netty.handler.timeout.ReadTimeoutException
|
import io.netty.handler.timeout.ReadTimeoutException
|
||||||
|
@ -41,6 +43,7 @@ import io.netty.handler.traffic.TrafficCounter
|
||||||
import io.netty.incubator.channel.uring.IOUring
|
import io.netty.incubator.channel.uring.IOUring
|
||||||
import io.netty.incubator.channel.uring.IOUringEventLoopGroup
|
import io.netty.incubator.channel.uring.IOUringEventLoopGroup
|
||||||
import io.netty.incubator.channel.uring.IOUringServerSocketChannel
|
import io.netty.incubator.channel.uring.IOUringServerSocketChannel
|
||||||
|
import io.netty.util.DomainWildcardMappingBuilder
|
||||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||||
import io.netty.util.internal.SystemPropertyUtil
|
import io.netty.util.internal.SystemPropertyUtil
|
||||||
import mdnet.Constants
|
import mdnet.Constants
|
||||||
|
@ -48,8 +51,9 @@ import mdnet.data.Statistics
|
||||||
import mdnet.logging.info
|
import mdnet.logging.info
|
||||||
import mdnet.logging.trace
|
import mdnet.logging.trace
|
||||||
import mdnet.logging.warn
|
import mdnet.logging.warn
|
||||||
|
import mdnet.settings.DevSettings
|
||||||
|
import mdnet.settings.RemoteSettings
|
||||||
import mdnet.settings.ServerSettings
|
import mdnet.settings.ServerSettings
|
||||||
import mdnet.settings.TlsCert
|
|
||||||
import org.http4k.core.HttpHandler
|
import org.http4k.core.HttpHandler
|
||||||
import org.http4k.server.Http4kChannelHandler
|
import org.http4k.server.Http4kChannelHandler
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
|
@ -136,8 +140,9 @@ sealed class NettyTransport(threads: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Netty(
|
class Netty(
|
||||||
private val tls: TlsCert,
|
private val remoteSettings: RemoteSettings,
|
||||||
private val serverSettings: ServerSettings,
|
private val serverSettings: ServerSettings,
|
||||||
|
private val devSettings: DevSettings,
|
||||||
private val statistics: Statistics
|
private val statistics: Statistics
|
||||||
) : ServerConfig {
|
) : ServerConfig {
|
||||||
override fun toServer(http: HttpHandler): Http4kServer = object : Http4kServer {
|
override fun toServer(http: HttpHandler): Http4kServer = object : Http4kServer {
|
||||||
|
@ -155,6 +160,7 @@ class Netty(
|
||||||
|
|
||||||
override fun start(): Http4kServer = apply {
|
override fun start(): Http4kServer = apply {
|
||||||
LOGGER.info { "Starting Netty!" }
|
LOGGER.info { "Starting Netty!" }
|
||||||
|
val tls = remoteSettings.tls!!
|
||||||
|
|
||||||
val certs = getX509Certs(tls.certificate)
|
val certs = getX509Certs(tls.certificate)
|
||||||
val sslContext = SslContextBuilder
|
val sslContext = SslContextBuilder
|
||||||
|
@ -167,7 +173,33 @@ class Netty(
|
||||||
.channelFactory(transport.factory)
|
.channelFactory(transport.factory)
|
||||||
.childHandler(object : ChannelInitializer<SocketChannel>() {
|
.childHandler(object : ChannelInitializer<SocketChannel>() {
|
||||||
public override fun initChannel(ch: SocketChannel) {
|
public override fun initChannel(ch: SocketChannel) {
|
||||||
ch.pipeline().addLast("ssl", sslContext.newHandler(ch.alloc()))
|
ch.pipeline().addLast(
|
||||||
|
"ssl",
|
||||||
|
SniHandler(DomainWildcardMappingBuilder(sslContext).build())
|
||||||
|
)
|
||||||
|
|
||||||
|
ch.pipeline().addLast(
|
||||||
|
"dropHostname",
|
||||||
|
object : ChannelInboundHandlerAdapter() {
|
||||||
|
private val hostToTest = remoteSettings.url.authority.let {
|
||||||
|
it.substring(0, it.lastIndexOf(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||||
|
if (evt is SniCompletionEvent) {
|
||||||
|
if (!devSettings.disableSniCheck) {
|
||||||
|
if (!evt.hostname().endsWith(hostToTest) &&
|
||||||
|
!evt.hostname().endsWith("localhost")
|
||||||
|
) {
|
||||||
|
ctx.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.fireUserEventTriggered(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ch.pipeline().addLast("codec", HttpServerCodec())
|
ch.pipeline().addLast("codec", HttpServerCodec())
|
||||||
ch.pipeline().addLast("keepAlive", HttpServerKeepAliveHandler())
|
ch.pipeline().addLast("keepAlive", HttpServerKeepAliveHandler())
|
||||||
|
|
|
@ -147,7 +147,7 @@ class ImageServer(
|
||||||
}
|
}
|
||||||
respondWithImage(tee, contentLength, contentType, lastModified, false)
|
respondWithImage(tee, contentLength, contentType, lastModified, false)
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info { "Request for $sanitizedUri is being served due to write errors" }
|
LOGGER.info { "Request for $sanitizedUri is being served as the cache is full" }
|
||||||
respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false)
|
respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,10 +181,10 @@ class ImageServer(
|
||||||
.then { next: HttpHandler ->
|
.then { next: HttpHandler ->
|
||||||
{ request: Request ->
|
{ request: Request ->
|
||||||
val response = next(request)
|
val response = next(request)
|
||||||
response.header("access-control-allow-origin", "https://mangadex.org")
|
response.header("access-control-allow-origin", "*")
|
||||||
.header("access-control-expose-headers", "*")
|
.header("access-control-expose-headers", "*")
|
||||||
.header("access-control-allow-methods", "GET")
|
.header("access-control-allow-methods", "GET")
|
||||||
.header("timing-allow-origin", "https://mangadex.org")
|
.header("timing-allow-origin", "*")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import mdnet.logging.warn
|
||||||
import mdnet.metrics.GeoIpMetricsFilterBuilder
|
import mdnet.metrics.GeoIpMetricsFilterBuilder
|
||||||
import mdnet.metrics.PostTransactionLabeler
|
import mdnet.metrics.PostTransactionLabeler
|
||||||
import mdnet.netty.Netty
|
import mdnet.netty.Netty
|
||||||
|
import mdnet.settings.DevSettings
|
||||||
import mdnet.settings.MetricsSettings
|
import mdnet.settings.MetricsSettings
|
||||||
import mdnet.settings.RemoteSettings
|
import mdnet.settings.RemoteSettings
|
||||||
import mdnet.settings.ServerSettings
|
import mdnet.settings.ServerSettings
|
||||||
|
@ -46,6 +47,7 @@ fun getServer(
|
||||||
storage: ImageStorage,
|
storage: ImageStorage,
|
||||||
remoteSettings: RemoteSettings,
|
remoteSettings: RemoteSettings,
|
||||||
serverSettings: ServerSettings,
|
serverSettings: ServerSettings,
|
||||||
|
devSettings: DevSettings,
|
||||||
metricsSettings: MetricsSettings,
|
metricsSettings: MetricsSettings,
|
||||||
statistics: Statistics,
|
statistics: Statistics,
|
||||||
registry: PrometheusMeterRegistry,
|
registry: PrometheusMeterRegistry,
|
||||||
|
@ -75,7 +77,7 @@ fun getServer(
|
||||||
}
|
}
|
||||||
|
|
||||||
circuitBreaker.eventPublisher.onReset {
|
circuitBreaker.eventPublisher.onReset {
|
||||||
LOGGER.warn { "Circuit breaker has rest" }
|
LOGGER.warn { "Circuit breaker has reset" }
|
||||||
}
|
}
|
||||||
|
|
||||||
circuitBreaker.eventPublisher.onStateTransition {
|
circuitBreaker.eventPublisher.onStateTransition {
|
||||||
|
@ -109,7 +111,7 @@ fun getServer(
|
||||||
)
|
)
|
||||||
|
|
||||||
return timeRequest()
|
return timeRequest()
|
||||||
.then(addCommonHeaders())
|
.then(addCommonHeaders(devSettings.sendServerHeader))
|
||||||
.then(catchAllHideDetails())
|
.then(catchAllHideDetails())
|
||||||
.then(
|
.then(
|
||||||
routes(
|
routes(
|
||||||
|
@ -142,7 +144,7 @@ fun getServer(
|
||||||
GeoIpMetricsFilterBuilder(metricsSettings.enableGeoip, metricsSettings.geoipLicenseKey, registry).build()
|
GeoIpMetricsFilterBuilder(metricsSettings.enableGeoip, metricsSettings.geoipLicenseKey, registry).build()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.asServer(Netty(remoteSettings.tls!!, serverSettings, statistics))
|
.asServer(Netty(remoteSettings, serverSettings, devSettings, statistics))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
||||||
|
|
|
@ -35,12 +35,17 @@ import java.util.*
|
||||||
private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH)
|
private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH)
|
||||||
private val LOGGER = LoggerFactory.getLogger("Application")
|
private val LOGGER = LoggerFactory.getLogger("Application")
|
||||||
|
|
||||||
fun addCommonHeaders(): Filter {
|
fun addCommonHeaders(sendServerHeader: Boolean): Filter {
|
||||||
return Filter { next: HttpHandler ->
|
return Filter { next: HttpHandler ->
|
||||||
{ request: Request ->
|
{ request: Request ->
|
||||||
val response = next(request)
|
val response = next(request)
|
||||||
response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC)))
|
response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC))).let {
|
||||||
.header("Server", "MangaDex@Home Node ${BuildInfo.VERSION} (${Constants.CLIENT_BUILD})")
|
if (sendServerHeader) {
|
||||||
|
it.header("Server", "MangaDex@Home Node ${BuildInfo.VERSION} (${Constants.CLIENT_BUILD})")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ package mdnet.server
|
||||||
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
fun md5Bytes(stringToHash: String) =
|
fun md5Bytes(stringToHash: String): ByteArray =
|
||||||
MessageDigest.getInstance("MD5")
|
MessageDigest.getInstance("MD5")
|
||||||
.digest(stringToHash.toByteArray())
|
.digest(stringToHash.toByteArray())
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,9 @@ data class ServerSettings(
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||||
data class DevSettings(
|
data class DevSettings(
|
||||||
val devUrl: String? = null
|
val devUrl: String? = null,
|
||||||
|
val disableSniCheck: Boolean = false,
|
||||||
|
val sendServerHeader: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.http4k.core.Response
|
||||||
import org.http4k.core.Status
|
import org.http4k.core.Status
|
||||||
import org.http4k.kotest.shouldHaveHeader
|
import org.http4k.kotest.shouldHaveHeader
|
||||||
import org.http4k.kotest.shouldHaveStatus
|
import org.http4k.kotest.shouldHaveStatus
|
||||||
|
import org.http4k.kotest.shouldNotHaveHeader
|
||||||
import org.http4k.routing.bind
|
import org.http4k.routing.bind
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import org.ktorm.database.Database
|
import org.ktorm.database.Database
|
||||||
|
@ -106,6 +107,12 @@ class ImageServerTest : FreeSpec() {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"should not have Server header" {
|
||||||
|
val response = handler(Request(Method.GET, "/data/02181a8f5fe8cd408720a771dd129fd8/T2.png"))
|
||||||
|
response.shouldNotHaveHeader("Server")
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"with real cache" - {
|
"with real cache" - {
|
||||||
|
|
Loading…
Reference in a new issue