Compare commits

...

8 Commits

Author SHA1 Message Date
d9e0d82f3c Added server support fro proxy protocol
Some checks failed
CI / build (push) Has been cancelled
2025-12-29 22:08:29 +08:00
9a9cb4ed2c bump Netty 4.2.9 and Kotlin 2.3.0 2025-12-26 17:14:31 +08:00
03a3dafecc updated dependencies
All checks were successful
CI / build (push) Successful in 6m12s
2025-11-21 22:12:27 +08:00
1ffe938c22 update to JDK 25
All checks were successful
CI / build (push) Successful in 4m21s
2025-10-24 07:00:25 +08:00
ce8e93f9d5 updated to netty 4.2.6 and Gradle 9.1.0
All checks were successful
CI / build (push) Successful in 5m51s
2025-09-30 21:40:03 +08:00
94021d94c3 updated Netty to 4.2.4
All checks were successful
CI / build (push) Successful in 3m2s
2025-08-15 10:44:38 +08:00
b3c6f29c0f updated library dependencies
All checks were successful
CI / build (push) Successful in 3m44s
2025-07-29 13:15:42 +08:00
ce7e5bb4a0 added documentation 2025-06-18 09:59:48 +08:00
35 changed files with 273 additions and 36 deletions

View File

@@ -5,7 +5,7 @@ on:
- 'dev'
jobs:
build:
runs-on: woryzen
runs-on: hostinger
steps:
- name: Checkout sources
uses: actions/checkout@v4

View File

