This commit is contained in:
@@ -4,6 +4,7 @@ import net.woggioni.gbcs.api.exception.ContentTooLargeException;
|
|||||||
|
|
||||||
import java.nio.channels.ReadableByteChannel;
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
|
||||||
|
|
||||||
public interface Cache extends AutoCloseable {
|
public interface Cache extends AutoCloseable {
|
||||||
ReadableByteChannel get(String key);
|
ReadableByteChannel get(String key);
|
||||||
|
|
||||||
|
@@ -43,11 +43,20 @@ public class Configuration {
|
|||||||
int maxRequestSize;
|
int maxRequestSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class Quota {
|
||||||
|
long calls;
|
||||||
|
Duration period;
|
||||||
|
long initialAvailableCalls;
|
||||||
|
long maxAvailableCalls;
|
||||||
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class Group {
|
public static class Group {
|
||||||
@EqualsAndHashCode.Include
|
@EqualsAndHashCode.Include
|
||||||
String name;
|
String name;
|
||||||
Set<Role> roles;
|
Set<Role> roles;
|
||||||
|
Quota quota;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@@ -56,7 +65,7 @@ public class Configuration {
|
|||||||
String name;
|
String name;
|
||||||
String password;
|
String password;
|
||||||
Set<Group> groups;
|
Set<Group> groups;
|
||||||
|
Quota quota;
|
||||||
|
|
||||||
public Set<Role> getRoles() {
|
public Set<Role> getRoles() {
|
||||||
return groups.stream()
|
return groups.stream()
|
||||||
@@ -75,6 +84,13 @@ public class Configuration {
|
|||||||
Group extract(X509Certificate cert);
|
Group extract(X509Certificate cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class Throttling {
|
||||||
|
KeyStore keyStore;
|
||||||
|
TrustStore trustStore;
|
||||||
|
boolean verifyClients;
|
||||||
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class Tls {
|
public static class Tls {
|
||||||
KeyStore keyStore;
|
KeyStore keyStore;
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
package net.woggioni.gbcs.api.exception;
|
||||||
|
|
||||||
|
public class CacheException extends GbcsException {
|
||||||
|
public CacheException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CacheException(String message) {
|
||||||
|
this(message, null);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,6 +16,8 @@ import net.woggioni.gradle.graalvm.NativeImageTask
|
|||||||
import net.woggioni.gradle.graalvm.JlinkPlugin
|
import net.woggioni.gradle.graalvm.JlinkPlugin
|
||||||
import net.woggioni.gradle.graalvm.JlinkTask
|
import net.woggioni.gradle.graalvm.JlinkTask
|
||||||
|
|
||||||
|
Property<String> mainModuleName = objects.property(String.class)
|
||||||
|
mainModuleName.set('net.woggioni.gbcs.cli')
|
||||||
Property<String> mainClassName = objects.property(String.class)
|
Property<String> mainClassName = objects.property(String.class)
|
||||||
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
|
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
envelopeJar {
|
envelopeJar {
|
||||||
mainModule = 'net.woggioni.gbcs.cli'
|
mainModule = mainModuleName
|
||||||
mainClass = mainClassName
|
mainClass = mainClassName
|
||||||
|
|
||||||
extraClasspath = ["plugins"]
|
extraClasspath = ["plugins"]
|
||||||
@@ -50,20 +52,31 @@ dependencies {
|
|||||||
|
|
||||||
// runtimeOnly catalog.slf4j.jdk14
|
// runtimeOnly catalog.slf4j.jdk14
|
||||||
runtimeOnly catalog.logback.classic
|
runtimeOnly catalog.logback.classic
|
||||||
|
// runtimeOnly catalog.slf4j.simple
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
|
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
|
||||||
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
|
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
|
||||||
// systemProperties['log.config.source'] = 'logging.properties'
|
// systemProperties['log.config.source'] = 'net/woggioni/gbcs/cli/logging.properties'
|
||||||
|
// systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/gbcs/cli/logging.properties'
|
||||||
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
|
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
|
||||||
|
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
|
||||||
|
|
||||||
|
// systemProperties['org.slf4j.simpleLogger.showDateTime'] = 'true'
|
||||||
|
// systemProperties['org.slf4j.simpleLogger.defaultLogLevel'] = 'debug'
|
||||||
|
// systemProperties['org.slf4j.simpleLogger.log.com.google.code.yanf4j'] = 'warn'
|
||||||
|
// systemProperties['org.slf4j.simpleLogger.log.net.rubyeye.xmemcached'] = 'warn'
|
||||||
|
// systemProperties['org.slf4j.simpleLogger.dateTimeFormat'] = 'yyyy-MM-dd\'T\'HH:mm:ss.SSSZ'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
||||||
mainClass = mainClassName
|
mainClass = mainClassName
|
||||||
|
mainModule = mainModuleName
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
|
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
|
||||||
mainClass = mainClassName
|
mainClass = mainClassName
|
||||||
|
mainModule = mainModuleName
|
||||||
useMusl = true
|
useMusl = true
|
||||||
buildStaticImage = true
|
buildStaticImage = true
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
Args=-H:Optimize=3 --gc=serial
|
Args=-H:Optimize=3 --gc=serial --initialize-at-run-time=io.netty
|
||||||
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
|
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
|
@@ -13,6 +13,7 @@ import net.woggioni.gbcs.cli.impl.commands.ServerCommand
|
|||||||
import net.woggioni.jwo.Application
|
import net.woggioni.jwo.Application
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.Model.CommandSpec
|
import picocli.CommandLine.Model.CommandSpec
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
|
@@ -5,6 +5,7 @@ import net.woggioni.gbcs.common.error
|
|||||||
import net.woggioni.gbcs.common.info
|
import net.woggioni.gbcs.common.info
|
||||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||||
import net.woggioni.gbcs.client.GradleBuildCacheClient
|
import net.woggioni.gbcs.client.GradleBuildCacheClient
|
||||||
|
import net.woggioni.jwo.JWO
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -45,7 +46,7 @@ class BenchmarkCommand : GbcsCommand() {
|
|||||||
val entryGenerator = sequence {
|
val entryGenerator = sequence {
|
||||||
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
||||||
while (true) {
|
while (true) {
|
||||||
val key = Base64.getUrlEncoder().encode(random.nextBytes(16)).toString(Charsets.UTF_8)
|
val key = JWO.bytesToHex(random.nextBytes(16))
|
||||||
val content = random.nextInt().toByte()
|
val content = random.nextInt().toByte()
|
||||||
val value = ByteArray(0x1000, { _ -> content })
|
val value = ByteArray(0x1000, { _ -> content })
|
||||||
yield(key to value)
|
yield(key to value)
|
||||||
|
@@ -184,7 +184,7 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
fun put(key: String, content: ByteArray): CompletableFuture<Unit> {
|
fun put(key: String, content: ByteArray): CompletableFuture<Unit> {
|
||||||
return sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content).thenApply {
|
return sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content).thenApply {
|
||||||
val status = it.status()
|
val status = it.status()
|
||||||
if (it.status() != HttpResponseStatus.CREATED) {
|
if (it.status() != HttpResponseStatus.CREATED && it.status() != HttpResponseStatus.OK) {
|
||||||
throw HttpException(status)
|
throw HttpException(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,6 @@ module net.woggioni.gbcs.common {
|
|||||||
requires kotlin.stdlib;
|
requires kotlin.stdlib;
|
||||||
requires net.woggioni.jwo;
|
requires net.woggioni.jwo;
|
||||||
|
|
||||||
|
provides java.net.spi.URLStreamHandlerProvider with net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory;
|
||||||
exports net.woggioni.gbcs.common;
|
exports net.woggioni.gbcs.common;
|
||||||
}
|
}
|
@@ -6,12 +6,13 @@ import java.net.URL
|
|||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import java.net.URLStreamHandler
|
import java.net.URLStreamHandler
|
||||||
import java.net.URLStreamHandlerFactory
|
import java.net.URLStreamHandlerFactory
|
||||||
|
import java.net.spi.URLStreamHandlerProvider
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
|
||||||
class GbcsUrlStreamHandlerFactory : URLStreamHandlerFactory {
|
class GbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
|
||||||
|
|
||||||
private class ClasspathHandler(private val classLoader: ClassLoader = GbcsUrlStreamHandlerFactory::class.java.classLoader) :
|
private class ClasspathHandler(private val classLoader: ClassLoader = GbcsUrlStreamHandlerFactory::class.java.classLoader) :
|
||||||
URLStreamHandler() {
|
URLStreamHandler() {
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
|
@@ -25,6 +25,11 @@ dependencies {
|
|||||||
testRuntimeOnly project(":gbcs-server-memcached")
|
testRuntimeOnly project(":gbcs-server-memcached")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
systemProperty("io.netty.leakDetectionLevel", "PARANOID")
|
||||||
|
systemProperty("jdk.httpclient.redirects.retrylimit", "1")
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
maven(MavenPublication) {
|
maven(MavenPublication) {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import net.woggioni.gbcs.api.CacheProvider;
|
import net.woggioni.gbcs.api.CacheProvider;
|
||||||
import net.woggioni.gbcs.server.cache.FileSystemCacheProvider;
|
import net.woggioni.gbcs.server.cache.FileSystemCacheProvider;
|
||||||
|
import net.woggioni.gbcs.server.cache.InMemoryCacheProvider;
|
||||||
|
|
||||||
module net.woggioni.gbcs.server {
|
module net.woggioni.gbcs.server {
|
||||||
requires java.sql;
|
requires java.sql;
|
||||||
@@ -24,5 +25,5 @@ module net.woggioni.gbcs.server {
|
|||||||
opens net.woggioni.gbcs.server.schema;
|
opens net.woggioni.gbcs.server.schema;
|
||||||
|
|
||||||
uses CacheProvider;
|
uses CacheProvider;
|
||||||
provides CacheProvider with FileSystemCacheProvider;
|
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;
|
||||||
}
|
}
|
@@ -2,11 +2,8 @@ package net.woggioni.gbcs.server
|
|||||||
|
|
||||||
import io.netty.bootstrap.ServerBootstrap
|
import io.netty.bootstrap.ServerBootstrap
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.buffer.Unpooled
|
|
||||||
import io.netty.channel.Channel
|
import io.netty.channel.Channel
|
||||||
import io.netty.channel.ChannelDuplexHandler
|
|
||||||
import io.netty.channel.ChannelFuture
|
import io.netty.channel.ChannelFuture
|
||||||
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.ChannelInboundHandlerAdapter
|
||||||
@@ -15,18 +12,13 @@ import io.netty.channel.ChannelOption
|
|||||||
import io.netty.channel.ChannelPromise
|
import io.netty.channel.ChannelPromise
|
||||||
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.compression.CompressionOptions
|
import io.netty.handler.codec.compression.CompressionOptions
|
||||||
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.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.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.HttpServerCodec
|
import io.netty.handler.codec.http.HttpServerCodec
|
||||||
import io.netty.handler.codec.http.HttpVersion
|
|
||||||
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
|
||||||
@@ -34,16 +26,13 @@ import io.netty.handler.ssl.SslHandler
|
|||||||
import io.netty.handler.stream.ChunkedWriteHandler
|
import io.netty.handler.stream.ChunkedWriteHandler
|
||||||
import io.netty.handler.timeout.IdleStateEvent
|
import io.netty.handler.timeout.IdleStateEvent
|
||||||
import io.netty.handler.timeout.IdleStateHandler
|
import io.netty.handler.timeout.IdleStateHandler
|
||||||
import io.netty.handler.timeout.ReadTimeoutException
|
|
||||||
import io.netty.handler.timeout.ReadTimeoutHandler
|
import io.netty.handler.timeout.ReadTimeoutHandler
|
||||||
import io.netty.handler.timeout.WriteTimeoutException
|
|
||||||
import io.netty.handler.timeout.WriteTimeoutHandler
|
import io.netty.handler.timeout.WriteTimeoutHandler
|
||||||
|
import io.netty.util.AttributeKey
|
||||||
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.Configuration
|
import net.woggioni.gbcs.api.Configuration
|
||||||
import net.woggioni.gbcs.api.Role
|
|
||||||
import net.woggioni.gbcs.api.exception.ConfigurationException
|
import net.woggioni.gbcs.api.exception.ConfigurationException
|
||||||
import net.woggioni.gbcs.api.exception.ContentTooLargeException
|
|
||||||
import net.woggioni.gbcs.common.GBCS.toUrl
|
import net.woggioni.gbcs.common.GBCS.toUrl
|
||||||
import net.woggioni.gbcs.common.PasswordSecurity.decodePasswordHash
|
import net.woggioni.gbcs.common.PasswordSecurity.decodePasswordHash
|
||||||
import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
|
import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
|
||||||
@@ -57,7 +46,9 @@ 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.exception.ExceptionHandler
|
||||||
import net.woggioni.gbcs.server.handler.ServerHandler
|
import net.woggioni.gbcs.server.handler.ServerHandler
|
||||||
|
import net.woggioni.gbcs.server.throttling.ThrottlingHandler
|
||||||
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
|
||||||
@@ -75,12 +66,14 @@ import java.util.regex.Pattern
|
|||||||
import javax.naming.ldap.LdapName
|
import javax.naming.ldap.LdapName
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException
|
import javax.net.ssl.SSLPeerUnverifiedException
|
||||||
|
|
||||||
|
|
||||||
class GradleBuildCacheServer(private val cfg: Configuration) {
|
class GradleBuildCacheServer(private val cfg: Configuration) {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
|
||||||
|
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
|
||||||
|
|
||||||
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
|
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
|
||||||
private const val SSL_HANDLER_NAME = "sslHandler"
|
private const val SSL_HANDLER_NAME = "sslHandler"
|
||||||
|
|
||||||
@@ -120,12 +113,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
@Sharable
|
@Sharable
|
||||||
private class ClientCertificateAuthenticator(
|
private class ClientCertificateAuthenticator(
|
||||||
authorizer: Authorizer,
|
authorizer: Authorizer,
|
||||||
private val anonymousUserRoles: Set<Role>?,
|
private val anonymousUserGroups: Set<Configuration.Group>?,
|
||||||
private val userExtractor: Configuration.UserExtractor?,
|
private val userExtractor: Configuration.UserExtractor?,
|
||||||
private val groupExtractor: Configuration.GroupExtractor?,
|
private val groupExtractor: Configuration.GroupExtractor?,
|
||||||
) : AbstractNettyHttpAuthenticator(authorizer) {
|
) : AbstractNettyHttpAuthenticator(authorizer) {
|
||||||
|
|
||||||
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): Set<Role>? {
|
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): AuthenticationResult? {
|
||||||
return try {
|
return try {
|
||||||
val sslHandler = (ctx.pipeline().get(SSL_HANDLER_NAME) as? SslHandler)
|
val sslHandler = (ctx.pipeline().get(SSL_HANDLER_NAME) as? SslHandler)
|
||||||
?: throw ConfigurationException("Client certificate authentication cannot be used when TLS is disabled")
|
?: throw ConfigurationException("Client certificate authentication cannot be used when TLS is disabled")
|
||||||
@@ -136,10 +129,11 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
val clientCertificate = peerCertificates.first() as X509Certificate
|
val clientCertificate = peerCertificates.first() as X509Certificate
|
||||||
val user = userExtractor?.extract(clientCertificate)
|
val user = userExtractor?.extract(clientCertificate)
|
||||||
val group = groupExtractor?.extract(clientCertificate)
|
val group = groupExtractor?.extract(clientCertificate)
|
||||||
(group?.roles ?: emptySet()) + (user?.roles ?: emptySet())
|
val allGroups = ((user?.groups ?: emptySet()).asSequence() + sequenceOf(group).filterNotNull()).toSet()
|
||||||
} ?: anonymousUserRoles
|
AuthenticationResult(user, allGroups)
|
||||||
|
} ?: anonymousUserGroups?.let{ AuthenticationResult(null, it) }
|
||||||
} catch (es: SSLPeerUnverifiedException) {
|
} catch (es: SSLPeerUnverifiedException) {
|
||||||
anonymousUserRoles
|
anonymousUserGroups?.let{ AuthenticationResult(null, it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,26 +144,26 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
) : AbstractNettyHttpAuthenticator(authorizer) {
|
) : AbstractNettyHttpAuthenticator(authorizer) {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
|
|
||||||
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): Set<Role>? {
|
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): AuthenticationResult? {
|
||||||
val authorizationHeader = req.headers()[HttpHeaderNames.AUTHORIZATION] ?: let {
|
val authorizationHeader = req.headers()[HttpHeaderNames.AUTHORIZATION] ?: let {
|
||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Missing Authorization header"
|
"Missing Authorization header"
|
||||||
}
|
}
|
||||||
return users[""]?.roles
|
return users[""]?.let { AuthenticationResult(it, it.groups) }
|
||||||
}
|
}
|
||||||
val cursor = authorizationHeader.indexOf(' ')
|
val cursor = authorizationHeader.indexOf(' ')
|
||||||
if (cursor < 0) {
|
if (cursor < 0) {
|
||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Invalid Authorization header: '$authorizationHeader'"
|
"Invalid Authorization header: '$authorizationHeader'"
|
||||||
}
|
}
|
||||||
return users[""]?.roles
|
return users[""]?.let { AuthenticationResult(it, it.groups) }
|
||||||
}
|
}
|
||||||
val authenticationType = authorizationHeader.substring(0, cursor)
|
val authenticationType = authorizationHeader.substring(0, cursor)
|
||||||
if ("Basic" != authenticationType) {
|
if ("Basic" != authenticationType) {
|
||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Invalid authentication type header: '$authenticationType'"
|
"Invalid authentication type header: '$authenticationType'"
|
||||||
}
|
}
|
||||||
return users[""]?.roles
|
return users[""]?.let { AuthenticationResult(it, it.groups) }
|
||||||
}
|
}
|
||||||
val (username, password) = Base64.getDecoder().decode(authorizationHeader.substring(cursor + 1))
|
val (username, password) = Base64.getDecoder().decode(authorizationHeader.substring(cursor + 1))
|
||||||
.let(::String)
|
.let(::String)
|
||||||
@@ -189,7 +183,9 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
val (_, salt) = decodePasswordHash(passwordAndSalt)
|
val (_, salt) = decodePasswordHash(passwordAndSalt)
|
||||||
hashPassword(password, Base64.getEncoder().encodeToString(salt)) == passwordAndSalt
|
hashPassword(password, Base64.getEncoder().encodeToString(salt)) == passwordAndSalt
|
||||||
} ?: false
|
} ?: false
|
||||||
}?.roles
|
}?.let { user ->
|
||||||
|
AuthenticationResult(user, user.groups)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +253,14 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val exceptionHandler = ExceptionHandler()
|
private val exceptionHandler = ExceptionHandler()
|
||||||
|
private val throttlingHandler = ThrottlingHandler(cfg)
|
||||||
|
|
||||||
private val authenticator = when (val auth = cfg.authentication) {
|
private val authenticator = when (val auth = cfg.authentication) {
|
||||||
is Configuration.BasicAuthentication -> NettyHttpBasicAuthenticator(cfg.users, RoleAuthorizer())
|
is Configuration.BasicAuthentication -> NettyHttpBasicAuthenticator(cfg.users, RoleAuthorizer())
|
||||||
is Configuration.ClientCertificateAuthentication -> {
|
is Configuration.ClientCertificateAuthentication -> {
|
||||||
ClientCertificateAuthenticator(
|
ClientCertificateAuthenticator(
|
||||||
RoleAuthorizer(),
|
RoleAuthorizer(),
|
||||||
cfg.users[""]?.roles,
|
cfg.users[""]?.groups,
|
||||||
userExtractor(auth),
|
userExtractor(auth),
|
||||||
groupExtractor(auth)
|
groupExtractor(auth)
|
||||||
)
|
)
|
||||||
@@ -312,10 +309,10 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pipeline = ch.pipeline()
|
val pipeline = ch.pipeline()
|
||||||
cfg.connection.apply {
|
cfg.connection.also { conn ->
|
||||||
pipeline.addLast(ReadTimeoutHandler(readTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
pipeline.addLast(ReadTimeoutHandler(conn.readTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
||||||
pipeline.addLast(WriteTimeoutHandler(writeTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
pipeline.addLast(WriteTimeoutHandler(conn.writeTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
||||||
pipeline.addLast(IdleStateHandler(false, 0, 0, idleTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
pipeline.addLast(IdleStateHandler(false, 0, 0, conn.idleTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
||||||
}
|
}
|
||||||
pipeline.addLast(object : ChannelInboundHandlerAdapter() {
|
pipeline.addLast(object : ChannelInboundHandlerAdapter() {
|
||||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||||
@@ -337,65 +334,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
|
|||||||
authenticator?.let {
|
authenticator?.let {
|
||||||
pipeline.addLast(it)
|
pipeline.addLast(it)
|
||||||
}
|
}
|
||||||
|
pipeline.addLast(throttlingHandler)
|
||||||
pipeline.addLast(eventExecutorGroup, serverHandler)
|
pipeline.addLast(eventExecutorGroup, serverHandler)
|
||||||
pipeline.addLast(exceptionHandler)
|
pipeline.addLast(exceptionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sharable
|
|
||||||
private class ExceptionHandler : ChannelDuplexHandler() {
|
|
||||||
private val log = contextLogger()
|
|
||||||
|
|
||||||
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
|
||||||
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER
|
|
||||||
).apply {
|
|
||||||
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TOO_BIG: FullHttpResponse = DefaultFullHttpResponse(
|
|
||||||
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER
|
|
||||||
).apply {
|
|
||||||
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
|
||||||
when (cause) {
|
|
||||||
is DecoderException -> {
|
|
||||||
log.error(cause.message, cause)
|
|
||||||
ctx.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
is SSLPeerUnverifiedException -> {
|
|
||||||
ctx.writeAndFlush(NOT_AUTHORIZED.retainedDuplicate())
|
|
||||||
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ContentTooLargeException -> {
|
|
||||||
ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
|
|
||||||
.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 -> {
|
|
||||||
log.error(cause.message, cause)
|
|
||||||
ctx.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerHandle(
|
class ServerHandle(
|
||||||
httpChannelFuture: ChannelFuture,
|
httpChannelFuture: ChannelFuture,
|
||||||
private val executorGroups: Iterable<EventExecutorGroup>
|
private val executorGroups: Iterable<EventExecutorGroup>
|
||||||
|
@@ -11,32 +11,48 @@ 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.HttpVersion
|
import io.netty.handler.codec.http.HttpVersion
|
||||||
import io.netty.util.ReferenceCountUtil
|
import io.netty.util.ReferenceCountUtil
|
||||||
|
import net.woggioni.gbcs.api.Configuration
|
||||||
|
import net.woggioni.gbcs.api.Configuration.Group
|
||||||
import net.woggioni.gbcs.api.Role
|
import net.woggioni.gbcs.api.Role
|
||||||
|
import net.woggioni.gbcs.server.GradleBuildCacheServer
|
||||||
|
|
||||||
|
|
||||||
abstract class AbstractNettyHttpAuthenticator(private val authorizer : Authorizer)
|
abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer) : ChannelInboundHandlerAdapter() {
|
||||||
: ChannelInboundHandlerAdapter() {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val AUTHENTICATION_FAILED: FullHttpResponse = DefaultFullHttpResponse(
|
private val AUTHENTICATION_FAILED: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, Unpooled.EMPTY_BUFFER).apply {
|
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER).apply {
|
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AuthenticationResult(val user: Configuration.User?, val groups: Set<Group>)
|
||||||
|
|
||||||
abstract fun authenticate(ctx : ChannelHandlerContext, req : HttpRequest) : Set<Role>?
|
abstract fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): AuthenticationResult?
|
||||||
|
|
||||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
if(msg is HttpRequest) {
|
if (msg is HttpRequest) {
|
||||||
val roles = authenticate(ctx, msg) ?: return authenticationFailure(ctx, msg)
|
val result = authenticate(ctx, msg) ?: return authenticationFailure(ctx, msg)
|
||||||
|
ctx.channel().attr(GradleBuildCacheServer.userAttribute).set(result.user)
|
||||||
|
ctx.channel().attr(GradleBuildCacheServer.groupAttribute).set(result.groups)
|
||||||
|
|
||||||
|
val roles = (
|
||||||
|
(result.user?.let { user ->
|
||||||
|
user.groups.asSequence().flatMap { group ->
|
||||||
|
group.roles.asSequence()
|
||||||
|
}
|
||||||
|
} ?: emptySequence<Role>()) +
|
||||||
|
result.groups.asSequence().flatMap { it.roles.asSequence() }
|
||||||
|
).toSet()
|
||||||
val authorized = authorizer.authorize(roles, msg)
|
val authorized = authorizer.authorize(roles, msg)
|
||||||
if(authorized) {
|
if (authorized) {
|
||||||
super.channelRead(ctx, msg)
|
super.channelRead(ctx, msg)
|
||||||
} else {
|
} else {
|
||||||
authorizationFailure(ctx, msg)
|
authorizationFailure(ctx, msg)
|
||||||
|
@@ -8,7 +8,7 @@ class RoleAuthorizer : Authorizer {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val METHOD_MAP = mapOf(
|
private val METHOD_MAP = mapOf(
|
||||||
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD),
|
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE),
|
||||||
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST)
|
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
21
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/CacheUtils.kt
vendored
Normal file
21
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/CacheUtils.kt
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package net.woggioni.gbcs.server.cache
|
||||||
|
|
||||||
|
import net.woggioni.jwo.JWO
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
object CacheUtils {
|
||||||
|
fun digest(
|
||||||
|
data: ByteArray,
|
||||||
|
md: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
): ByteArray {
|
||||||
|
md.update(data)
|
||||||
|
return md.digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun digestString(
|
||||||
|
data: ByteArray,
|
||||||
|
md: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
): String {
|
||||||
|
return JWO.bytesToHex(digest(data, md))
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
package net.woggioni.gbcs.server.cache
|
package net.woggioni.gbcs.server.cache
|
||||||
|
|
||||||
import net.woggioni.gbcs.api.Cache
|
import net.woggioni.gbcs.api.Cache
|
||||||
import net.woggioni.jwo.JWO
|
import net.woggioni.gbcs.common.contextLogger
|
||||||
|
import net.woggioni.gbcs.server.cache.CacheUtils.digestString
|
||||||
import net.woggioni.jwo.LockFile
|
import net.woggioni.jwo.LockFile
|
||||||
import java.nio.channels.Channels
|
import java.nio.channels.Channels
|
||||||
import java.nio.channels.FileChannel
|
import java.nio.channels.FileChannel
|
||||||
@@ -19,7 +20,6 @@ import java.util.zip.DeflaterOutputStream
|
|||||||
import java.util.zip.Inflater
|
import java.util.zip.Inflater
|
||||||
import java.util.zip.InflaterInputStream
|
import java.util.zip.InflaterInputStream
|
||||||
|
|
||||||
|
|
||||||
class FileSystemCache(
|
class FileSystemCache(
|
||||||
val root: Path,
|
val root: Path,
|
||||||
val maxAge: Duration,
|
val maxAge: Duration,
|
||||||
@@ -28,7 +28,7 @@ class FileSystemCache(
|
|||||||
val compressionLevel: Int
|
val compressionLevel: Int
|
||||||
) : Cache {
|
) : Cache {
|
||||||
|
|
||||||
private fun lockFilePath(key: String): Path = root.resolve("$key.lock")
|
private val log = contextLogger()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Files.createDirectories(root)
|
Files.createDirectories(root)
|
||||||
@@ -41,11 +41,21 @@ class FileSystemCache(
|
|||||||
?.let { md ->
|
?.let { md ->
|
||||||
digestString(key.toByteArray(), md)
|
digestString(key.toByteArray(), md)
|
||||||
} ?: key).let { digest ->
|
} ?: key).let { digest ->
|
||||||
LockFile.acquire(lockFilePath(digest), true).use {
|
root.resolve(digest).takeIf(Files::exists)
|
||||||
root.resolve(digest).takeIf(Files::exists)?.let { file ->
|
?.let { file ->
|
||||||
|
file.takeIf(Files::exists)?.let { file ->
|
||||||
if (compressionEnabled) {
|
if (compressionEnabled) {
|
||||||
val inflater = Inflater()
|
val inflater = Inflater()
|
||||||
Channels.newChannel(InflaterInputStream(Files.newInputStream(file), inflater))
|
Channels.newChannel(
|
||||||
|
InflaterInputStream(
|
||||||
|
Channels.newInputStream(
|
||||||
|
FileChannel.open(
|
||||||
|
file,
|
||||||
|
StandardOpenOption.READ
|
||||||
|
)
|
||||||
|
), inflater
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
FileChannel.open(file, StandardOpenOption.READ)
|
FileChannel.open(file, StandardOpenOption.READ)
|
||||||
}
|
}
|
||||||
@@ -61,7 +71,6 @@ class FileSystemCache(
|
|||||||
?.let { md ->
|
?.let { md ->
|
||||||
digestString(key.toByteArray(), md)
|
digestString(key.toByteArray(), md)
|
||||||
} ?: key).let { digest ->
|
} ?: key).let { digest ->
|
||||||
LockFile.acquire(lockFilePath(digest), false).use {
|
|
||||||
val file = root.resolve(digest)
|
val file = root.resolve(digest)
|
||||||
val tmpFile = Files.createTempFile(root, null, ".tmp")
|
val tmpFile = Files.createTempFile(root, null, ".tmp")
|
||||||
try {
|
try {
|
||||||
@@ -80,7 +89,6 @@ class FileSystemCache(
|
|||||||
Files.delete(tmpFile)
|
Files.delete(tmpFile)
|
||||||
throw t
|
throw t
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}.also {
|
}.also {
|
||||||
gc()
|
gc()
|
||||||
}
|
}
|
||||||
@@ -97,37 +105,16 @@ class FileSystemCache(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun actualGc(now: Instant) {
|
private fun actualGc(now: Instant) {
|
||||||
Files.list(root).filter {
|
Files.list(root).filter {
|
||||||
!it.fileName.toString().endsWith(".lock")
|
|
||||||
}.filter {
|
|
||||||
val creationTimeStamp = Files.readAttributes(it, BasicFileAttributes::class.java)
|
val creationTimeStamp = Files.readAttributes(it, BasicFileAttributes::class.java)
|
||||||
.creationTime()
|
.creationTime()
|
||||||
.toInstant()
|
.toInstant()
|
||||||
now > creationTimeStamp.plus(maxAge)
|
now > creationTimeStamp.plus(maxAge)
|
||||||
}.forEach { file ->
|
}.forEach { file ->
|
||||||
val lockFile = lockFilePath(file.fileName.toString())
|
LockFile.acquire(file, false).use {
|
||||||
LockFile.acquire(lockFile, false).use {
|
|
||||||
Files.delete(file)
|
Files.delete(file)
|
||||||
}
|
}
|
||||||
Files.delete(lockFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {}
|
override fun close() {}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun digest(
|
|
||||||
data: ByteArray,
|
|
||||||
md: MessageDigest = MessageDigest.getInstance("MD5")
|
|
||||||
): ByteArray {
|
|
||||||
md.update(data)
|
|
||||||
return md.digest()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun digestString(
|
|
||||||
data: ByteArray,
|
|
||||||
md: MessageDigest = MessageDigest.getInstance("MD5")
|
|
||||||
): String {
|
|
||||||
return JWO.bytesToHex(digest(data, md))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
106
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCache.kt
vendored
Normal file
106
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCache.kt
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package net.woggioni.gbcs.server.cache
|
||||||
|
|
||||||
|
import net.woggioni.gbcs.api.Cache
|
||||||
|
import net.woggioni.gbcs.server.cache.CacheUtils.digestString
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.Channels
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.PriorityBlockingQueue
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.zip.Deflater
|
||||||
|
import java.util.zip.DeflaterOutputStream
|
||||||
|
import java.util.zip.Inflater
|
||||||
|
import java.util.zip.InflaterInputStream
|
||||||
|
|
||||||
|
class InMemoryCache(
|
||||||
|
val maxAge: Duration,
|
||||||
|
val digestAlgorithm: String?,
|
||||||
|
val compressionEnabled: Boolean,
|
||||||
|
val compressionLevel: Int
|
||||||
|
) : Cache {
|
||||||
|
|
||||||
|
private val map = ConcurrentHashMap<String, MapValue>()
|
||||||
|
|
||||||
|
private class MapValue(val rc: AtomicInteger, val payload : AtomicReference<ByteArray>)
|
||||||
|
|
||||||
|
private class RemovalQueueElement(val key: String, val expiry : Instant) : Comparable<RemovalQueueElement> {
|
||||||
|
override fun compareTo(other: RemovalQueueElement)= expiry.compareTo(other.expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
|
||||||
|
|
||||||
|
private var running = true
|
||||||
|
private val garbageCollector = Thread({
|
||||||
|
while(true) {
|
||||||
|
val el = removalQueue.take()
|
||||||
|
val now = Instant.now()
|
||||||
|
if(now > el.expiry) {
|
||||||
|
val value = map[el.key] ?: continue
|
||||||
|
val rc = value.rc.decrementAndGet()
|
||||||
|
if(rc == 0) {
|
||||||
|
map.remove(el.key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removalQueue.put(el)
|
||||||
|
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).apply {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
running = false
|
||||||
|
garbageCollector.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(key: String) =
|
||||||
|
(digestAlgorithm
|
||||||
|
?.let(MessageDigest::getInstance)
|
||||||
|
?.let { md ->
|
||||||
|
digestString(key.toByteArray(), md)
|
||||||
|
} ?: key
|
||||||
|
).let { digest ->
|
||||||
|
map[digest]
|
||||||
|
?.let(MapValue::payload)
|
||||||
|
?.let(AtomicReference<ByteArray>::get)
|
||||||
|
?.let { value ->
|
||||||
|
if (compressionEnabled) {
|
||||||
|
val inflater = Inflater()
|
||||||
|
Channels.newChannel(InflaterInputStream(ByteArrayInputStream(value), inflater))
|
||||||
|
} else {
|
||||||
|
Channels.newChannel(ByteArrayInputStream(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun put(key: String, content: ByteArray) {
|
||||||
|
(digestAlgorithm
|
||||||
|
?.let(MessageDigest::getInstance)
|
||||||
|
?.let { md ->
|
||||||
|
digestString(key.toByteArray(), md)
|
||||||
|
} ?: key).let { digest ->
|
||||||
|
val value = if (compressionEnabled) {
|
||||||
|
val deflater = Deflater(compressionLevel)
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
DeflaterOutputStream(baos, deflater).use { stream ->
|
||||||
|
stream.write(content)
|
||||||
|
}
|
||||||
|
baos.toByteArray()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
val mapValue = map.computeIfAbsent(digest) {
|
||||||
|
MapValue(AtomicInteger(0), AtomicReference())
|
||||||
|
}
|
||||||
|
mapValue.payload.set(value)
|
||||||
|
removalQueue.put(RemovalQueueElement(digest, Instant.now().plus(maxAge)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCacheConfiguration.kt
vendored
Normal file
23
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCacheConfiguration.kt
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package net.woggioni.gbcs.server.cache
|
||||||
|
|
||||||
|
import net.woggioni.gbcs.api.Configuration
|
||||||
|
import net.woggioni.gbcs.common.GBCS
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
data class InMemoryCacheConfiguration(
|
||||||
|
val maxAge: Duration,
|
||||||
|
val digestAlgorithm : String?,
|
||||||
|
val compressionEnabled: Boolean,
|
||||||
|
val compressionLevel: Int,
|
||||||
|
) : Configuration.Cache {
|
||||||
|
override fun materialize() = InMemoryCache(
|
||||||
|
maxAge,
|
||||||
|
digestAlgorithm,
|
||||||
|
compressionEnabled,
|
||||||
|
compressionLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
|
||||||
|
|
||||||
|
override fun getTypeName() = "inMemoryCacheType"
|
||||||
|
}
|
59
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCacheProvider.kt
vendored
Normal file
59
gbcs-server/src/main/kotlin/net/woggioni/gbcs/server/cache/InMemoryCacheProvider.kt
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package net.woggioni.gbcs.server.cache
|
||||||
|
|
||||||
|
import net.woggioni.gbcs.api.CacheProvider
|
||||||
|
import net.woggioni.gbcs.common.GBCS
|
||||||
|
import net.woggioni.gbcs.common.Xml
|
||||||
|
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.zip.Deflater
|
||||||
|
|
||||||
|
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
||||||
|
|
||||||
|
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/server/schema/gbcs.xsd"
|
||||||
|
|
||||||
|
override fun getXmlType() = "inMemoryCacheType"
|
||||||
|
|
||||||
|
override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server"
|
||||||
|
|
||||||
|
override fun deserialize(el: Element): InMemoryCacheConfiguration {
|
||||||
|
val maxAge = el.renderAttribute("max-age")
|
||||||
|
?.let(Duration::parse)
|
||||||
|
?: Duration.ofDays(1)
|
||||||
|
val enableCompression = el.renderAttribute("enable-compression")
|
||||||
|
?.let(String::toBoolean)
|
||||||
|
?: true
|
||||||
|
val compressionLevel = el.renderAttribute("compression-level")
|
||||||
|
?.let(String::toInt)
|
||||||
|
?: Deflater.DEFAULT_COMPRESSION
|
||||||
|
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5"
|
||||||
|
|
||||||
|
return InMemoryCacheConfiguration(
|
||||||
|
maxAge,
|
||||||
|
digestAlgorithm,
|
||||||
|
enableCompression,
|
||||||
|
compressionLevel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(doc: Document, cache : InMemoryCacheConfiguration) = cache.run {
|
||||||
|
val result = doc.createElement("cache")
|
||||||
|
Xml.of(doc, result) {
|
||||||
|
val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
|
||||||
|
attr("xs:type", "${prefix}:inMemoryCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
|
||||||
|
attr("max-age", maxAge.toString())
|
||||||
|
digestAlgorithm?.let { digestAlgorithm ->
|
||||||
|
attr("digest", digestAlgorithm)
|
||||||
|
}
|
||||||
|
attr("enable-compression", compressionEnabled.toString())
|
||||||
|
compressionLevel.takeIf {
|
||||||
|
it != Deflater.DEFAULT_COMPRESSION
|
||||||
|
}?.let {
|
||||||
|
attr("compression-level", it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
@@ -25,7 +25,7 @@ import java.time.temporal.ChronoUnit
|
|||||||
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(), null)
|
||||||
var connection: Configuration.Connection = Configuration.Connection(
|
var connection: Configuration.Connection = Configuration.Connection(
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
Duration.of(10, ChronoUnit.SECONDS),
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
Duration.of(10, ChronoUnit.SECONDS),
|
||||||
@@ -38,7 +38,7 @@ object Parser {
|
|||||||
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
|
||||||
var users : Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
|
var users: Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
|
||||||
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")
|
||||||
@@ -85,6 +85,7 @@ object Parser {
|
|||||||
"users" -> {
|
"users" -> {
|
||||||
knownUsers += parseUsers(gchild)
|
knownUsers += parseUsers(gchild)
|
||||||
}
|
}
|
||||||
|
|
||||||
"groups" -> {
|
"groups" -> {
|
||||||
val pair = parseGroups(gchild, knownUsers)
|
val pair = parseGroups(gchild, knownUsers)
|
||||||
users = pair.first
|
users = pair.first
|
||||||
@@ -107,7 +108,7 @@ object Parser {
|
|||||||
val typeNamespace = tf.typeNamespace
|
val typeNamespace = tf.typeNamespace
|
||||||
val typeName = tf.typeName
|
val typeName = tf.typeName
|
||||||
CacheSerializers.index[typeNamespace to typeName]
|
CacheSerializers.index[typeNamespace to typeName]
|
||||||
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' not found")
|
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' with name '$typeName' not found")
|
||||||
}.deserialize(child)
|
}.deserialize(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +134,13 @@ object Parser {
|
|||||||
maxRequestSize
|
maxRequestSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"event-executor" -> {
|
"event-executor" -> {
|
||||||
val useVirtualThread = root.renderAttribute("use-virtual-threads")
|
val useVirtualThread = root.renderAttribute("use-virtual-threads")
|
||||||
?.let(String::toBoolean) ?: true
|
?.let(String::toBoolean) ?: true
|
||||||
eventExecutor = Configuration.EventExecutor(useVirtualThread)
|
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
|
||||||
@@ -200,20 +203,54 @@ object Parser {
|
|||||||
}.toSet()
|
}.toSet()
|
||||||
|
|
||||||
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
|
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
|
||||||
when(it.localName) {
|
when (it.localName) {
|
||||||
"user" -> it.renderAttribute("ref")
|
"user" -> it.renderAttribute("ref")
|
||||||
"anonymous" -> ""
|
"anonymous" -> ""
|
||||||
else -> ConfigurationException("Unrecognized tag '${it.localName}'")
|
else -> ConfigurationException("Unrecognized tag '${it.localName}'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseQuota(el: Element): Configuration.Quota {
|
||||||
|
val calls = el.renderAttribute("calls")
|
||||||
|
?.let(String::toLong)
|
||||||
|
?: throw ConfigurationException("Missing attribute 'calls'")
|
||||||
|
val maxAvailableCalls = el.renderAttribute("max-available-calls")
|
||||||
|
?.let(String::toLong)
|
||||||
|
?: calls
|
||||||
|
val initialAvailableCalls = el.renderAttribute("initial-available-calls")
|
||||||
|
?.let(String::toLong)
|
||||||
|
?: maxAvailableCalls
|
||||||
|
val period = el.renderAttribute("period")
|
||||||
|
?.let(Duration::parse)
|
||||||
|
?: throw ConfigurationException("Missing attribute 'period'")
|
||||||
|
return Configuration.Quota(calls, period, initialAvailableCalls, maxAvailableCalls)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseUsers(root: Element): Sequence<User> {
|
private fun parseUsers(root: Element): Sequence<User> {
|
||||||
return root.asIterable().asSequence().filter {
|
return root.asIterable().asSequence().mapNotNull { child ->
|
||||||
it.localName == "user"
|
when (child.localName) {
|
||||||
}.map { el ->
|
"user" -> {
|
||||||
val username = el.renderAttribute("name")
|
val username = child.renderAttribute("name")
|
||||||
val password = el.renderAttribute("password")
|
val password = child.renderAttribute("password")
|
||||||
User(username, password, emptySet())
|
var quota: Configuration.Quota? = null
|
||||||
|
for (gchild in child.asIterable()) {
|
||||||
|
if (gchild.localName == "quota") {
|
||||||
|
quota = parseQuota(gchild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
User(username, password, emptySet(), quota)
|
||||||
|
}
|
||||||
|
"anonymous" -> {
|
||||||
|
var quota: Configuration.Quota? = null
|
||||||
|
for (gchild in child.asIterable()) {
|
||||||
|
if (gchild.localName == "quota") {
|
||||||
|
quota= parseQuota(gchild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
User("", null, emptySet(), quota)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +262,7 @@ object Parser {
|
|||||||
}.map { el ->
|
}.map { el ->
|
||||||
val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
|
val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
|
||||||
var roles = emptySet<Role>()
|
var roles = emptySet<Role>()
|
||||||
|
var quota: Configuration.Quota? = null
|
||||||
for (child in el.asIterable()) {
|
for (child in el.asIterable()) {
|
||||||
when (child.localName) {
|
when (child.localName) {
|
||||||
"users" -> {
|
"users" -> {
|
||||||
@@ -238,12 +276,15 @@ object Parser {
|
|||||||
"roles" -> {
|
"roles" -> {
|
||||||
roles = parseRoles(child)
|
roles = parseRoles(child)
|
||||||
}
|
}
|
||||||
|
"quota" -> {
|
||||||
|
quota = parseQuota(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groupName to Group(groupName, roles)
|
}
|
||||||
|
groupName to Group(groupName, roles, quota)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
val users = knownUsersMap.map { (name, user) ->
|
val users = knownUsersMap.map { (name, user) ->
|
||||||
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
|
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet(), user.quota)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
return users to groups
|
return users to groups
|
||||||
}
|
}
|
||||||
|
@@ -54,6 +54,27 @@ object Serializer {
|
|||||||
user.password?.let { password ->
|
user.password?.let { password ->
|
||||||
attr("password", password)
|
attr("password", password)
|
||||||
}
|
}
|
||||||
|
user.quota?.let { quota ->
|
||||||
|
node("quota") {
|
||||||
|
attr("calls", quota.calls.toString())
|
||||||
|
attr("period", quota.period.toString())
|
||||||
|
attr("max-available-calls", quota.maxAvailableCalls.toString())
|
||||||
|
attr("initial-available-calls", quota.initialAvailableCalls.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.users[""]
|
||||||
|
?.let { anonymousUser ->
|
||||||
|
anonymousUser.quota?.let { quota ->
|
||||||
|
node("anonymous") {
|
||||||
|
node("quota") {
|
||||||
|
attr("calls", quota.calls.toString())
|
||||||
|
attr("period", quota.period.toString())
|
||||||
|
attr("max-available-calls", quota.maxAvailableCalls.toString())
|
||||||
|
attr("initial-available-calls", quota.initialAvailableCalls.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +113,14 @@ object Serializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
group.quota?.let { quota ->
|
||||||
|
node("quota") {
|
||||||
|
attr("calls", quota.calls.toString())
|
||||||
|
attr("period", quota.period.toString())
|
||||||
|
attr("max-available-calls", quota.maxAvailableCalls.toString())
|
||||||
|
attr("initial-available-calls", quota.initialAvailableCalls.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,92 @@
|
|||||||
|
package net.woggioni.gbcs.server.exception
|
||||||
|
|
||||||
|
import io.netty.buffer.Unpooled
|
||||||
|
import io.netty.channel.ChannelDuplexHandler
|
||||||
|
import io.netty.channel.ChannelFutureListener
|
||||||
|
import io.netty.channel.ChannelHandler
|
||||||
|
import io.netty.channel.ChannelHandlerContext
|
||||||
|
import io.netty.handler.codec.DecoderException
|
||||||
|
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus
|
||||||
|
import io.netty.handler.codec.http.HttpVersion
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutException
|
||||||
|
import io.netty.handler.timeout.WriteTimeoutException
|
||||||
|
import net.woggioni.gbcs.api.exception.CacheException
|
||||||
|
import net.woggioni.gbcs.api.exception.ContentTooLargeException
|
||||||
|
import net.woggioni.gbcs.common.contextLogger
|
||||||
|
import net.woggioni.gbcs.common.debug
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException
|
||||||
|
|
||||||
|
@ChannelHandler.Sharable
|
||||||
|
class ExceptionHandler : ChannelDuplexHandler() {
|
||||||
|
private val log = contextLogger()
|
||||||
|
|
||||||
|
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
|
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TOO_BIG: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
|
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val NOT_AVAILABLE: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
|
HttpVersion.HTTP_1_1, HttpResponseStatus.SERVICE_UNAVAILABLE, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SERVER_ERROR: FullHttpResponse = DefaultFullHttpResponse(
|
||||||
|
HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.EMPTY_BUFFER
|
||||||
|
).apply {
|
||||||
|
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
|
when (cause) {
|
||||||
|
is DecoderException -> {
|
||||||
|
log.error(cause.message, cause)
|
||||||
|
ctx.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
is SSLPeerUnverifiedException -> {
|
||||||
|
ctx.writeAndFlush(NOT_AUTHORIZED.retainedDuplicate())
|
||||||
|
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ContentTooLargeException -> {
|
||||||
|
ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
is CacheException -> {
|
||||||
|
log.error(cause.message, cause)
|
||||||
|
ctx.writeAndFlush(NOT_AVAILABLE.retainedDuplicate())
|
||||||
|
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log.error(cause.message, cause)
|
||||||
|
ctx.writeAndFlush(SERVER_ERROR.retainedDuplicate())
|
||||||
|
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -15,9 +15,9 @@ import io.netty.handler.codec.http.HttpMethod
|
|||||||
import io.netty.handler.codec.http.HttpResponseStatus
|
import io.netty.handler.codec.http.HttpResponseStatus
|
||||||
import io.netty.handler.codec.http.HttpUtil
|
import io.netty.handler.codec.http.HttpUtil
|
||||||
import io.netty.handler.codec.http.LastHttpContent
|
import io.netty.handler.codec.http.LastHttpContent
|
||||||
import io.netty.handler.stream.ChunkedNioFile
|
|
||||||
import io.netty.handler.stream.ChunkedNioStream
|
import io.netty.handler.stream.ChunkedNioStream
|
||||||
import net.woggioni.gbcs.api.Cache
|
import net.woggioni.gbcs.api.Cache
|
||||||
|
import net.woggioni.gbcs.api.exception.CacheException
|
||||||
import net.woggioni.gbcs.common.contextLogger
|
import net.woggioni.gbcs.common.contextLogger
|
||||||
import net.woggioni.gbcs.server.debug
|
import net.woggioni.gbcs.server.debug
|
||||||
import net.woggioni.gbcs.server.warn
|
import net.woggioni.gbcs.server.warn
|
||||||
@@ -38,7 +38,11 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
|
|||||||
val prefix = path.parent
|
val prefix = path.parent
|
||||||
val key = path.fileName.toString()
|
val key = path.fileName.toString()
|
||||||
if (serverPrefix == prefix) {
|
if (serverPrefix == prefix) {
|
||||||
cache.get(key)?.let { channel ->
|
try {
|
||||||
|
cache.get(key)
|
||||||
|
} catch(ex : Throwable) {
|
||||||
|
throw CacheException("Error accessing the cache backend", ex)
|
||||||
|
}?.let { channel ->
|
||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Cache hit for key '$key'"
|
"Cache hit for key '$key'"
|
||||||
}
|
}
|
||||||
@@ -55,17 +59,18 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
|
|||||||
when (channel) {
|
when (channel) {
|
||||||
is FileChannel -> {
|
is FileChannel -> {
|
||||||
if (keepAlive) {
|
if (keepAlive) {
|
||||||
ctx.write(ChunkedNioFile(channel))
|
ctx.write(DefaultFileRegion(channel, 0, channel.size()))
|
||||||
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
|
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
|
||||||
} else {
|
} else {
|
||||||
ctx.writeAndFlush(DefaultFileRegion(channel, 0, channel.size()))
|
ctx.writeAndFlush(DefaultFileRegion(channel, 0, channel.size()))
|
||||||
.addListener(ChannelFutureListener.CLOSE)
|
.addListener(ChannelFutureListener.CLOSE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
ctx.write(ChunkedNioStream(channel))
|
ctx.write(ChunkedNioStream(channel)).addListener { evt ->
|
||||||
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
|
channel.close()
|
||||||
|
}
|
||||||
|
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: let {
|
} ?: let {
|
||||||
@@ -102,7 +107,11 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
|
|||||||
array()
|
array()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
cache.put(key, bodyBytes)
|
cache.put(key, bodyBytes)
|
||||||
|
} catch(ex : Throwable) {
|
||||||
|
throw CacheException("Error accessing the cache backend", ex)
|
||||||
|
}
|
||||||
val response = DefaultFullHttpResponse(
|
val response = DefaultFullHttpResponse(
|
||||||
msg.protocolVersion(), HttpResponseStatus.CREATED,
|
msg.protocolVersion(), HttpResponseStatus.CREATED,
|
||||||
Unpooled.copiedBuffer(key.toByteArray())
|
Unpooled.copiedBuffer(key.toByteArray())
|
||||||
@@ -117,11 +126,35 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
|
|||||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
ctx.writeAndFlush(response)
|
ctx.writeAndFlush(response)
|
||||||
}
|
}
|
||||||
|
} else if(method == HttpMethod.TRACE) {
|
||||||
|
val replayedRequestHead = ctx.alloc().buffer()
|
||||||
|
replayedRequestHead.writeCharSequence("TRACE ${Path.of(msg.uri())} ${msg.protocolVersion().text()}\r\n", Charsets.US_ASCII)
|
||||||
|
msg.headers().forEach { (key, value) ->
|
||||||
|
replayedRequestHead.apply {
|
||||||
|
writeCharSequence(key, Charsets.US_ASCII)
|
||||||
|
writeCharSequence(": ", Charsets.US_ASCII)
|
||||||
|
writeCharSequence(value, Charsets.UTF_8)
|
||||||
|
writeCharSequence("\r\n", Charsets.US_ASCII)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replayedRequestHead.writeCharSequence("\r\n", Charsets.US_ASCII)
|
||||||
|
val requestBody = msg.content()
|
||||||
|
requestBody.retain()
|
||||||
|
val responseBody = ctx.alloc().compositeBuffer(2).apply {
|
||||||
|
addComponents(true, replayedRequestHead)
|
||||||
|
addComponents(true, requestBody)
|
||||||
|
}
|
||||||
|
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK, responseBody)
|
||||||
|
response.headers().apply {
|
||||||
|
set(HttpHeaderNames.CONTENT_TYPE, "message/http")
|
||||||
|
set(HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes())
|
||||||
|
}
|
||||||
|
ctx.writeAndFlush(response)
|
||||||
} else {
|
} else {
|
||||||
log.warn(ctx) {
|
log.warn(ctx) {
|
||||||
"Got request with unhandled method '${msg.method().name()}'"
|
"Got request with unhandled method '${msg.method().name()}'"
|
||||||
}
|
}
|
||||||
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
|
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.METHOD_NOT_ALLOWED)
|
||||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
|
||||||
ctx.writeAndFlush(response)
|
ctx.writeAndFlush(response)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
package net.woggioni.gbcs.server.throttling
|
||||||
|
|
||||||
|
import net.woggioni.gbcs.api.Configuration
|
||||||
|
import net.woggioni.jwo.Bucket
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class BucketManager private constructor(
|
||||||
|
private val bucketsByUser: Map<Configuration.User, Bucket> = HashMap(),
|
||||||
|
private val bucketsByGroup: Map<Configuration.Group, Bucket> = HashMap(),
|
||||||
|
loader: Function<InetSocketAddress, Bucket>?
|
||||||
|
) {
|
||||||
|
|
||||||
|
private class BucketsByAddress(
|
||||||
|
private val map: MutableMap<ByteArrayKey, Bucket>,
|
||||||
|
private val loader: Function<InetSocketAddress, Bucket>
|
||||||
|
) {
|
||||||
|
fun getBucket(socketAddress : InetSocketAddress) = map.computeIfAbsent(ByteArrayKey(socketAddress.address.address)) {
|
||||||
|
loader.apply(socketAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val bucketsByAddress: BucketsByAddress? = loader?.let {
|
||||||
|
BucketsByAddress(ConcurrentHashMap(), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ByteArrayKey(val array: ByteArray) {
|
||||||
|
override fun equals(other: Any?) = (other as? ByteArrayKey)?.let { bak ->
|
||||||
|
array contentEquals bak.array
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
override fun hashCode() = Arrays.hashCode(array)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBucketByAddress(address : InetSocketAddress) : Bucket? {
|
||||||
|
return bucketsByAddress?.getBucket(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBucketByUser(user : Configuration.User) = bucketsByUser[user]
|
||||||
|
fun getBucketByGroup(group : Configuration.Group) = bucketsByGroup[group]
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(cfg : Configuration) : BucketManager {
|
||||||
|
val bucketsByUser = cfg.users.values.asSequence().filter {
|
||||||
|
it.quota != null
|
||||||
|
}.map { user ->
|
||||||
|
val quota = user.quota
|
||||||
|
val bucket = Bucket.local(
|
||||||
|
quota.maxAvailableCalls,
|
||||||
|
quota.calls,
|
||||||
|
quota.period,
|
||||||
|
quota.initialAvailableCalls
|
||||||
|
)
|
||||||
|
user to bucket
|
||||||
|
}.toMap()
|
||||||
|
val bucketsByGroup = cfg.groups.values.asSequence().filter {
|
||||||
|
it.quota != null
|
||||||
|
}.map { group ->
|
||||||
|
val quota = group.quota
|
||||||
|
val bucket = Bucket.local(
|
||||||
|
quota.maxAvailableCalls,
|
||||||
|
quota.calls,
|
||||||
|
quota.period,
|
||||||
|
quota.initialAvailableCalls
|
||||||
|
)
|
||||||
|
group to bucket
|
||||||
|
}.toMap()
|
||||||
|
return BucketManager(
|
||||||
|
bucketsByUser,
|
||||||
|
bucketsByGroup,
|
||||||
|
cfg.users[""]?.quota?.let { anonymousUserQuota ->
|
||||||
|
Function {
|
||||||
|
Bucket.local(
|
||||||
|
anonymousUserQuota.maxAvailableCalls,
|
||||||
|
anonymousUserQuota.calls,
|
||||||
|
anonymousUserQuota.period,
|
||||||
|
anonymousUserQuota.initialAvailableCalls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
package net.woggioni.gbcs.server.throttling
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelHandler.Sharable
|
||||||
|
import io.netty.channel.ChannelHandlerContext
|
||||||
|
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||||
|
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus
|
||||||
|
import io.netty.handler.codec.http.HttpVersion
|
||||||
|
import net.woggioni.gbcs.api.Configuration
|
||||||
|
import net.woggioni.gbcs.common.contextLogger
|
||||||
|
import net.woggioni.gbcs.server.GradleBuildCacheServer
|
||||||
|
import net.woggioni.jwo.Bucket
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
|
@Sharable
|
||||||
|
class ThrottlingHandler(cfg: Configuration) :
|
||||||
|
ChannelInboundHandlerAdapter() {
|
||||||
|
|
||||||
|
private val log = contextLogger()
|
||||||
|
private val bucketManager = BucketManager.from(cfg)
|
||||||
|
|
||||||
|
private val connectionConfiguration = cfg.connection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the suggested waiting time from the bucket is lower than this
|
||||||
|
* amount, then the server will simply wait by itself before sending a response
|
||||||
|
* instead of replying with 429
|
||||||
|
*/
|
||||||
|
private val waitThreshold = minOf(
|
||||||
|
connectionConfiguration.idleTimeout,
|
||||||
|
connectionConfiguration.readIdleTimeout,
|
||||||
|
connectionConfiguration.writeIdleTimeout
|
||||||
|
).dividedBy(2)
|
||||||
|
|
||||||
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
|
val buckets = mutableListOf<Bucket>()
|
||||||
|
val user = ctx.channel().attr(GradleBuildCacheServer.userAttribute).get()
|
||||||
|
if (user != null) {
|
||||||
|
bucketManager.getBucketByUser(user)?.let(buckets::add)
|
||||||
|
}
|
||||||
|
val groups = ctx.channel().attr(GradleBuildCacheServer.groupAttribute).get() ?: emptySet()
|
||||||
|
if (groups.isNotEmpty()) {
|
||||||
|
groups.forEach { group ->
|
||||||
|
bucketManager.getBucketByGroup(group)?.let(buckets::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user == null && groups.isEmpty()) {
|
||||||
|
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
|
||||||
|
}
|
||||||
|
if (buckets.isEmpty()) {
|
||||||
|
return super.channelRead(ctx, msg)
|
||||||
|
} else {
|
||||||
|
var nextAttempt = Long.MAX_VALUE
|
||||||
|
for (bucket in buckets) {
|
||||||
|
val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
|
||||||
|
if (bucketNextAttempt < 0) {
|
||||||
|
return super.channelRead(ctx, msg)
|
||||||
|
} else if (bucketNextAttempt < nextAttempt) {
|
||||||
|
nextAttempt = bucketNextAttempt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val waitDuration = Duration.ofNanos(nextAttempt)
|
||||||
|
if (waitDuration < waitThreshold) {
|
||||||
|
ctx.executor().schedule({
|
||||||
|
ctx.fireChannelRead(msg)
|
||||||
|
}, waitDuration.toNanos(), TimeUnit.NANOSECONDS)
|
||||||
|
} else {
|
||||||
|
sendThrottledResponse(ctx, waitDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) {
|
||||||
|
val response = DefaultFullHttpResponse(
|
||||||
|
HttpVersion.HTTP_1_1,
|
||||||
|
HttpResponseStatus.TOO_MANY_REQUESTS
|
||||||
|
)
|
||||||
|
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
||||||
|
response.headers()[HttpHeaderNames.RETRY_AFTER] = retryAfter.seconds
|
||||||
|
ctx.writeAndFlush(response)
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +1,2 @@
|
|||||||
net.woggioni.gbcs.server.cache.FileSystemCacheProvider
|
net.woggioni.gbcs.server.cache.FileSystemCacheProvider
|
||||||
|
net.woggioni.gbcs.server.cache.InMemoryCacheProvider
|
@@ -48,6 +48,17 @@
|
|||||||
|
|
||||||
<xs:complexType name="cacheType" abstract="true"/>
|
<xs:complexType name="cacheType" abstract="true"/>
|
||||||
|
|
||||||
|
<xs:complexType name="inMemoryCacheType">
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="gbcs:cacheType">
|
||||||
|
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
||||||
|
<xs:attribute name="digest" type="xs:token" default="MD5"/>
|
||||||
|
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
|
||||||
|
<xs:attribute name="compression-level" type="xs:byte" default="-1"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="fileSystemCacheType">
|
<xs:complexType name="fileSystemCacheType">
|
||||||
<xs:complexContent>
|
<xs:complexContent>
|
||||||
<xs:extension base="gbcs:cacheType">
|
<xs:extension base="gbcs:cacheType">
|
||||||
@@ -92,17 +103,34 @@
|
|||||||
</xs:choice>
|
</xs:choice>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="usersType">
|
<xs:complexType name="quotaType">
|
||||||
|
<xs:attribute name="calls" type="xs:positiveInteger" use="required"/>
|
||||||
|
<xs:attribute name="period" type="xs:duration" use="required"/>
|
||||||
|
<xs:attribute name="max-available-calls" type="xs:positiveInteger" use="optional"/>
|
||||||
|
<xs:attribute name="initial-available-calls" type="xs:positiveInteger" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="anonymousUserType">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:element name="user" type="gbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
|
<xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="userType">
|
<xs:complexType name="userType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
<xs:attribute name="name" type="xs:token" use="required"/>
|
<xs:attribute name="name" type="xs:token" use="required"/>
|
||||||
<xs:attribute name="password" type="xs:string" use="optional"/>
|
<xs:attribute name="password" type="xs:string" use="optional"/>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="usersType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="user" type="gbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element name="anonymous" type="gbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="groupsType">
|
<xs:complexType name="groupsType">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:element name="group" type="gbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
|
<xs:element name="group" type="gbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
|
||||||
@@ -118,6 +146,7 @@
|
|||||||
</xs:unique>
|
</xs:unique>
|
||||||
</xs:element>
|
</xs:element>
|
||||||
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
|
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
|
||||||
|
<xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
<xs:attribute name="name" type="xs:token"/>
|
<xs:attribute name="name" type="xs:token"/>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
@@ -24,8 +24,8 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
|||||||
protected val random = Random(101325)
|
protected val random = Random(101325)
|
||||||
protected val keyValuePair = newEntry(random)
|
protected val keyValuePair = newEntry(random)
|
||||||
protected val serverPath = "gbcs"
|
protected val serverPath = "gbcs"
|
||||||
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
|
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null)
|
||||||
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
|
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null)
|
||||||
|
|
||||||
abstract protected val users : List<Configuration.User>
|
abstract protected val users : List<Configuration.User>
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import net.woggioni.gbcs.api.Configuration
|
|||||||
import net.woggioni.gbcs.api.Role
|
import net.woggioni.gbcs.api.Role
|
||||||
import net.woggioni.gbcs.common.Xml
|
import net.woggioni.gbcs.common.Xml
|
||||||
import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
|
import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
|
||||||
|
import net.woggioni.gbcs.server.cache.InMemoryCacheConfiguration
|
||||||
import net.woggioni.gbcs.server.configuration.Serializer
|
import net.woggioni.gbcs.server.configuration.Serializer
|
||||||
import net.woggioni.gbcs.server.test.utils.CertificateUtils
|
import net.woggioni.gbcs.server.test.utils.CertificateUtils
|
||||||
import net.woggioni.gbcs.server.test.utils.CertificateUtils.X509Credentials
|
import net.woggioni.gbcs.server.test.utils.CertificateUtils.X509Credentials
|
||||||
@@ -45,8 +46,8 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
|||||||
private lateinit var trustStore: KeyStore
|
private lateinit var trustStore: KeyStore
|
||||||
protected lateinit var ca: X509Credentials
|
protected lateinit var ca: X509Credentials
|
||||||
|
|
||||||
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
|
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null)
|
||||||
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
|
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null)
|
||||||
protected val random = Random(101325)
|
protected val random = Random(101325)
|
||||||
protected val keyValuePair = newEntry(random)
|
protected val keyValuePair = newEntry(random)
|
||||||
private val serverPath : String? = null
|
private val serverPath : String? = null
|
||||||
@@ -158,6 +159,12 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
|||||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||||
digestAlgorithm = "MD5"
|
digestAlgorithm = "MD5"
|
||||||
),
|
),
|
||||||
|
// InMemoryCacheConfiguration(
|
||||||
|
// maxAge = Duration.ofSeconds(3600 * 24),
|
||||||
|
// compressionEnabled = true,
|
||||||
|
// compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||||
|
// digestAlgorithm = "MD5"
|
||||||
|
// ),
|
||||||
Configuration.ClientCertificateAuthentication(
|
Configuration.ClientCertificateAuthentication(
|
||||||
Configuration.TlsCertificateExtractor("CN", "(.*)"),
|
Configuration.TlsCertificateExtractor("CN", "(.*)"),
|
||||||
null
|
null
|
||||||
|
@@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import java.net.http.HttpClient
|
import java.net.http.HttpClient
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
|
||||||
class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
||||||
@@ -19,10 +21,16 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val users = listOf(
|
override val users = listOf(
|
||||||
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),
|
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup), null),
|
||||||
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)),
|
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup), null),
|
||||||
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup)),
|
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup), null),
|
||||||
Configuration.User("", null, setOf(readersGroup))
|
Configuration.User("", null, setOf(readersGroup), null),
|
||||||
|
Configuration.User("user4", hashPassword(PASSWORD), setOf(readersGroup),
|
||||||
|
Configuration.Quota(1, Duration.of(1, ChronoUnit.DAYS), 0, 1)
|
||||||
|
),
|
||||||
|
Configuration.User("user5", hashPassword(PASSWORD), setOf(readersGroup),
|
||||||
|
Configuration.Quota(1, Duration.of(5, ChronoUnit.SECONDS), 0, 1)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -144,4 +152,41 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
|||||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(6)
|
||||||
|
fun getAsAThrottledUser() {
|
||||||
|
val client: HttpClient = HttpClient.newHttpClient()
|
||||||
|
|
||||||
|
val (key, value) = keyValuePair
|
||||||
|
val user = cfg.users.values.find {
|
||||||
|
it.name == "user4"
|
||||||
|
} ?: throw RuntimeException("user4 not found")
|
||||||
|
|
||||||
|
val requestBuilder = newRequestBuilder(key)
|
||||||
|
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||||
|
.GET()
|
||||||
|
|
||||||
|
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
|
Assertions.assertEquals(HttpResponseStatus.TOO_MANY_REQUESTS.code(), response.statusCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(7)
|
||||||
|
fun getAsAThrottledUser2() {
|
||||||
|
val client: HttpClient = HttpClient.newHttpClient()
|
||||||
|
|
||||||
|
val (key, value) = keyValuePair
|
||||||
|
val user = cfg.users.values.find {
|
||||||
|
it.name == "user5"
|
||||||
|
} ?: throw RuntimeException("user5 not found")
|
||||||
|
|
||||||
|
val requestBuilder = newRequestBuilder(key)
|
||||||
|
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||||
|
.GET()
|
||||||
|
|
||||||
|
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
|
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||||
|
Assertions.assertArrayEquals(value, response.body())
|
||||||
|
}
|
||||||
}
|
}
|
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Assertions
|
|||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.ValueSource
|
import org.junit.jupiter.params.provider.ValueSource
|
||||||
|
import org.xml.sax.SAXParseException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
@@ -16,9 +17,9 @@ class ConfigurationTest {
|
|||||||
|
|
||||||
@ValueSource(
|
@ValueSource(
|
||||||
strings = [
|
strings = [
|
||||||
"classpath:net/woggioni/gbcs/server/test/gbcs-default.xml",
|
"classpath:net/woggioni/gbcs/server/test/valid/gbcs-default.xml",
|
||||||
"classpath:net/woggioni/gbcs/server/test/gbcs-memcached.xml",
|
"classpath:net/woggioni/gbcs/server/test/valid/gbcs-memcached.xml",
|
||||||
"classpath:net/woggioni/gbcs/server/test/gbcs-tls.xml",
|
"classpath:net/woggioni/gbcs/server/test/valid/gbcs-tls.xml",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -35,4 +36,20 @@ class ConfigurationTest {
|
|||||||
val parsed = Parser.parse(Xml.parseXml(configFile.toUri().toURL()))
|
val parsed = Parser.parse(Xml.parseXml(configFile.toUri().toURL()))
|
||||||
Assertions.assertEquals(cfg, parsed)
|
Assertions.assertEquals(cfg, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ValueSource(
|
||||||
|
strings = [
|
||||||
|
"classpath:net/woggioni/gbcs/server/test/invalid/invalid-user-ref.xml",
|
||||||
|
"classpath:net/woggioni/gbcs/server/test/invalid/duplicate-anonymous-user.xml",
|
||||||
|
"classpath:net/woggioni/gbcs/server/test/invalid/duplicate-anonymous-user2.xml",
|
||||||
|
"classpath:net/woggioni/gbcs/server/test/invalid/multiple-user-quota.xml",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@ParameterizedTest
|
||||||
|
fun invalidConfigurationTest(configurationUrl: String) {
|
||||||
|
GbcsUrlStreamHandlerFactory.install()
|
||||||
|
Assertions.assertThrows(SAXParseException::class.java) {
|
||||||
|
Xml.parseXml(configurationUrl.toUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -18,9 +18,9 @@ class NoAnonymousUserBasicAuthServerTest : AbstractBasicAuthServerTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val users = listOf(
|
override val users = listOf(
|
||||||
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),
|
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup), null),
|
||||||
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)),
|
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup), null),
|
||||||
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup)),
|
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup), null),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -12,9 +12,9 @@ import java.net.http.HttpResponse
|
|||||||
class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() {
|
class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() {
|
||||||
|
|
||||||
override val users = listOf(
|
override val users = listOf(
|
||||||
Configuration.User("user1", null, setOf(readersGroup)),
|
Configuration.User("user1", null, setOf(readersGroup), null),
|
||||||
Configuration.User("user2", null, setOf(writersGroup)),
|
Configuration.User("user2", null, setOf(writersGroup), null),
|
||||||
Configuration.User("user3", null, setOf(readersGroup, writersGroup)),
|
Configuration.User("user3", null, setOf(readersGroup, writersGroup), null),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -4,6 +4,7 @@ import io.netty.handler.codec.http.HttpResponseStatus
|
|||||||
import net.woggioni.gbcs.api.Configuration
|
import net.woggioni.gbcs.api.Configuration
|
||||||
import net.woggioni.gbcs.common.Xml
|
import net.woggioni.gbcs.common.Xml
|
||||||
import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
|
import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
|
||||||
|
import net.woggioni.gbcs.server.cache.InMemoryCacheConfiguration
|
||||||
import net.woggioni.gbcs.server.configuration.Serializer
|
import net.woggioni.gbcs.server.configuration.Serializer
|
||||||
import net.woggioni.gbcs.server.test.utils.NetworkUtils
|
import net.woggioni.gbcs.server.test.utils.NetworkUtils
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
@@ -23,7 +24,7 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
class NoAuthServerTest : AbstractServerTest() {
|
class NoAuthServerTest : AbstractServerTest() {
|
||||||
|
|
||||||
private lateinit var cacheDir : Path
|
private lateinit var cacheDir: Path
|
||||||
|
|
||||||
private val random = Random(101325)
|
private val random = Random(101325)
|
||||||
private val keyValuePair = newEntry(random)
|
private val keyValuePair = newEntry(random)
|
||||||
@@ -47,8 +48,7 @@ class NoAuthServerTest : AbstractServerTest() {
|
|||||||
),
|
),
|
||||||
emptyMap(),
|
emptyMap(),
|
||||||
emptyMap(),
|
emptyMap(),
|
||||||
FileSystemCacheConfiguration(
|
InMemoryCacheConfiguration(
|
||||||
this.cacheDir,
|
|
||||||
maxAge = Duration.ofSeconds(3600 * 24),
|
maxAge = Duration.ofSeconds(3600 * 24),
|
||||||
compressionEnabled = true,
|
compressionEnabled = true,
|
||||||
digestAlgorithm = "MD5",
|
digestAlgorithm = "MD5",
|
||||||
@@ -63,10 +63,10 @@ class NoAuthServerTest : AbstractServerTest() {
|
|||||||
override fun tearDown() {
|
override fun tearDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
|
fun newRequestBuilder(key: String) = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key"))
|
.uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key"))
|
||||||
|
|
||||||
fun newEntry(random : Random) : Pair<String, ByteArray> {
|
fun newEntry(random: Random): Pair<String, ByteArray> {
|
||||||
val key = ByteArray(0x10).let {
|
val key = ByteArray(0x10).let {
|
||||||
random.nextBytes(it)
|
random.nextBytes(it)
|
||||||
Base64.getUrlEncoder().encodeToString(it)
|
Base64.getUrlEncoder().encodeToString(it)
|
||||||
@@ -95,10 +95,11 @@ class NoAuthServerTest : AbstractServerTest() {
|
|||||||
@Order(2)
|
@Order(2)
|
||||||
fun getWithNoAuthorizationHeader() {
|
fun getWithNoAuthorizationHeader() {
|
||||||
val client: HttpClient = HttpClient.newHttpClient()
|
val client: HttpClient = HttpClient.newHttpClient()
|
||||||
val (key, value ) = keyValuePair
|
val (key, value) = keyValuePair
|
||||||
val requestBuilder = newRequestBuilder(key)
|
val requestBuilder = newRequestBuilder(key)
|
||||||
.GET()
|
.GET()
|
||||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
val response: HttpResponse<ByteArray> =
|
||||||
|
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||||
Assertions.assertArrayEquals(value, response.body())
|
Assertions.assertArrayEquals(value, response.body())
|
||||||
}
|
}
|
||||||
@@ -111,31 +112,23 @@ class NoAuthServerTest : AbstractServerTest() {
|
|||||||
val (key, _) = newEntry(random)
|
val (key, _) = newEntry(random)
|
||||||
val requestBuilder = newRequestBuilder(key).GET()
|
val requestBuilder = newRequestBuilder(key).GET()
|
||||||
|
|
||||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
val response: HttpResponse<ByteArray> =
|
||||||
|
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
@Test
|
||||||
// @Order(4)
|
@Order(4)
|
||||||
// fun manyRequestsTest() {
|
fun traceTest() {
|
||||||
// val client: HttpClient = HttpClient.newHttpClient()
|
val client: HttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build()
|
||||||
//
|
val requestBuilder = newRequestBuilder("").method(
|
||||||
// for(i in 0 until 100000) {
|
"TRACE",
|
||||||
//
|
HttpRequest.BodyPublishers.ofByteArray("sfgsdgfaiousfiuhsd".toByteArray())
|
||||||
// val newEntry = random.nextBoolean()
|
)
|
||||||
// val (key, _) = if(newEntry) {
|
|
||||||
// newEntry(random)
|
val response: HttpResponse<ByteArray> =
|
||||||
// } else {
|
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||||
// keyValuePair
|
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||||
// }
|
println(String(response.body()))
|
||||||
// val requestBuilder = newRequestBuilder(key).GET()
|
}
|
||||||
//
|
|
||||||
// val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
|
||||||
// if(newEntry) {
|
|
||||||
// Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
|
||||||
// } else {
|
|
||||||
// Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
@@ -15,10 +15,10 @@ import java.net.http.HttpResponse
|
|||||||
class TlsServerTest : AbstractTlsServerTest() {
|
class TlsServerTest : AbstractTlsServerTest() {
|
||||||
|
|
||||||
override val users = listOf(
|
override val users = listOf(
|
||||||
Configuration.User("user1", null, setOf(readersGroup)),
|
Configuration.User("user1", null, setOf(readersGroup), null),
|
||||||
Configuration.User("user2", null, setOf(writersGroup)),
|
Configuration.User("user2", null, setOf(writersGroup), null),
|
||||||
Configuration.User("user3", null, setOf(readersGroup, writersGroup)),
|
Configuration.User("user3", null, setOf(readersGroup, writersGroup), null),
|
||||||
Configuration.User("", null, setOf(readersGroup))
|
Configuration.User("", null, setOf(readersGroup), null)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
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">
|
||||||
|
<bind host="127.0.0.1" port="11443"/>
|
||||||
|
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
|
||||||
|
<authorization>
|
||||||
|
<users>
|
||||||
|
<user name="user1" password="password1"/>
|
||||||
|
<user name="user2" password="password2"/>
|
||||||
|
<anonymous>
|
||||||
|
<quota calls="10" period="P3D"/>
|
||||||
|
</anonymous>
|
||||||
|
<anonymous>
|
||||||
|
<quota calls="15" period="P3D"/>
|
||||||
|
</anonymous>
|
||||||
|
</users>
|
||||||
|
</authorization>
|
||||||
|
</gbcs:server>
|
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
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">
|
||||||
|
<bind host="127.0.0.1" port="11443"/>
|
||||||
|
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
|
||||||
|
<authorization>
|
||||||
|
<users>
|
||||||
|
<user name="user1" password="password1"/>
|
||||||
|
<user name="user2" password="password2"/>
|
||||||
|
</users>
|
||||||
|
<groups>
|
||||||
|
<group name="group1">
|
||||||
|
<users>
|
||||||
|
<anonymous/>
|
||||||
|
<user ref="user1"/>
|
||||||
|
<anonymous/>
|
||||||
|
</users>
|
||||||
|
<roles>
|
||||||
|
<reader/>
|
||||||
|
</roles>
|
||||||
|
</group>
|
||||||
|
</groups>
|
||||||
|
</authorization>
|
||||||
|
</gbcs:server>
|
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
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">
|
||||||
|
<bind host="127.0.0.1" port="11443"/>
|
||||||
|
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
|
||||||
|
<authorization>
|
||||||
|
<users>
|
||||||
|
<user name="user1" password="password1"/>
|
||||||
|
<user name="user2" password="password2"/>
|
||||||
|
</users>
|
||||||
|
<groups>
|
||||||
|
<group name="readers">
|
||||||
|
<users>
|
||||||
|
<user ref="user1"/>
|
||||||
|
<user ref="user5"/>
|
||||||
|
</users>
|
||||||
|
<roles>
|
||||||
|
<reader/>
|
||||||
|
</roles>
|
||||||
|
</group>
|
||||||
|
</groups>
|
||||||
|
</authorization>
|
||||||
|
</gbcs:server>
|
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
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">
|
||||||
|
<bind host="127.0.0.1" port="11443"/>
|
||||||
|
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
|
||||||
|
<authorization>
|
||||||
|
<users>
|
||||||
|
<user name="user1" password="password1">
|
||||||
|
<quota calls="10" period="PT20S"/>
|
||||||
|
<quota calls="20" period="PT20S"/>
|
||||||
|
</user>
|
||||||
|
</users>
|
||||||
|
</authorization>
|
||||||
|
</gbcs:server>
|
@@ -11,23 +11,28 @@
|
|||||||
idle-timeout="PT30M"
|
idle-timeout="PT30M"
|
||||||
max-request-size="4096"/>
|
max-request-size="4096"/>
|
||||||
<event-executor use-virtual-threads="false"/>
|
<event-executor use-virtual-threads="false"/>
|
||||||
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
|
<cache xs:type="gbcs:inMemoryCacheType" max-age="P7D"/>
|
||||||
<authorization>
|
<authorization>
|
||||||
<users>
|
<users>
|
||||||
<user name="user1" password="password1"/>
|
<user name="user1" password="password1">
|
||||||
|
<quota calls="3600" period="PT1H"/>
|
||||||
|
</user>
|
||||||
<user name="user2" password="password2"/>
|
<user name="user2" password="password2"/>
|
||||||
<user name="user3" password="password3"/>
|
<user name="user3" password="password3"/>
|
||||||
|
<anonymous>
|
||||||
|
<quota calls="10" period="PT1M"/>
|
||||||
|
</anonymous>
|
||||||
</users>
|
</users>
|
||||||
<groups>
|
<groups>
|
||||||
<group name="readers">
|
<group name="readers">
|
||||||
<users>
|
<users>
|
||||||
<user ref="user1"/>
|
<user ref="user1"/>
|
||||||
<!-- <user ref="user5"/>-->
|
|
||||||
<anonymous/>
|
<anonymous/>
|
||||||
</users>
|
</users>
|
||||||
<roles>
|
<roles>
|
||||||
<reader/>
|
<reader/>
|
||||||
</roles>
|
</roles>
|
||||||
|
<quota calls="10" period="PT1S"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="writers">
|
<group name="writers">
|
||||||
<users>
|
<users>
|
||||||
@@ -45,6 +50,7 @@
|
|||||||
<reader/>
|
<reader/>
|
||||||
<writer/>
|
<writer/>
|
||||||
</roles>
|
</roles>
|
||||||
|
<quota calls="1000" period="P1D"/>
|
||||||
</group>
|
</group>
|
||||||
</groups>
|
</groups>
|
||||||
</authorization>
|
</authorization>
|
@@ -2,9 +2,9 @@ org.gradle.configuration-cache=false
|
|||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
|
|
||||||
gbcs.version = 0.0.8
|
gbcs.version = 0.0.11
|
||||||
|
|
||||||
lys.version = 2025.01.17
|
lys.version = 2025.01.24
|
||||||
|
|
||||||
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||||
docker.registry.url=gitea.woggioni.net
|
docker.registry.url=gitea.woggioni.net
|
||||||
|
Reference in New Issue
Block a user