Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
43fdf131fa
|
|||
|
b58462a085
|
|||
|
e9f9f23f91
|
|||
|
5854a632f8
|
|||
|
9a9cb4ed2c
|
|||
|
03a3dafecc
|
|||
|
1ffe938c22
|
@@ -9,8 +9,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Execute Gradle build
|
||||
run: ./gradlew build
|
||||
- name: Prepare Docker image build
|
||||
@@ -18,12 +16,6 @@ jobs:
|
||||
- name: Get project version
|
||||
id: retrieve-version
|
||||
run: ./gradlew -q version >> "$GITHUB_OUTPUT"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker-container
|
||||
- name: Login to Gitea container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -34,6 +26,7 @@ jobs:
|
||||
name: Build rbcs Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
@@ -41,11 +34,11 @@ jobs:
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla-dev
|
||||
target: release-vanilla
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs memcache Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
@@ -53,12 +46,11 @@ jobs:
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache-dev
|
||||
target: release-memcache
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs native Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
@@ -70,6 +62,7 @@ jobs:
|
||||
name: Build rbcs jlink Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
|
||||
@@ -5,12 +5,10 @@ on:
|
||||
- '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: hostinger
|
||||
runs-on: woryzen
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Execute Gradle build
|
||||
run: ./gradlew build
|
||||
- name: Prepare Docker image build
|
||||
@@ -18,12 +16,6 @@ jobs:
|
||||
- name: Get project version
|
||||
id: retrieve-version
|
||||
run: ./gradlew -q version >> "$GITHUB_OUTPUT"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker-container
|
||||
- name: Login to Gitea container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -34,6 +26,7 @@ jobs:
|
||||
name: Build rbcs Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
@@ -42,11 +35,11 @@ jobs:
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-vanilla
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs memcache Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
@@ -57,12 +50,11 @@ jobs:
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-memcache
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs native Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
@@ -75,6 +67,7 @@ jobs:
|
||||
name: Build rbcs jlink Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
builder: "multiplatform-builder"
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
|
||||
173
AGENTS.md
Normal file
173
AGENTS.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# AGENTS.md — Coding Agent Guide for RBCS
|
||||
|
||||
## Project Overview
|
||||
|
||||
RBCS (Remote Build Cache Server) is a Kotlin/Java multi-module Gradle project built on Netty.
|
||||
It serves as a remote build cache for Gradle and Maven. Java is used for the public API layer
|
||||
(with Lombok); Kotlin is used for all implementation modules. All modules use JPMS
|
||||
(`module-info.java`). The project is built on Netty with Java 25.
|
||||
|
||||
**Modules:** `rbcs-api`, `rbcs-common`, `rbcs-server`, `rbcs-server-memcache`, `rbcs-client`,
|
||||
`rbcs-cli`, `rbcs-servlet`, `docker`
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
./gradlew build # Full build: compile + test + assemble
|
||||
./gradlew assemble # Build JARs without running tests
|
||||
./gradlew compileJava compileKotlin # Compile only
|
||||
./gradlew clean # Clean build outputs
|
||||
./gradlew :rbcs-server:compileKotlin # Compile a single module
|
||||
./gradlew -q version # Print project version
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
```bash
|
||||
./gradlew test # Run all tests (all modules)
|
||||
./gradlew :rbcs-server:test # Run tests for a single module
|
||||
./gradlew :rbcs-server:test --tests "net.woggioni.rbcs.server.test.NoAuthServerTest"
|
||||
# Run a single test class
|
||||
./gradlew :rbcs-server:test --tests "net.woggioni.rbcs.server.test.NoAuthServerTest.putWithNoAuthorizationHeader"
|
||||
# Run a single test method
|
||||
./gradlew test --tests "*TlsServer*" # Run tests matching a pattern
|
||||
./gradlew :rbcs-server:jacocoTestReport # Generate code coverage (rbcs-server only)
|
||||
```
|
||||
|
||||
**Test framework:** JUnit 5 (Jupiter). Tests are integration-style — they start real Netty
|
||||
servers and use `java.net.http.HttpClient` to make HTTP requests. Netty leak detection is
|
||||
set to `PARANOID` in test configurations.
|
||||
|
||||
**Test locations:** `src/test/kotlin/` and `src/test/java/` following standard Gradle layout.
|
||||
Test resources (XML configs, logback) are in `src/test/resources/`.
|
||||
|
||||
## Lint / Format
|
||||
|
||||
No linting or formatting tools are configured (no Checkstyle, Detekt, ktlint, Spotless, or
|
||||
similar). Follow the existing code style described below.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Language Split
|
||||
|
||||
- **Java** for `rbcs-api` (public API consumed by Java clients) with Lombok annotations
|
||||
- **Kotlin** for all implementation modules
|
||||
- **`module-info.java`** in every module (JPMS)
|
||||
|
||||
### Import Ordering (Kotlin)
|
||||
|
||||
Three groups separated by blank lines, each alphabetically sorted:
|
||||
1. External/third-party (`io.netty.*`, `org.slf4j.*`)
|
||||
2. Java standard library (`java.*`, `javax.*`)
|
||||
3. Internal project (`net.woggioni.rbcs.*`)
|
||||
|
||||
Import aliases are rare; used only to resolve conflicts:
|
||||
`import io.netty.util.concurrent.Future as NettyFuture`
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Element | Convention | Examples |
|
||||
|---------------------|-------------------------|---------------------------------------------------|
|
||||
| Classes/interfaces | PascalCase | `RemoteBuildCacheServer`, `CacheHandler` |
|
||||
| Abstract classes | `Abstract` prefix | `AbstractServerTest`, `AbstractNettyHttpAuthenticator` |
|
||||
| Functions/methods | camelCase | `loadConfiguration()`, `sendShutdownSignal()` |
|
||||
| Variables/properties| camelCase | `bucketManager`, `sslContext` |
|
||||
| Constants | SCREAMING_SNAKE_CASE | `SSL_HANDLER_NAME`, `RBCS_NAMESPACE_URI` |
|
||||
| Handler names | `val NAME = ...::class.java.name` in companion object |
|
||||
| Packages | lowercase dot-separated | `net.woggioni.rbcs.server.throttling` |
|
||||
| Enum values | PascalCase | `Role.Reader`, `Role.Writer` |
|
||||
| Kotlin files | PascalCase matching primary class; lowercase for files with only top-level functions |
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Custom unchecked exception hierarchy rooted at `RbcsException extends RuntimeException`
|
||||
- Domain subclasses: `CacheException`, `ConfigurationException`, `ContentTooLargeException`
|
||||
- Async errors propagated via `CompletableFuture.completeExceptionally()`
|
||||
- Synchronous errors thrown directly: `throw IllegalArgumentException(...)`
|
||||
- Kotlin null safety idioms preferred over null checks: `?.let`, `?:`, `takeIf`
|
||||
- `ExceptionHandler` maps exception types to HTTP responses via exhaustive `when`
|
||||
|
||||
### Type Annotations
|
||||
|
||||
- **Kotlin:** Heavy use of type inference for local variables. Explicit types required on:
|
||||
- Class properties: `private val sslContext: SslContext?`
|
||||
- Non-trivial return types: `fun run(): ServerHandle`
|
||||
- `lateinit var` in tests: `protected lateinit var cfg: Configuration`
|
||||
- **Java:** Lombok `@Value` for immutable data classes; modern pattern matching with `instanceof`
|
||||
- No `typealias` declarations are used in this project
|
||||
|
||||
### Async Patterns
|
||||
|
||||
- Primary abstraction: `CompletableFuture<T>` — **no Kotlin coroutines**
|
||||
- Netty event-driven callbacks (`ChannelFuture`, `GenericFutureListener`)
|
||||
- Custom `AsyncCloseable` interface wraps `CompletableFuture<Void>` for async shutdown
|
||||
- Retry logic uses `CompletableFuture` composition with exponential backoff + jitter
|
||||
- Virtual threads used selectively (background GC, configurable event executors)
|
||||
- Connection pooling via Netty `FixedChannelPool`
|
||||
|
||||
### Logging
|
||||
|
||||
- SLF4J via custom Kotlin lazy-evaluation extension functions (defined in `rbcs-common`)
|
||||
- Logger creation: `private val log = createLogger<ClassName>()` (in companion object)
|
||||
- Lazy log calls: `log.debug { "message with ${interpolation}" }` (lambda only evaluated if enabled)
|
||||
- Channel-aware variants: `log.debug(ctx) { "message" }` (adds MDC: channel-id, remote-address)
|
||||
- Java classes use Lombok `@Slf4j`
|
||||
- String templates with `${}` for Kotlin log messages
|
||||
|
||||
### Kotlin Idioms
|
||||
|
||||
- `apply` as builder pattern: `ServerBootstrap().apply { group(...); option(...) }`
|
||||
- `also` for side effects, `let` for transformation, `run` for scoping
|
||||
- Trailing commas in constructor parameter lists and multi-line function calls
|
||||
- `sealed class`/`sealed interface` for algebraic types (e.g., `OperationOutcome`, `Authentication`)
|
||||
- `data class` for value types; `companion object` for static members and constants
|
||||
- No trailing semicolons
|
||||
|
||||
### Java Idioms (API module)
|
||||
|
||||
- Lombok annotations: `@Value`, `@Getter`, `@RequiredArgsConstructor`, `@EqualsAndHashCode.Include`
|
||||
- `sealed interface` with `final` permitted subtypes (e.g., `CacheMessage`)
|
||||
- `@FunctionalInterface` on single-method interfaces
|
||||
- JPMS `module-info.java` in every module with `requires`, `exports`, `uses`, `provides`
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
- `@TestInstance(Lifecycle.PER_CLASS)` — single instance per test class
|
||||
- `@TestMethodOrder(MethodOrderer.OrderAnnotation)` with `@Order(n)` for sequential execution
|
||||
- Abstract base class hierarchy: `AbstractServerTest` → `AbstractBasicAuthServerTest` → concrete
|
||||
- Server lifecycle in `@BeforeAll` / `@AfterAll` (start/stop real Netty server)
|
||||
- `@TempDir` for temporary directories
|
||||
- `@ParameterizedTest` with `@ValueSource` and `@ArgumentsSource` for parameterized tests
|
||||
- Assertions via `org.junit.jupiter.api.Assertions` (`assertEquals`, `assertArrayEquals`)
|
||||
- No mocking framework — all tests are integration-style against real servers
|
||||
|
||||
### Documentation
|
||||
|
||||
Minimal doc comments in the codebase. Inline comments used sparingly for clarification.
|
||||
When adding new public API, follow existing style — doc comments are not enforced but
|
||||
are welcome on complex logic.
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```
|
||||
rbcs-api → (standalone, Lombok, Netty types)
|
||||
rbcs-common → Netty, Kotlin stdlib
|
||||
rbcs-server → rbcs-api, rbcs-common
|
||||
rbcs-server-memcache → rbcs-api, rbcs-common
|
||||
rbcs-client → rbcs-api, rbcs-common
|
||||
rbcs-cli → rbcs-client, rbcs-server (Picocli for CLI)
|
||||
rbcs-servlet → rbcs-api, rbcs-common (Jakarta Servlet/CDI)
|
||||
docker → rbcs-cli, rbcs-server-memcache
|
||||
```
|
||||
|
||||
## Plugin System
|
||||
|
||||
Cache backends use the `ServiceLoader` pattern via JPMS `provides`/`uses` directives.
|
||||
To add a new cache provider, implement `CacheProvider<T extends Configuration.Cache>` and
|
||||
register it in your module's `module-info.java`.
|
||||
|
||||
## Configuration
|
||||
|
||||
- XML-based with XSD schema validation
|
||||
- Schemas in `src/main/resources/.../schema/*.xsd`
|
||||
- Default config loaded from JPMS resources: `jpms://net.woggioni.rbcs.server/...`
|
||||
@@ -28,7 +28,7 @@ allprojects { subproject ->
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
pluginManager.withPlugin('java-library') {
|
||||
pluginManager.withPlugin('java') {
|
||||
|
||||
ext {
|
||||
jpmsModuleName = subproject.group + '.' + subproject.name.replace('-', '.')
|
||||
@@ -38,7 +38,7 @@ allprojects { subproject ->
|
||||
withSourcesJar()
|
||||
modularity.inferModulePath = true
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ allprojects { subproject ->
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
modularity.inferModulePath = true
|
||||
options.release = 21
|
||||
options.release = 25
|
||||
}
|
||||
|
||||
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
|
||||
@@ -80,7 +80,7 @@ allprojects { subproject ->
|
||||
|
||||
pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) {
|
||||
tasks.withType(KotlinCompile.class) {
|
||||
compilerOptions.jvmTarget = JvmTarget.JVM_21
|
||||
compilerOptions.jvmTarget = JvmTarget.JVM_25
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,21 @@ Configures server socket settings.
|
||||
**Attributes:**
|
||||
- `host` (required): Server bind address
|
||||
- `port` (required): Server port number
|
||||
- `proxy-protocol` (optional, default: false): Enable [HAProxy proxy protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. When enabled, the server decodes proxy protocol headers to extract the real client IP address from proxied connections.
|
||||
- `incoming-connections-backlog-size` (optional, default: 1024): Maximum queue length for incoming connection indications
|
||||
|
||||
**Child Elements:**
|
||||
|
||||
##### `<trusted-proxies>`
|
||||
Restricts which proxy servers are trusted to provide accurate client IP information via the proxy protocol. Only used when `proxy-protocol` is set to `true`.
|
||||
|
||||
If omitted or empty, all proxies are trusted. When specified, only connections originating from the listed CIDR ranges will have their forwarded client IP honored.
|
||||
|
||||
- Contains `<allow>` elements:
|
||||
|
||||
**Attributes:**
|
||||
- `cidr` (required): An IPv4 or IPv6 CIDR range identifying a trusted proxy address (e.g. `192.168.0.0/24`, `::1/128`)
|
||||
|
||||
#### `<connection>`
|
||||
Configures connection handling parameters.
|
||||
|
||||
@@ -127,7 +140,12 @@ Configures TLS encryption.
|
||||
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="0.0.0.0" port="8080" incoming-connections-backlog-size="1024"/>
|
||||
<bind host="0.0.0.0" port="8080" incoming-connections-backlog-size="1024" proxy-protocol="true">
|
||||
<trusted-proxies>
|
||||
<allow cidr="192.168.0.11/32"/>
|
||||
<allow cidr="::1/128"/>
|
||||
</trusted-proxies>
|
||||
</bind>
|
||||
<connection
|
||||
max-request-size="67108864"
|
||||
idle-timeout="PT10S"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 $@
|
||||
@@ -2,9 +2,9 @@ org.gradle.configuration-cache=false
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
rbcs.version = 0.3.4
|
||||
rbcs.version = 0.3.7
|
||||
|
||||
lys.version = 2025.09.30
|
||||
lys.version = 2026.02.19
|
||||
|
||||
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||
docker.registry.url=gitea.woggioni.net
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
5
gradlew
vendored
5
gradlew
vendored
@@ -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="\\\"\\\""
|
||||
|
||||
|
||||
# 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" )
|
||||
|
||||
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
3
gradlew.bat
vendored
3
gradlew.bat
vendored
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
Args=-O3 -march=x86-64-v2 --gc=serial --install-exit-handlers --initialize-at-run-time=io.netty --enable-url-protocols=jpms --initialize-at-build-time=net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory,net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory$JpmsHandler
|
||||
Args=-O3 -march=x86-64-v2 --gc=serial --initialize-at-run-time=io.netty --enable-url-protocols=jpms -H:+UnlockExperimentalVMOptions -H:+SharedArenaSupport --initialize-at-build-time=net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory,net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory$JpmsHandler
|
||||
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -97,6 +97,8 @@ object GraalNativeImageConfiguration {
|
||||
val serverConfiguration = Configuration(
|
||||
"127.0.0.1",
|
||||
serverPort,
|
||||
false,
|
||||
emptyList(),
|
||||
100,
|
||||
null,
|
||||
Configuration.EventExecutor(true),
|
||||
|
||||
62
rbcs-common/src/main/kotlin/net/woggioni/rbcs/common/Cidr.kt
Normal file
62
rbcs-common/src/main/kotlin/net/woggioni/rbcs/common/Cidr.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import java.net.InetAddress
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CidrTest {
|
||||
class CidrTest {
|
||||
@Test
|
||||
fun test() {
|
||||
val cidr = Cidr.from("2a02:4780:12:368b::1/128")
|
||||
Assertions.assertTrue {
|
||||
cidr.contains(InetAddress.ofLiteral("2a02:4780:12:368b::1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -72,5 +72,4 @@ abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer
|
||||
ReferenceCountUtil.release(msg)
|
||||
ctx.writeAndFlush(NOT_AUTHORIZED.retainedDuplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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) }.also {
|
||||
if(!it && log.isTraceEnabled) {
|
||||
log.trace {
|
||||
"Received a proxied connection request from $sourceAddress which is not a trusted proxy address, " +
|
||||
"the proxy server address will be used instead"
|
||||
}
|
||||
}
|
||||
}) {
|
||||
val proxiedClientAddress = InetSocketAddress(
|
||||
InetAddress.ofLiteral(msg.sourceAddress()),
|
||||
msg.sourcePort()
|
||||
)
|
||||
if(log.isTraceEnabled) {
|
||||
log.trace {
|
||||
"Received proxied connection request from $sourceAddress forwarded for $proxiedClientAddress"
|
||||
}
|
||||
}
|
||||
ctx.channel().attr(RemoteBuildCacheServer.clientIp).set(proxiedClientAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -34,6 +34,8 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
||||
cfg = Configuration.of(
|
||||
"127.0.0.1",
|
||||
getFreePort(),
|
||||
false,
|
||||
emptyList(),
|
||||
50,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
|
||||
@@ -140,6 +140,9 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
||||
cfg = Configuration(
|
||||
"127.0.0.1",
|
||||
getFreePort(),
|
||||
false,
|
||||
emptyList(),
|
||||
|
||||
100,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
|
||||
@@ -34,6 +34,8 @@ class NoAuthServerTest : AbstractServerTest() {
|
||||
cfg = Configuration(
|
||||
"127.0.0.1",
|
||||
getFreePort(),
|
||||
false,
|
||||
emptyList(),
|
||||
100,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
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')
|
||||
}
|
||||
@@ -30,8 +19,6 @@ 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')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,4 +33,3 @@ include 'rbcs-client'
|
||||
include 'rbcs-server'
|
||||
include 'rbcs-servlet'
|
||||
include 'docker'
|
||||
//include 'bug'
|
||||
Reference in New Issue
Block a user