@@ -38,7 +38,7 @@ allprojects { subproject ->
withSourcesJar()
modularity.inferModulePath = true
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}
@@ -46,6 +46,7 @@ allprojects { subproject ->
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
testRuntimeOnly catalog.junit.platform.launcher
}
test {

View File

@@ -1,4 +1,4 @@
FROM eclipse-temurin:21-jre-alpine AS base-release
FROM eclipse-temurin:25-jre-alpine AS base-release
RUN adduser -D luser
USER luser
WORKDIR /home/luser
@@ -33,6 +33,7 @@ ENTRYPOINT ["/usr/bin/rbcs-cli", "-XX:MaximumHeapSizePercent=70"]
FROM debian:12-slim AS release-jlink
RUN mkdir -p /usr/share/java/rbcs
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-cli*.tar -C /usr/share/java/rbcs
RUN chmod 755 /usr/share/java/rbcs/bin/*
ADD --chmod=755 rbcs-cli.sh /usr/local/bin/rbcs-cli
RUN adduser -u 1000 luser
USER luser

View File

@@ -5,7 +5,7 @@ There are 3 image flavours:
- native
The `vanilla` image only contains the envelope
jar file with no plugins and is based on `eclipse-temurin:21-jre-alpine`
jar file with no plugins and is based on `eclipse-temurin:25-jre-alpine`
The `memcache` image is similar to the `vanilla` image, except that it also contains
the `rbcs-server-memcache` plugin in the `plugins` folder, use this image if you don't want to use the `native`

View File

@@ -1,3 +1,3 @@
#!/bin/sh
DIR=/usr/share/java/rbcs
$DIR/bin/java $JAVA_OPTS -m net.woggioni.rbcs.cli "$@"
exec $DIR/bin/java $JAVA_OPTS -m net.woggioni.rbcs.cli $@

View File

@@ -2,9 +2,9 @@ org.gradle.configuration-cache=false
org.gradle.parallel=true
org.gradle.caching=true
rbcs.version = 0.3.1
rbcs.version = 0.3.7
lys.version = 2025.06.10
lys.version = 2025.12.27
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
docker.registry.url=gitea.woggioni.net

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -205,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

3
gradlew.bat vendored
View File

@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -4,10 +4,12 @@ package net.woggioni.rbcs.api;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
import net.woggioni.rbcs.common.Cidr;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -16,6 +18,8 @@ import java.util.stream.Collectors;
public class Configuration {
String host;
int port;
boolean proxyProtocolEnabled;
List<Cidr> trustedProxyIPs;
int incomingConnectionsBacklogSize;
String serverPath;
@NonNull
@@ -30,6 +34,7 @@ public class Configuration {
Authentication authentication;
Tls tls;
@Value
public static class RateLimiter {
boolean delayRequest;
@@ -140,6 +145,8 @@ public class Configuration {
public static Configuration of(
String host,
int port,
boolean proxyProtocolEnabled,
List<Cidr> trustedProxyIPs,
int incomingConnectionsBacklogSize,
String serverPath,
EventExecutor eventExecutor,
@@ -154,6 +161,8 @@ public class Configuration {
return new Configuration(
host,
port,
proxyProtocolEnabled,
trustedProxyIPs,
incomingConnectionsBacklogSize,
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
eventExecutor,

View File

@@ -89,7 +89,7 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.E
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration"
@@ -107,7 +107,7 @@ tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfi
nativeImage {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = mainClassName
@@ -126,7 +126,7 @@ Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME,
Provider<JlinkTask> jlinkTaskProvider = tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.GRAAL_VM
}

View File

@@ -183,9 +183,6 @@
"name":"io.netty.channel.SimpleChannelInboundHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.channel.embedded.EmbeddedChannel$2"
},
{
"name":"io.netty.channel.pool.SimpleChannelPool$1"
},
@@ -195,7 +192,7 @@
},
{
"name":"io.netty.handler.codec.ByteToMessageDecoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.MessageAggregator",
@@ -214,7 +211,8 @@
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.compression.JdkZlibDecoder"
"name":"io.netty.handler.codec.compression.JdkZlibDecoder",
"methods":[{"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.compression.JdkZlibEncoder",
@@ -227,6 +225,10 @@
"name":"io.netty.handler.codec.http.HttpContentDecoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.http.HttpContentDecoder$ByteBufForwarder",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.http.HttpContentDecompressor"
},
@@ -278,9 +280,13 @@
"name":"io.netty.util.concurrent.DefaultPromise",
"fields":[{"name":"result"}]
},
{
"name":"io.netty.util.concurrent.MpscIntQueue$MpscAtomicIntegerArrayQueue",
"fields":[{"name":"consumerIndex"}, {"name":"producerIndex"}, {"name":"producerLimit"}]
},
{
"name":"io.netty.util.concurrent.SingleThreadEventExecutor",
"fields":[{"name":"state"}, {"name":"threadProperties"}]
"fields":[{"name":"accumulatedActiveTimeNanos"}, {"name":"consecutiveBusyCycles"}, {"name":"consecutiveIdleCycles"}, {"name":"state"}, {"name":"threadProperties"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields",
@@ -566,7 +572,7 @@
},
{
"name":"net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.ServerHandler",

View File

@@ -36,6 +36,8 @@
"pattern":"\\Qnet/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
}, {
"pattern":"jdk.jfr:\\Qjdk/jfr/internal/types/metadata.bin\\E"
}]},
"bundles":[{
"name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message",

View File

@@ -97,6 +97,8 @@ object GraalNativeImageConfiguration {
val serverConfiguration = Configuration(
"127.0.0.1",
serverPort,
false,
emptyList(),
100,
null,
Configuration.EventExecutor(true),

View File

@@ -12,6 +12,7 @@ import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource
import org.junit.jupiter.params.support.ParameterDeclarations
class RetryTest {
@@ -23,7 +24,10 @@ class RetryTest {
)
class TestArguments : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
override fun provideArguments(
parameters: ParameterDeclarations,
context: ExtensionContext
): Stream<out Arguments> {
return Stream.of(
TestArgs(
seed = 101325,

View File

@@ -0,0 +1,62 @@
package net.woggioni.rbcs.common
import java.net.InetAddress
data class Cidr private constructor(
val networkAddress: InetAddress,
val prefixLength: Int
) {
companion object {
fun from(cidr: String) : Cidr {
val separator = cidr.indexOf("/")
if(separator < 0) {
throw IllegalArgumentException("Invalid CIDR format: $cidr")
}
val networkAddress = InetAddress.getByName(cidr.substring(0, separator))
val prefixLength = cidr.substring(separator + 1, cidr.length).toInt()
// Validate prefix length
val maxPrefix = if (networkAddress.address.size == 4) 32 else 128
require(prefixLength in 0..maxPrefix) { "Invalid prefix length: $prefixLength" }
return Cidr(networkAddress, prefixLength)
}
}
fun contains(address: InetAddress): Boolean {
val networkBytes = networkAddress.address
val addressBytes = address.address
if (networkBytes.size != addressBytes.size) {
return false
}
// Calculate how many full bytes and remaining bits to check
val fullBytes = prefixLength / 8
val remainingBits = prefixLength % 8
// Check full bytes
for (i in 0..<fullBytes) {
if (networkBytes[i] != addressBytes[i]) {
return false
}
}
// Check remaining bits if any
if (remainingBits > 0 && fullBytes < networkBytes.size) {
val mask = (0xFF shl (8 - remainingBits)).toByte()
if ((networkBytes[fullBytes].toInt() and mask.toInt()) != (addressBytes[fullBytes].toInt() and mask.toInt())) {
return false
}
}
return true
}
override fun toString(): String {
return networkAddress.hostAddress + "/" + prefixLength
}
}

View File

@@ -81,9 +81,6 @@ inline fun Logger.log(level: Level, channel: Channel, crossinline messageBuilder
)
withMDC(params) {
val builder = makeLoggingEventBuilder(level)
// for ((key, value) in params) {
// builder.addKeyValue(key, value)
// }
messageBuilder(builder)
builder.log()
}

View File

@@ -19,6 +19,8 @@ to `memcacheCacheType`.
The plugins currently supports the following configuration attributes:
- `max-age`: the amount of time cache entries will be retained on memcache
- `key-prefix`: a string that will be prepended to all the keys inserted in memcache,
useful in case the caching backend is shared with other applications
- `digest`: digest algorithm to use on the key before submission
to memcache (optional, no digest is applied if omitted)
- `compression`: compression algorithm to apply to cache values before,
@@ -35,6 +37,7 @@ The plugins currently supports the following configuration attributes:
...
<cache xs:type="rbcs-memcache:memcacheCacheType"
max-age="P7D"
key-prefix="rbcs-"
digest="SHA-256"
compression-mode="deflate"
compression-level="6">

View File

@@ -12,6 +12,7 @@ dependencies {
implementation catalog.netty.handler
implementation catalog.netty.buffer
implementation catalog.netty.transport
implementation catalog.netty.codec.haproxy
api project(':rbcs-common')
api project(':rbcs-api')

View File

@@ -16,6 +16,7 @@ module net.woggioni.rbcs.server {
requires io.netty.buffer;
requires io.netty.common;
requires io.netty.codec;
requires io.netty.codec.haproxy;
requires org.slf4j;
exports net.woggioni.rbcs.server;

View File

@@ -20,6 +20,7 @@ import io.netty.channel.socket.nio.NioDatagramChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.compression.CompressionOptions
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder
import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.HttpContentCompressor
import io.netty.handler.codec.http.HttpDecoderConfig
@@ -57,6 +58,7 @@ import javax.net.ssl.SSLPeerUnverifiedException
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.Cidr
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS.getTrustManager
@@ -73,6 +75,7 @@ import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.exception.ExceptionHandler
import net.woggioni.rbcs.server.handler.MaxRequestSizeHandler
import net.woggioni.rbcs.server.handler.ProxyProtocolHandler
import net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler
import net.woggioni.rbcs.server.handler.ServerHandler
import net.woggioni.rbcs.server.throttling.BucketManager
@@ -85,6 +88,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
val clientIp: AttributeKey<InetSocketAddress> = AttributeKey.valueOf("client-ip")
val DEFAULT_CONFIGURATION_URL by lazy { "jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/rbcs-default.xml".toUrl() }
private const val SSL_HANDLER_NAME = "sslHandler"
@@ -234,6 +238,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
else ClientAuth.OPTIONAL
} ?: ClientAuth.NONE
clientAuth(clientAuth)
}.build()
}
}
@@ -259,6 +264,9 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
else -> null
}
private val proxyProtocolEnabled: Boolean = cfg.isProxyProtocolEnabled
private val trustedProxyIPs: List<Cidr> = cfg.trustedProxyIPs
private val sslContext: SslContext? = cfg.tls?.let(Companion::createSslCtx)
private fun userExtractor(authentication: Configuration.ClientCertificateAuthentication) =
@@ -290,6 +298,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
}
override fun initChannel(ch: Channel) {
ch.attr(clientIp).set(ch.remoteAddress() as InetSocketAddress)
log.debug {
"Created connection ${ch.id().asShortText()} with ${ch.remoteAddress()}"
}
@@ -338,6 +347,10 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
}
}
})
if(proxyProtocolEnabled) {
pipeline.addLast(HAProxyMessageDecoder())
pipeline.addLast(ProxyProtocolHandler(trustedProxyIPs))
}
sslContext?.newHandler(ch.alloc())?.also {
pipeline.addLast(SSL_HANDLER_NAME, it)
}

View File

@@ -72,5 +72,4 @@ abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer
ReferenceCountUtil.release(msg)
ctx.writeAndFlush(NOT_AUTHORIZED.retainedDuplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
}

View File

@@ -128,4 +128,4 @@ class InMemoryCache(
}
}
}
}
}

View File

@@ -2,8 +2,6 @@ package net.woggioni.rbcs.server.cache
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterOutputStream

View File

@@ -16,6 +16,7 @@ import net.woggioni.rbcs.api.Configuration.TrustStore
import net.woggioni.rbcs.api.Configuration.User
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.Cidr
import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
@@ -38,6 +39,8 @@ object Parser {
var cache: Cache? = null
var host = "127.0.0.1"
var port = 11080
var proxyProtocolEnabled = false
var trustedProxies = emptyList<Cidr>()
var users: Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
var groups = emptyMap<String, Group>()
var tls: Tls? = null
@@ -98,9 +101,23 @@ object Parser {
"bind" -> {
host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
port = Integer.parseInt(child.renderAttribute("port"))
proxyProtocolEnabled = child.renderAttribute("proxy-protocol")
?.let(String::toBoolean) ?: false
incomingConnectionsBacklogSize = child.renderAttribute("incoming-connections-backlog-size")
?.let(Integer::parseInt)
?: 1024
for(grandChild in child.asIterable()) {
when(grandChild.localName) {
"trusted-proxies" -> {
trustedProxies = parseTrustedProxies(grandChild)
}
}
}
child.asIterable().filter {
it.localName == "trusted-proxies"
}.firstOrNull()?.let(::parseTrustedProxies)
}
"cache" -> {
@@ -195,6 +212,8 @@ object Parser {
return Configuration.of(
host,
port,
proxyProtocolEnabled,
trustedProxies,
incomingConnectionsBacklogSize,
serverPath,
eventExecutor,
@@ -217,6 +236,15 @@ object Parser {
}
}.toSet()
private fun parseTrustedProxies(root: Element) = root.asIterable().asSequence().map {
when (it.localName) {
"allow" -> it.renderAttribute("cidr")
?.let(Cidr::from)
?: throw ConfigurationException("Missing 'cidr' attribute")
else -> throw ConfigurationException("Unrecognized tag '${it.localName}'")
}
}.toList()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
when (it.localName) {
"user" -> it.renderAttribute("ref")

View File

@@ -33,6 +33,17 @@ object Serializer {
attr("host", conf.host)
attr("port", conf.port.toString())
attr("incoming-connections-backlog-size", conf.incomingConnectionsBacklogSize.toString())
attr("proxy-protocol", conf.isProxyProtocolEnabled.toString())
if (conf.trustedProxyIPs.isNotEmpty()) {
node("trusted-proxies") {
for(trustedProxy in conf.trustedProxyIPs) {
node("allow") {
attr("cidr", trustedProxy.toString())
}
}
}
}
}
node("connection") {
conf.connection.let { connection ->

View File

@@ -0,0 +1,40 @@
package net.woggioni.rbcs.server.handler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.haproxy.HAProxyMessage
import java.net.InetAddress
import java.net.InetSocketAddress
import net.woggioni.rbcs.common.Cidr
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.trace
import net.woggioni.rbcs.server.RemoteBuildCacheServer
class ProxyProtocolHandler(private val trustedProxyIPs : List<Cidr>) : SimpleChannelInboundHandler<HAProxyMessage>() {
companion object {
private val log = createLogger<ProxyProtocolHandler>()
}
override fun channelRead0(
ctx: ChannelHandlerContext,
msg: HAProxyMessage
) {
val sourceAddress = ctx.channel().remoteAddress()
if (sourceAddress is InetSocketAddress &&
trustedProxyIPs.isEmpty() ||
trustedProxyIPs.any { it.contains((sourceAddress as InetSocketAddress).address) }) {
val proxiedClientAddress = InetSocketAddress(
InetAddress.ofLiteral(msg.sourceAddress()),
msg.sourcePort()
)
if(log.isTraceEnabled) {
log.trace {
"Received proxied request from $sourceAddress forwarded for $proxiedClientAddress"
}
}
ctx.channel().attr(RemoteBuildCacheServer.clientIp).set(proxiedClientAddress)
}
}
}

View File

@@ -197,7 +197,8 @@ class ThrottlingHandler(
}
}
if (user == null && groups.isEmpty()) {
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
val clientAddress = ctx.channel().attr<InetSocketAddress>(RemoteBuildCacheServer.clientIp).get()
bucketManager.getBucketByAddress(clientAddress)?.let(buckets::add)
}
var nextAttempt = -1L

View File

@@ -62,6 +62,9 @@
</xs:complexType>
<xs:complexType name="bindType">
<xs:sequence minOccurs="0">
<xs:element name="trusted-proxies" type="rbcs:trustedProxiesType"/>
</xs:sequence>
<xs:attribute name="host" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>Server bind address</xs:documentation>
@@ -72,6 +75,12 @@
<xs:documentation>Server port number</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="proxy-protocol" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Enable proxy protocol</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024">
<xs:annotation>
<xs:documentation>
@@ -83,6 +92,16 @@
</xs:attribute>
</xs:complexType>
<xs:complexType name="trustedProxiesType">
<xs:sequence minOccurs="0">
<xs:element name="allow" type="rbcs:allowType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="allowType">
<xs:attribute name="cidr" type="rbcs:cidr"/>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
<xs:annotation>
@@ -681,4 +700,20 @@
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cidrIPv4">
<xs:restriction base="xs:string">
<xs:pattern value="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[12]?[0-9])" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cidrIPv6">
<xs:restriction base="xs:string">
<xs:pattern value="((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(:([0-9A-Fa-f]{1,4}){1,7}|:)))(%[\p{L}\p{N}_-]+)?\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cidr">
<xs:union memberTypes="rbcs:cidrIPv4 rbcs:cidrIPv6" />
</xs:simpleType>
</xs:schema>

View File

@@ -34,6 +34,8 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
cfg = Configuration.of(
"127.0.0.1",
getFreePort(),
false,
emptyList(),
50,
serverPath,
Configuration.EventExecutor(false),

View File

@@ -140,6 +140,9 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
cfg = Configuration(
"127.0.0.1",
getFreePort(),
false,
emptyList(),
100,
serverPath,
Configuration.EventExecutor(false),

View File

@@ -34,6 +34,8 @@ class NoAuthServerTest : AbstractServerTest() {
cfg = Configuration(
"127.0.0.1",
getFreePort(),
false,
emptyList(),
100,
serverPath,
Configuration.EventExecutor(false),

View File

@@ -2,7 +2,13 @@
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd">
<bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="22"/>
<bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="22" proxy-protocol="true">
<trusted-proxies>
<allow cidr="192.168.0.11/32"/>
<allow cidr="::1/128"/>
<allow cidr="fda7:9b54:5678::2f9/128"/>
</trusted-proxies>
</bind>
<connection
read-idle-timeout="PT10M"
write-idle-timeout="PT11M"

View File

@@ -1,5 +1,16 @@
pluginManagement {
repositories {
// mavenLocal {
// content {
// includeGroup 'net.woggioni.gradle'
// includeGroup 'net.woggioni.gradle.jpms-check'
// includeGroup 'net.woggioni.gradle.lombok'
// includeGroup 'net.woggioni.gradle.jdeps'
// includeGroup 'net.woggioni.gradle.sambal'
// includeGroup 'net.woggioni.gradle.graalvm.jlink'
// includeGroup 'net.woggioni.gradle.graalvm.native-image'
// }
// }
maven {
url = getProperty('gitea.maven.url')
}
@@ -19,6 +30,8 @@ dependencyResolutionManagement {
versionCatalogs {
catalog {
from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version')
// version('my-gradle-plugins', '2025.04.16')
// version('junit', '5.12.0')
}
}
}
@@ -33,3 +46,4 @@ include 'rbcs-client'
include 'rbcs-server'
include 'rbcs-servlet'
include 'docker'
//include 'bug'