added server timeouts
All checks were successful
CI / build (push) Successful in 3m18s

This commit is contained in:
2025-01-20 15:45:13 +08:00
parent 702556bfbb
commit 3d1847c408
14 changed files with 399 additions and 218 deletions

View File

@@ -14,6 +14,3 @@ WORKDIR /home/luser/plugins
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-server-memcached*.tar RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-server-memcached*.tar
WORKDIR /home/luser WORKDIR /home/luser
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
FROM release-memcached as compose
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml

View File

@@ -2,10 +2,12 @@ package net.woggioni.gbcs.api;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value; import lombok.Value;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -14,15 +16,32 @@ import java.util.stream.Collectors;
public class Configuration { public class Configuration {
String host; String host;
int port; int port;
int incomingConnectionsBacklogSize;
String serverPath; String serverPath;
@NonNull
EventExecutor eventExecutor;
@NonNull
Connection connection;
Map<String, User> users; Map<String, User> users;
Map<String, Group> groups; Map<String, Group> groups;
Cache cache; Cache cache;
Authentication authentication; Authentication authentication;
Tls tls; Tls tls;
boolean useVirtualThread;
int maxRequestSize; @Value
int incomingConnectionsBacklogSize; public static class EventExecutor {
boolean useVirtualThreads;
}
@Value
public static class Connection {
Duration readTimeout;
Duration writeTimeout;
Duration idleTimeout;
Duration readIdleTimeout;
Duration writeIdleTimeout;
int maxRequestSize;
}
@Value @Value
public static class Group { public static class Group {
@@ -103,28 +122,28 @@ public class Configuration {
public static Configuration of( public static Configuration of(
String host, String host,
int port, int port,
int incomingConnectionsBacklogSize,
String serverPath, String serverPath,
EventExecutor eventExecutor,
Connection connection,
Map<String, User> users, Map<String, User> users,
Map<String, Group> groups, Map<String, Group> groups,
Cache cache, Cache cache,
Authentication authentication, Authentication authentication,
Tls tls, Tls tls
boolean useVirtualThread,
int maxRequestSize,
int incomingConnectionsBacklogSize
) { ) {
return new Configuration( return new Configuration(
host, host,
port, port,
incomingConnectionsBacklogSize,
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null, serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
eventExecutor,
connection,
users, users,
groups, groups,
cache, cache,
authentication, authentication,
tls, tls
useVirtualThread,
maxRequestSize,
incomingConnectionsBacklogSize
); );
} }
} }

View File

@@ -14,6 +14,7 @@
<xs:complexType name="profileType"> <xs:complexType name="profileType">
<xs:choice> <xs:choice>
<xs:element name="no-auth" type="gbcs-client:noAuthType"/>
<xs:element name="basic-auth" type="gbcs-client:basicAuthType"/> <xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/> <xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
</xs:choice> </xs:choice>
@@ -22,6 +23,8 @@
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/> <xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="noAuthType"/>
<xs:complexType name="basicAuthType"> <xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/> <xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/> <xs:attribute name="password" type="xs:string" use="required"/>

View File

@@ -9,41 +9,37 @@ import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPromise import io.netty.channel.ChannelPromise
import io.netty.channel.DefaultFileRegion
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.DecoderException import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.compression.CompressionOptions import io.netty.handler.codec.compression.CompressionOptions
import io.netty.handler.codec.http.DefaultFullHttpResponse import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpContentCompressor import io.netty.handler.codec.http.HttpContentCompressor
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpObjectAggregator import io.netty.handler.codec.http.HttpObjectAggregator
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpServerCodec import io.netty.handler.codec.http.HttpServerCodec
import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.ssl.ClientAuth import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SslContext import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SslHandler import io.netty.handler.ssl.SslHandler
import io.netty.handler.stream.ChunkedNioFile
import io.netty.handler.stream.ChunkedNioStream
import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.handler.timeout.IdleStateEvent
import io.netty.handler.timeout.IdleStateHandler
import io.netty.handler.timeout.ReadTimeoutException
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutException
import io.netty.handler.timeout.WriteTimeoutHandler
import io.netty.util.concurrent.DefaultEventExecutorGroup import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.api.exception.ConfigurationException import net.woggioni.gbcs.api.exception.ConfigurationException
@@ -53,6 +49,7 @@ import net.woggioni.gbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.gbcs.common.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.gbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.common.info import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.server.auth.AbstractNettyHttpAuthenticator import net.woggioni.gbcs.server.auth.AbstractNettyHttpAuthenticator
import net.woggioni.gbcs.server.auth.Authorizer import net.woggioni.gbcs.server.auth.Authorizer
@@ -60,11 +57,11 @@ import net.woggioni.gbcs.server.auth.ClientCertificateValidator
import net.woggioni.gbcs.server.auth.RoleAuthorizer import net.woggioni.gbcs.server.auth.RoleAuthorizer
import net.woggioni.gbcs.server.configuration.Parser import net.woggioni.gbcs.server.configuration.Parser
import net.woggioni.gbcs.server.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.server.handler.ServerHandler
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2 import net.woggioni.jwo.Tuple2
import java.io.OutputStream import java.io.OutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.channels.FileChannel
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyStore import java.security.KeyStore
@@ -72,6 +69,7 @@ import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.Arrays import java.util.Arrays
import java.util.Base64 import java.util.Base64
import java.util.concurrent.TimeUnit
import java.util.regex.Matcher import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.naming.ldap.LdapName import javax.naming.ldap.LdapName
@@ -200,28 +198,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
private val eventExecutorGroup: EventExecutorGroup private val eventExecutorGroup: EventExecutorGroup
) : ChannelInitializer<Channel>() { ) : ChannelInitializer<Channel>() {
private val serverHandler = let {
val cacheImplementation = cfg.cache.materialize()
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
ServerHandler(cacheImplementation, prefix)
}
private val exceptionHandler = ExceptionHandler()
private val authenticator = when (val auth = cfg.authentication) {
is Configuration.BasicAuthentication -> NettyHttpBasicAuthenticator(cfg.users, RoleAuthorizer())
is Configuration.ClientCertificateAuthentication -> {
ClientCertificateAuthenticator(
RoleAuthorizer(),
cfg.users[""]?.roles,
userExtractor(auth),
groupExtractor(auth)
)
}
else -> null
}
companion object { companion object {
private fun createSslCtx(tls: Configuration.Tls): SslContext { private fun createSslCtx(tls: Configuration.Tls): SslContext {
val keyStore = tls.keyStore val keyStore = tls.keyStore
@@ -272,6 +248,30 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
} }
} }
private val log = contextLogger()
private val serverHandler = let {
val cacheImplementation = cfg.cache.materialize()
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
ServerHandler(cacheImplementation, prefix)
}
private val exceptionHandler = ExceptionHandler()
private val authenticator = when (val auth = cfg.authentication) {
is Configuration.BasicAuthentication -> NettyHttpBasicAuthenticator(cfg.users, RoleAuthorizer())
is Configuration.ClientCertificateAuthentication -> {
ClientCertificateAuthenticator(
RoleAuthorizer(),
cfg.users[""]?.roles,
userExtractor(auth),
groupExtractor(auth)
)
}
else -> null
}
private val sslContext: SslContext? = cfg.tls?.let(Companion::createSslCtx) private val sslContext: SslContext? = cfg.tls?.let(Companion::createSslCtx)
private fun userExtractor(authentication: Configuration.ClientCertificateAuthentication) = private fun userExtractor(authentication: Configuration.ClientCertificateAuthentication) =
@@ -303,14 +303,37 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
} }
override fun initChannel(ch: Channel) { override fun initChannel(ch: Channel) {
log.debug {
"Created connection ${ch.id().asShortText()}"
}
ch.closeFuture().addListener {
log.debug {
"Closed connection ${ch.id().asShortText()}"
}
}
val pipeline = ch.pipeline() val pipeline = ch.pipeline()
cfg.connection.apply {
pipeline.addLast(ReadTimeoutHandler(readTimeout.toMillis(), TimeUnit.MILLISECONDS))
pipeline.addLast(WriteTimeoutHandler(writeTimeout.toMillis(), TimeUnit.MILLISECONDS))
pipeline.addLast(IdleStateHandler(false, 0, 0, idleTimeout.toMillis(), TimeUnit.MILLISECONDS))
}
pipeline.addLast(object : ChannelInboundHandlerAdapter() {
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is IdleStateEvent) {
log.debug {
"Idle timeout reached on channel ${ch.id().asShortText()}, closing the connection"
}
ctx.close()
}
}
})
sslContext?.newHandler(ch.alloc())?.also { sslContext?.newHandler(ch.alloc())?.also {
pipeline.addLast(SSL_HANDLER_NAME, it) pipeline.addLast(SSL_HANDLER_NAME, it)
} }
pipeline.addLast(HttpServerCodec()) pipeline.addLast(HttpServerCodec())
pipeline.addLast(HttpChunkContentCompressor(1024)) pipeline.addLast(HttpChunkContentCompressor(1024))
pipeline.addLast(ChunkedWriteHandler()) pipeline.addLast(ChunkedWriteHandler())
pipeline.addLast(HttpObjectAggregator(cfg.maxRequestSize)) pipeline.addLast(HttpObjectAggregator(cfg.connection.maxRequestSize))
authenticator?.let { authenticator?.let {
pipeline.addLast(it) pipeline.addLast(it)
} }
@@ -351,7 +374,20 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
ctx.writeAndFlush(TOO_BIG.retainedDuplicate()) ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE) .addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
} }
is ReadTimeoutException -> {
log.debug {
val channelId = ctx.channel().id().asShortText()
"Read timeout on channel $channelId, closing the connection"
}
ctx.close()
}
is WriteTimeoutException -> {
log.debug {
val channelId = ctx.channel().id().asShortText()
"Write timeout on channel $channelId, closing the connection"
}
ctx.close()
}
else -> { else -> {
log.error(cause.message, cause) log.error(cause.message, cause)
ctx.close() ctx.close()
@@ -360,110 +396,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
} }
} }
@Sharable
private class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
SimpleChannelInboundHandler<FullHttpRequest>() {
private val log = contextLogger()
override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method()
if (method === HttpMethod.GET) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
cache.get(key)?.let { channel ->
log.debug(ctx) {
"Cache hit for key '$key'"
}
val response = DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = HttpHeaderValues.APPLICATION_OCTET_STREAM
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.IDENTITY)
} else {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
}
ctx.write(response)
when (channel) {
is FileChannel -> {
if (keepAlive) {
ctx.write(ChunkedNioFile(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
} else {
ctx.writeAndFlush(DefaultFileRegion(channel, 0, channel.size()))
.addListener(ChannelFutureListener.CLOSE)
}
}
else -> {
ctx.write(ChunkedNioStream(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
}
}
} ?: let {
log.debug(ctx) {
"Cache miss for key '$key'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.NOT_FOUND)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
} else if (method === HttpMethod.PUT) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
log.debug(ctx) {
"Added value for key '$key' to build cache"
}
val bodyBytes = msg.content().run {
if (isDirect) {
ByteArray(readableBytes()).also {
readBytes(it)
}
} else {
array()
}
}
cache.put(key, bodyBytes)
val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray())
)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
ctx.writeAndFlush(response)
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
} else {
log.warn(ctx) {
"Got request with unhandled method '${msg.method().name()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
}
}
class ServerHandle( class ServerHandle(
httpChannelFuture: ChannelFuture, httpChannelFuture: ChannelFuture,
private val executorGroups: Iterable<EventExecutorGroup> private val executorGroups: Iterable<EventExecutorGroup>
@@ -496,7 +428,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val serverSocketChannel = NioServerSocketChannel::class.java val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = bossGroup val workerGroup = bossGroup
val eventExecutorGroup = run { val eventExecutorGroup = run {
val threadFactory = if (cfg.isUseVirtualThread) { val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) {
Thread.ofVirtual().factory() Thread.ofVirtual().factory()
} else { } else {
null null

View File

@@ -19,11 +19,16 @@ import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.TypeInfo import org.w3c.dom.TypeInfo
import java.nio.file.Paths import java.nio.file.Paths
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
object Parser { object Parser {
fun parse(document: Document): Configuration { fun parse(document: Document): Configuration {
val root = document.documentElement val root = document.documentElement
val anonymousUser = User("", null, emptySet()) val anonymousUser = User("", null, emptySet())
var connection: Configuration.Connection? = null
var eventExecutor: Configuration.EventExecutor? = null
var cache: Cache? = null var cache: Cache? = null
var host = "127.0.0.1" var host = "127.0.0.1"
var port = 11080 var port = 11080
@@ -31,46 +36,11 @@ object Parser {
var groups = emptyMap<String, Group>() var groups = emptyMap<String, Group>()
var tls: Tls? = null var tls: Tls? = null
val serverPath = root.renderAttribute("path") val serverPath = root.renderAttribute("path")
val useVirtualThread = root.renderAttribute("use-virtual-threads") var incomingConnectionsBacklogSize = 1024
?.let(String::toBoolean) ?: true
val maxRequestSize = root.renderAttribute("max-request-size")
?.let(String::toInt) ?: 67108864
val incomingConnectionsBacklogSize = root.renderAttribute("incoming-connections-backlog-size")
?.let(String::toInt) ?: 1024
var authentication: Authentication? = null var authentication: Authentication? = null
for (child in root.asIterable()) { for (child in root.asIterable()) {
val tagName = child.localName val tagName = child.localName
when (tagName) { when (tagName) {
"authorization" -> {
var knownUsers = sequenceOf(anonymousUser)
for (gchild in child.asIterable()) {
when (gchild.localName) {
"users" -> {
knownUsers += parseUsers(gchild)
}
"groups" -> {
val pair = parseGroups(gchild, knownUsers)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
port = Integer.parseInt(child.renderAttribute("port"))
}
"cache" -> {
cache = (child as TypeInfo).let { tf ->
val typeNamespace = tf.typeNamespace
val typeName = tf.typeName
CacheSerializers.index[typeNamespace to typeName]
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' not found")
}.deserialize(child)
}
"authentication" -> { "authentication" -> {
for (gchild in child.asIterable()) { for (gchild in child.asIterable()) {
when (gchild.localName) { when (gchild.localName) {
@@ -102,6 +72,66 @@ object Parser {
} }
} }
"authorization" -> {
var knownUsers = sequenceOf(anonymousUser)
for (gchild in child.asIterable()) {
when (gchild.localName) {
"users" -> {
knownUsers += parseUsers(gchild)
}
"groups" -> {
val pair = parseGroups(gchild, knownUsers)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
port = Integer.parseInt(child.renderAttribute("port"))
incomingConnectionsBacklogSize = child.renderAttribute("incoming-connections-backlog-size")
?.let(Integer::parseInt)
?: 1024
}
"cache" -> {
cache = (child as TypeInfo).let { tf ->
val typeNamespace = tf.typeNamespace
val typeName = tf.typeName
CacheSerializers.index[typeNamespace to typeName]
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' not found")
}.deserialize(child)
}
"connection" -> {
val writeTimeout = child.renderAttribute("write-timeout")
?.let(Duration::parse) ?: Duration.of(10, ChronoUnit.SECONDS)
val readTimeout = child.renderAttribute("read-timeout")
?.let(Duration::parse) ?: Duration.of(10, ChronoUnit.SECONDS)
val idleTimeout = child.renderAttribute("idle-timeout")
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
val readIdleTimeout = child.renderAttribute("read-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val writeIdleTimeout = child.renderAttribute("write-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val maxRequestSize = child.renderAttribute("max-request-size")
?.let(String::toInt) ?: 67108864
connection = Configuration.Connection(
readTimeout,
writeTimeout,
idleTimeout,
readIdleTimeout,
writeIdleTimeout,
maxRequestSize
)
}
"event-executor" -> {
val useVirtualThread = root.renderAttribute("use-virtual-threads")
?.let(String::toBoolean) ?: true
eventExecutor = Configuration.EventExecutor(useVirtualThread)
}
"tls" -> { "tls" -> {
val verifyClients = child.renderAttribute("verify-clients") val verifyClients = child.renderAttribute("verify-clients")
?.let(String::toBoolean) ?: false ?.let(String::toBoolean) ?: false
@@ -140,18 +170,18 @@ object Parser {
} }
} }
} }
return Configuration( return Configuration.of(
host, host,
port, port,
incomingConnectionsBacklogSize,
serverPath, serverPath,
eventExecutor,
connection,
users, users,
groups, groups,
cache!!, cache!!,
authentication, authentication,
tls, tls,
useVirtualThread,
maxRequestSize,
incomingConnectionsBacklogSize
) )
} }

View File

@@ -14,10 +14,6 @@ object Serializer {
it.xmlNamespace to it.xmlSchemaLocation it.xmlNamespace to it.xmlSchemaLocation
}.toMap() }.toMap()
return Xml.of(GBCS.GBCS_NAMESPACE_URI, GBCS.GBCS_PREFIX + ":server") { return Xml.of(GBCS.GBCS_NAMESPACE_URI, GBCS.GBCS_PREFIX + ":server") {
attr("use-virtual-threads", conf.isUseVirtualThread.toString())
attr("max-request-size", conf.maxRequestSize.toString())
attr("incoming-connections-backlog-size", conf.incomingConnectionsBacklogSize.toString())
// attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI) // attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI)
val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ") val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ")
attr("xs:schemaLocation", value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI) attr("xs:schemaLocation", value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI)
@@ -30,6 +26,20 @@ object Serializer {
node("bind") { node("bind") {
attr("host", conf.host) attr("host", conf.host)
attr("port", conf.port.toString()) attr("port", conf.port.toString())
attr("incoming-connections-backlog-size", conf.incomingConnectionsBacklogSize.toString())
}
node("connection") {
conf.connection.let { connection ->
attr("read-timeout", connection.readTimeout.toString())
attr("write-timeout", connection.writeTimeout.toString())
attr("idle-timeout", connection.idleTimeout.toString())
attr("read-idle-timeout", connection.readIdleTimeout.toString())
attr("write-idle-timeout", connection.writeIdleTimeout.toString())
attr("max-request-size", connection.maxRequestSize.toString())
}
}
node("event-executor") {
attr("use-virtual-threads", conf.eventExecutor.isUseVirtualThreads.toString())
} }
val cache = conf.cache val cache = conf.cache
val serializer : CacheProvider<Configuration.Cache> = val serializer : CacheProvider<Configuration.Cache> =

View File

@@ -0,0 +1,129 @@
package net.woggioni.gbcs.server.handler
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.DefaultFileRegion
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.stream.ChunkedNioFile
import io.netty.handler.stream.ChunkedNioStream
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.server.debug
import net.woggioni.gbcs.server.warn
import java.nio.channels.FileChannel
import java.nio.file.Path
@ChannelHandler.Sharable
class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
SimpleChannelInboundHandler<FullHttpRequest>() {
private val log = contextLogger()
override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method()
if (method === HttpMethod.GET) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
cache.get(key)?.let { channel ->
log.debug(ctx) {
"Cache hit for key '$key'"
}
val response = DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = HttpHeaderValues.APPLICATION_OCTET_STREAM
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.IDENTITY)
} else {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
}
ctx.write(response)
when (channel) {
is FileChannel -> {
if (keepAlive) {
ctx.write(ChunkedNioFile(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
} else {
ctx.writeAndFlush(DefaultFileRegion(channel, 0, channel.size()))
.addListener(ChannelFutureListener.CLOSE)
}
}
else -> {
ctx.write(ChunkedNioStream(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
}
}
} ?: let {
log.debug(ctx) {
"Cache miss for key '$key'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.NOT_FOUND)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
} else if (method === HttpMethod.PUT) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
log.debug(ctx) {
"Added value for key '$key' to build cache"
}
val bodyBytes = msg.content().run {
if (isDirect) {
ByteArray(readableBytes()).also {
readBytes(it)
}
} else {
array()
}
}
cache.put(key, bodyBytes)
val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray())
)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
ctx.writeAndFlush(response)
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
} else {
log.warn(ctx) {
"Got request with unhandled method '${msg.method().name()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
}
}

View File

@@ -8,6 +8,8 @@
<xs:complexType name="serverType"> <xs:complexType name="serverType">
<xs:sequence minOccurs="0"> <xs:sequence minOccurs="0">
<xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/> <xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="gbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="gbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="gbcs:cacheType" maxOccurs="1"/> <xs:element name="cache" type="gbcs:cacheType" maxOccurs="1"/>
<xs:element name="authorization" type="gbcs:authorizationType" minOccurs="0"> <xs:element name="authorization" type="gbcs:authorizationType" minOccurs="0">
<xs:key name="userId"> <xs:key name="userId">
@@ -23,14 +25,25 @@
<xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/> <xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="path" type="xs:string" use="optional"/> <xs:attribute name="path" type="xs:string" use="optional"/>
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true"/>
<xs:attribute name="max-request-size" type="xs:unsignedInt" use="optional" default="67108864"/>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="bindType"> <xs:complexType name="bindType">
<xs:attribute name="host" type="xs:token" use="required"/> <xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/> <xs:attribute name="port" type="xs:unsignedShort" use="required"/>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024"/>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT10S"/>
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT10S"/>
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="max-request-size" type="xs:unsignedInt" use="optional" default="67108864"/>
</xs:complexType>
<xs:complexType name="eventExecutorType">
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="cacheType" abstract="true"/> <xs:complexType name="cacheType" abstract="true"/>

View File

@@ -11,6 +11,7 @@ import java.net.http.HttpRequest
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.Base64 import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import kotlin.random.Random import kotlin.random.Random
@@ -30,10 +31,20 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
override fun setUp() { override fun setUp() {
this.cacheDir = testDir.resolve("cache") this.cacheDir = testDir.resolve("cache")
cfg = Configuration( cfg = Configuration.of(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), NetworkUtils.getFreePort(),
50,
serverPath, serverPath,
Configuration.EventExecutor(false),
Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
0x1000
),
users.asSequence().map { it.name to it}.toMap(), users.asSequence().map { it.name to it}.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
FileSystemCacheConfiguration(this.cacheDir, FileSystemCacheConfiguration(this.cacheDir,
@@ -44,9 +55,6 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
), ),
Configuration.BasicAuthentication(), Configuration.BasicAuthentication(),
null, null,
true,
0x10000,
100
) )
Xml.write(Serializer.serialize(cfg), System.out) Xml.write(Serializer.serialize(cfg), System.out)
} }

View File

@@ -18,6 +18,7 @@ import java.nio.file.Path
import java.security.KeyStore import java.security.KeyStore
import java.security.KeyStore.PasswordProtection import java.security.KeyStore.PasswordProtection
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.Base64 import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import javax.net.ssl.KeyManagerFactory import javax.net.ssl.KeyManagerFactory
@@ -138,7 +139,17 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), NetworkUtils.getFreePort(),
100,
serverPath, serverPath,
Configuration.EventExecutor(false),
Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
0x1000
),
users.asSequence().map { it.name to it }.toMap(), users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
FileSystemCacheConfiguration(this.cacheDir, FileSystemCacheConfiguration(this.cacheDir,
@@ -156,9 +167,6 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
Configuration.TrustStore(this.trustStoreFile, null, false), Configuration.TrustStore(this.trustStoreFile, null, false),
true true
), ),
false,
0x10000,
100
) )
Xml.write(Serializer.serialize(cfg), System.out) Xml.write(Serializer.serialize(cfg), System.out)
} }

View File

@@ -15,6 +15,7 @@ import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.Base64 import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import kotlin.random.Random import kotlin.random.Random
@@ -33,7 +34,17 @@ class NoAuthServerTest : AbstractServerTest() {
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), NetworkUtils.getFreePort(),
100,
serverPath, serverPath,
Configuration.EventExecutor(false),
Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS),
0x1000
),
emptyMap(), emptyMap(),
emptyMap(), emptyMap(),
FileSystemCacheConfiguration( FileSystemCacheConfiguration(
@@ -45,9 +56,6 @@ class NoAuthServerTest : AbstractServerTest() {
), ),
null, null,
null, null,
true,
0x10000,
100
) )
Xml.write(Serializer.serialize(cfg), System.out) Xml.write(Serializer.serialize(cfg), System.out)
} }

View File

@@ -1,8 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server use-virtual-threads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="22"/>
<connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M"
write-idle-timeout="PT11M"
idle-timeout="PT30M"
max-request-size="101325"/>
<event-executor use-virtual-threads="false"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authentication> <authentication>
<none/> <none/>

View File

@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server use-virtual-threads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs.server.memcached" xmlns:gbcs-memcached="urn:net.woggioni.gbcs.server.memcached"
xs:schemaLocation="urn:net.woggioni.gbcs.server.memcached jpms://net.woggioni.gbcs.server.memcached/net/woggioni/gbcs/server/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server.memcached jpms://net.woggioni.gbcs.server.memcached/net/woggioni/gbcs/server/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443" /> <bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="50"/>
<connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M"
write-idle-timeout="PT11M"
idle-timeout="PT30M"
max-request-size="101325"/>
<event-executor use-virtual-threads="false"/>
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="101325" digest="SHA-256"> <cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="101325" digest="SHA-256">
<server host="127.0.0.1" port="11211"/> <server host="127.0.0.1" port="11211"/>
</cache> </cache>

View File

@@ -1,8 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server use-virtual-threads="false" max-request-size="4096" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="180"/>
<connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M"
write-idle-timeout="PT11M"
idle-timeout="PT30M"
max-request-size="4096"/>
<event-executor use-virtual-threads="false"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>
<users> <users>