Compare commits

...

16 Commits

Author SHA1 Message Date
a1398045ac fixed native image configuration task
All checks were successful
CI / build (push) Successful in 37m16s
2025-02-26 17:42:02 +08:00
1f93602102 added healthcheck role
improved documentation

client configuration promoted to standalone class
2025-02-26 15:26:18 +08:00
c818463a2e fixed entrypoint in native Docker image 2025-02-25 22:26:29 +08:00
cd28563985 fixed typo 2025-02-25 22:06:52 +08:00
8ef2d9c64e improved usability of native docker image
All checks were successful
CI / build (push) Successful in 33m19s
2025-02-25 21:31:02 +08:00
1510956989 update JWO version to fix bug
All checks were successful
CI / build (push) Successful in 34m13s
the `RBCS_CONFIGURATION_DIR` was ignored because of a bug in JWO
2025-02-25 20:10:09 +08:00
ac4f0fdd19 increased tolerance of RetryTest 2025-02-25 19:19:07 +08:00
37da03c719 added signal handler to native executable
All checks were successful
CI / build (push) Successful in 33m50s
2025-02-25 19:15:48 +08:00
60bc4375cf update lys-catalog version 2025-02-25 15:54:11 +08:00
725fe22b80 added server configuration file documentation 2025-02-25 15:31:26 +08:00
ca18b63f27 added GraalVM native image executable build 2025-02-25 15:30:58 +08:00
23f2a351a6 shared event executor group between server and clients
All checks were successful
CI / build (push) Successful in 3m44s
- improved documentation
- closed memcache client's thread pools
2025-02-24 13:52:20 +08:00
c7d2b89d82 fixed server prefix handling 2025-02-22 15:25:41 +08:00
72c34b57a6 fixed memory leak 2025-02-22 15:25:41 +08:00
619873c4a9 removed readTimeout and writeTimeout from server configuration
added Markdown documentation
2025-02-22 15:25:36 +08:00
591f6e2af4 parametrized password hashing algorithm for basic authentication 2025-02-20 16:46:15 +08:00
75 changed files with 3564 additions and 885 deletions

View File

@@ -39,9 +39,9 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/rbcs:latest gitea.woggioni.net/woggioni/rbcs:vanilla
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
target: release target: release-vanilla
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
- -
name: Build rbcs memcache Docker image name: Build rbcs memcache Docker image
@@ -57,6 +57,20 @@ jobs:
target: release-memcache target: release-memcache
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx 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 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:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:latest
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
gitea.woggioni.net/woggioni/rbcs:native
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
target: release-native
- name: Publish artifacts - name: Publish artifacts
env: env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Y. T. CHUNG <zonyitoo@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

347
README.md
View File

@@ -0,0 +1,347 @@
# Remote Build Cache Server
Remote Build Cache Server (shortened to RBCS) allows you to share and reuse unchanged build
and test outputs across the team. This speeds up local and CI builds since cycles are not wasted
re-building components that are unaffected by new code changes. RBCS supports both Gradle and
Maven build tool environments.
It comes with pluggable storage backends, the core application offers in-memory storage or disk-backed storage,
in addition to this there is an official plugin to use memcached as the storage backend.
It supports HTTP basic authentication or, alternatively, TLS certificate authentication, role-based access control (RBAC),
and throttling.
## Quickstart
### Downloading the jar file
You can download the latest version from [this link](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/)
Assuming you have Java 21 or later installed, you can launch the server directly with
```bash
java -jar rbcs-cli.jar server
```
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
writing data to the disk, that you can use for testing
### Using the Docker image
You can pull the latest Docker image with
```bash
docker pull gitea.woggioni.net/woggioni/rbcs:latest
```
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
writing data to the disk, that you can use for testing
### Using the native executable
If you are on a Linux X86_64 machine you can download the native executable
from [here](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/).
It behaves the same as the jar file but it doesn't require a JVM and it has faster startup times.
becausue of GraalVm's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis),
the native executable does not supports plugins, so it comes with all plugins embedded into it.
## Usage
### Configuration
The location of the `rbcs-server.xml` configuration file depends on the operating system,
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir` Java system property
to the directory that contain the `rbcs-server.xml` file.
The server configuration file follows the XML format and uses XML schema for validation
(you can find the schema for the main configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd)).
The configuration values are enclosed inside XML attribute and support system property / environmental variable interpolation.
As an example, you can configure RBCS to read the server port number from the `RBCS_SERVER_PORT` environmental variable
and the bind address from the `rbc.bind.address` JVM system property with
```xml
<bind host="${sys:rpc.bind.address}" port="${env:RBCS_SERVER_PORT}"/>
```
Full documentation for all tags and attributes is available [here](doc/server_configuration.md).
### Plugins
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
Plugins need to be stored in a folder named `plugins` in the located server's working directory
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
the content of the archive into the `plugins` directory for the server to pick them up.
### Using RBCS with Gradle
Add this to the `settings.gradle` file of your project
```groovy
buildCache {
remote(HttpBuildCache) {
url = 'https://rbcs.example.com/'
push = true
allowInsecureProtocol = false
// The credentials block is only required if you enable
// HTTP basic authentication on RBCS
credentials {
username = 'build-cache-user'
password = 'some-complicated-password'
}
}
}
```
alternatively you can add this to `${GRADLE_HOME}/init.gradle` to configure the remote cache
at the system level
```groovy
gradle.settingsEvaluated { settings ->
settings.buildCache {
remote(HttpBuildCache) {
url = 'https://rbcs.example.com/'
push = true
allowInsecureProtocol = false
// The credentials block is only required if you enable
// HTTP basic authentication on RBCS
credentials {
username = 'build-cache-user'
password = 'some-complicated-password'
}
}
}
}
```
add `org.gradle.caching=true` to your `<project>/gradle.properties` or run gradle with `--build-cache`.
Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information.
### Using RBCS with Maven
1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content
```xml
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.1.0 https://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-build-cache-extension</artifactId>
<version>1.2.0</version>
</extension>
</extensions>
```
2. Copy [maven-build-cache-config.xml](https://maven.apache.org/extensions/maven-build-cache-extension/maven-build-cache-config.xml) into `<project>/.mvn/` folder
3. Edit the `cache/configuration/remote` element
```xml
<remote enabled="true" id="rbcs">
<url>https://rbcs.example.com/</url>
</remote>
```
4. Run maven with
```bash
mvn -Dmaven.build.cache.enabled=true -Dmaven.build.cache.debugOutput=true -Dmaven.build.cache.remote.save.enabled=true package
```
Alternatively you can set those properties in your `<project>/pom.xml`
Read [here](https://maven.apache.org/extensions/maven-build-cache-extension/remote-cache.html)
for more informations
## Authentication
RBCS supports 2 authentication mechanisms:
- HTTP basic authentication
- TLS certificate authentication
### Configure HTTP basic authentication
Add a `<basic>` element to the `<authentication>` element in your `rbcs-server.xml`
```xml
<authentication>
<basic/>
</authentication>
```
### Configure TLS certificate authentication
Add a `<client-certificate>` element to the `<authentication>` element in your `rbcs-server.xml`
```xml
<authentication>
<client-certificate>
<user-extractor attribute-name="CN" pattern="(.*)"/>
<group-extractor attribute-name="O" pattern="(.*)"/>
</client-certificate>
</authentication>
```
The `<user-extractor>` here determines how the username is extracted from the
subject's X.500 name in the TLS certificate presented by the client, where `attribute-name`
is the `RelativeDistinguishedName` (RDN) identifier and pattern is a regular expression
that will be applied to extract the username from the first group present in the regex.
An error will be thrown if the regular expression contains no groups, while additional
groups are ignored.
Similarly, the `<group-extractor>` here determines how the group name is extracted from the
subject's X.500 name in the TLS certificate presented by the client.
Note that this allows to assign roles to incoming requests without necessarily assigning them
a username.
## Access control
RBCS supports role-based access control (RBAC), three roles are available:
- `Reader` can perform `GET` calls
- `Writer` can perform `PUT` calls
- `Healthcheck` can perform `TRACE` calls
Roles are assigned to groups so that a user will have a role only if that roles belongs
to one of the groups he is a member of.
There is also a special `<anonymous>` user
which matches any request who hasn't been authenticated and that can be assigned
to any group like a normal user. This permits to have a build cache that is
publicly readable but only writable by authenticated users (e.g. CI/CD pipeline).
### Defining users
Users can be defined in the `<authorization>` element
```xml
<authorization>
<users>
<user name="user1" password="kb/vNnkn2RvyPkTN6Q07uH0F7wI7u61MkManD3NHregRukBg4KHehfbqtLTb39fZjHA+SRH+EpEWDCf+Rihr5H5C1YN5qwmArV0p8O5ptC4="/>
<user name="user2" password="2J7MAhdIzZ3SO+JGB+K6wPhb4P5LH1L4L7yJCl5QrxNfAWRr5jTUExJRbcgbH1UfnkCbIO1p+xTDq+FCj3LFBZeMZUNZ47npN+WR7AX3VTo="/>
<anonymous/>
</users>
<groups>
<group name="readers">
<users>
<anonymous/>
</users>
<roles>
<reader/>
</roles>
</group>
<group name="writers">
<users>
<user ref="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
<healthcheck/>
</roles>
</group>
</groups>
</authorization>
```
The `password` attribute is only used for HTTP Basic authentication, so it can be omitted
if you use TLS certificate authentication. It must contain a password hash that can be derived from
the actual password using the following command
```bash
java -jar rbcs-cli.jar password
```
## Reliability
RBCS implements the [TRACE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE) HTTP method and this functionality can be used
as a health check (mind you need to have `Healthcheck` role in order to perform it and match the server's `prefix` in the URL).
## RBCS Client
RBCS ships with a command line client that can be used for testing, benchmarking or to manually
upload/download files to the cache. It must be configured with the `rbcs-client.xml`,
whose location follows the same logic of the `rbcs-server.xml`
### GET command
```bash
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME get -k $CACHE_KEY -v $FILE_WHERE_THE_VALUE_WILL_BE_STORED
```
### PUT command
```bash
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME put -k $CACHE_KEY -v $FILE_TO_BE_UPLOADED
```
## Logging
RBCS uses [logback](https://logback.qos.ch/) and ships with a [default logging configuration](./conf/logback.xml) that
can be overridden with `-Dlogback.configurationFile=path/to/custom/configuration.xml`, refer to
[Logback documentation](https://logback.qos.ch/manual/configuration.html) for more details about
how to configure Logback
## FAQ
### Why should I use a build cache?
#### Build Caches Improve Build & Test Performance
Building software consists of a number of steps, like compiling sources, executing tests, and linking binaries. Weve seen that a binary artifact repository helps when such a step requires an external component by downloading the artifact from the repository rather than building it locally.
However, there are many additional steps in this build process which can be optimized to reduce the build time. An obvious strategy is to avoid executing build steps which dominate the total build time when these build steps are not needed.
Most build times are dominated by the testing step.
While binary repositories cannot capture the outcome of a test build step (only the test reports
when included in binary artifacts), build caches are designed to eliminate redundant executions
for every build step. Moreover, it generalizes the concept of avoiding work associated with any
incremental step of the build, including test execution, compilation and resource processing.
The mechanism itself is comparable to a pure function. That is, given some inputs such as source
files and environment parameters we know that the output is always going to be the same.
As a result, we can cache it and retrieve it based on a simple cryptographic hash of the inputs.
Build caching is supported natively by some build tools.
#### Improve CI builds with a remote build cache
When analyzing the role of a build cache it is important to take into account the granularity
of the changes that it caches. Imagine a full build for a project with 40 to 50 modules
which fails at the last step (deployment) because the staging environment is temporarily unavailable.
Although the vast majority of the build steps (potentially thousands) succeed,
the change can not be deployed to the staging environment.
Without a build cache one typically relies on a very complex CI configuration to reuse build step outputs
or would have to repeat the full build once the environment is available.
Some build tools dont support incremental builds properly. For example, outputs of a build started
from scratch may vary when compared to subsequent builds that rely on the initial builds output.
As a result, to preserve build integrity, its crucial to rebuild from scratch, or cleanly, in this
scenario.
With a build cache, only the last step needs to be executed and the build can be re-triggered
when the environment is back online. This automatically saves all of the time and
resources required across the different build steps which were successfully executed.
Instead of executing the intermediate steps, the build tool pulls the outputs from the build cache,
avoiding a lot of redundant work
#### Share outputs with a remote build cache
One of the most important advantages of a remote build cache is the ability to share build outputs.
In most CI configurations, for example, a number of pipelines are created.
These may include one for building the sources, one for testing, one for publishing the outcomes
to a remote repository, and other pipelines to test on different platforms.
There are even situations where CI builds partially build a project (i.e. some modules and not others).
Most of those pipelines share a lot of intermediate build steps. All builds which perform testing
require the binaries to be ready. All publishing builds require all previous steps to be executed.
And because modern CI infrastructure means executing everything in containerized (isolated) environments,
significant resources are wasted by repeatedly building the same intermediate artifacts.
A remote build cache greatly reduces this overhead by orders of magnitudes because it provides a way
for all those pipelines to share their outputs. After all, there is no point recreating an output that
is already available in the cache.
Because there are inherent dependencies between software components of a build,
introducing a build cache dramatically reduces the impact of exploding a component into multiple pieces,
allowing for increased modularity without increased overhead.
#### Make local developers more efficient with remote build caches
It is common for different teams within a company to work on different modules of a single large
application. In this case, most teams dont care about building the other parts of the software.
By introducing a remote cache developers immediately benefit from pre-built artifacts when checking out code.
Because it has already been built on CI, they dont have to do it locally.
Introducing a remote cache is a huge benefit for those developers. Consider that a typical developers
day begins by performing a code checkout. Most likely the checked out code has already been built on CI.
Therefore, no time is wasted running the first build of the day. The remote cache provides all of the
intermediate artifacts needed. And, in the event local changes are made, the remote cache still leverages
partial cache hits for projects which are independent. As other developers in the organization request
CI builds, the remote cache continues to populate, increasing the likelihood of these remote cache hits
across team members.

View File

@@ -14,9 +14,7 @@ allprojects { subproject ->
if(project.currentTag.isPresent()) { if(project.currentTag.isPresent()) {
version = project.currentTag.map { it[0] }.get() version = project.currentTag.map { it[0] }.get()
} else { } else {
version = project.gitRevision.map { gitRevision -> version = "${getProperty('rbcs.version')}-SNAPSHOT"
"${getProperty('rbcs.version')}.${gitRevision[0..10]}"
}.get()
} }
repositories { repositories {
@@ -24,7 +22,6 @@ allprojects { subproject ->
url = getProperty('gitea.maven.url') url = getProperty('gitea.maven.url')
content { content {
includeModule 'net.woggioni', 'jwo' includeModule 'net.woggioni', 'jwo'
includeModule 'net.woggioni', 'xmemcached'
includeGroup 'com.lys' includeGroup 'com.lys'
} }
} }
@@ -41,7 +38,7 @@ allprojects { subproject ->
withSourcesJar() withSourcesJar()
modularity.inferModulePath = true modularity.inferModulePath = true
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(23)
vendor = JvmVendorSpec.ORACLE vendor = JvmVendorSpec.ORACLE
} }
} }

125
doc/client_configuration.md Normal file
View File

@@ -0,0 +1,125 @@
# XML Schema Documentation: RBCS Client Configuration
This document provides detailed information about the XML schema for RBCS client configuration, which defines profiles for connecting to RBCS servers.
## Root Element
### `profiles`
The root element that contains a collection of server profiles.
- **Type**: `profilesType`
- **Contains**: Zero or more `profile` elements
## Complex Types
### `profilesType`
Defines the structure for the profiles collection.
- **Elements**:
- `profile`: Server connection profile (0 to unbounded)
### `profileType`
Defines a server connection profile with authentication, connection settings, and retry policies.
- **Attributes**:
- `name` (required): Name of the server profile, referenced with the '-p' parameter in rbcs-cli
- `base-url` (required): RBCs server URL
- `max-connections`: Maximum number of concurrent TCP connections (default: 50)
- `connection-timeout`: Timeout for establishing connections
- `enable-compression`: Whether to enable HTTP compression (default: true)
- **Elements** (in sequence):
- **Authentication** (choice of one):
- `no-auth`: Disable authentication
- `basic-auth`: Enable HTTP basic authentication
- `tls-client-auth`: Enable TLS certificate authentication
- `connection` (optional): Connection timeout settings
- `retry-policy` (optional): Retry policy for failed requests
- `tls-trust-store` (optional): Custom truststore for server certificate validation
### `connectionType`
Defines connection timeout settings.
- **Attributes**:
- `idle-timeout`: Close connection after inactivity period (default: PT30S - 30 seconds)
- `read-idle-timeout`: Close connection when no read occurs (default: PT60S - 60 seconds)
- `write-idle-timeout`: Close connection when no write occurs (default: PT60S - 60 seconds)
### `noAuthType`
Indicates no authentication should be used.
- No attributes or elements
### `basicAuthType`
Configures HTTP Basic Authentication.
- **Attributes**:
- `user` (required): Username for authentication
- `password` (required): Password for authentication
### `tlsClientAuthType`
Configures TLS client certificate authentication.
- **Attributes**:
- `key-store-file` (required): Path to the keystore file
- `key-store-password` (required): Password to open the keystore
- `key-alias` (required): Alias of the keystore entry with the private key
- `key-password` (optional): Private key entry's encryption password
### `retryType`
Defines retry policy using exponential backoff.
- **Attributes**:
- `max-attempts` (required): Maximum number of retry attempts
- `initial-delay`: Delay before first retry (default: PT1S - 1 second)
- `exp`: Exponent for computing next delay (default: 2.0)
### `trustStoreType`
Configures custom truststore for server certificate validation.
- **Attributes**:
- `file` (required): Path to the truststore file
- `password`: Truststore file password
- `check-certificate-status`: Whether to check certificate validity using CRL/OCSP
- `verify-server-certificate`: Whether to validate server certificates (default: true)
## Sample XML Document
```xml
<?xml version="1.0" encoding="UTF-8"?>
<profiles xmlns="urn:net.woggioni.rbcs.client">
<!-- Profile with basic authentication -->
<profile name="production-server"
base-url="https://rbcs.example.com/api"
max-connections="100"
enable-compression="true">
<basic-auth user="admin" password="secure_password123"/>
<connection idle-timeout="PT45S"
read-idle-timeout="PT90S"
write-idle-timeout="PT90S"/>
<retry-policy max-attempts="5"
initial-delay="PT2S"
exp="1.5"/>
<tls-trust-store file="/path/to/truststore.jks"
password="truststore_password"
check-certificate-status="true"/>
</profile>
<!-- Profile with TLS client authentication -->
<profile name="secure-server"
base-url="https://secure.example.com/api"
max-connections="25">
<tls-client-auth key-store-file="/path/to/keystore.p12"
key-store-password="keystore_password"
key-alias="client-cert"
key-password="key_password"/>
<retry-policy max-attempts="3"/>
</profile>
<!-- Profile with no authentication -->
<profile name="development"
base-url="http://localhost:8080/api"
enable-compression="false">
<no-auth/>
</profile>
</profiles>
```
This sample XML document demonstrates three different profiles with various authentication methods and configuration options as defined in the schema.

178
doc/server_configuration.md Normal file
View File

@@ -0,0 +1,178 @@
### RBCS server configuration file elements and attributes
#### Root Element: `server`
The root element that contains all server configuration.
**Attributes:**
- `path` (optional): URI path prefix for cache requests. Example: if set to "cache", requests would be made to "http://www.example.com/cache/KEY"
#### Child Elements
#### `<bind>`
Configures server socket settings.
**Attributes:**
- `host` (required): Server bind address
- `port` (required): Server port number
- `incoming-connections-backlog-size` (optional, default: 1024): Maximum queue length for incoming connection indications
#### `<connection>`
Configures connection handling parameters.
**Attributes:**
- `idle-timeout` (optional, default: PT30S): Connection timeout when no activity
- `read-idle-timeout` (optional, default: PT60S): Connection timeout when no reads
- `write-idle-timeout` (optional, default: PT60S): Connection timeout when no writes
- `max-request-size` (optional, default: 0x4000000): Maximum allowed request body size
#### `<event-executor>`
Configures event execution settings.
**Attributes:**
- `use-virtual-threads` (optional, default: true): Whether to use virtual threads for the server handler
#### `<cache>`
Defines cache storage implementation. Two types are available:
##### InMemory Cache
A simple storage backend that uses an hash map to store data in memory
**Attributes:**
- `max-age` (default: P1D): Cache entry lifetime
- `max-size` (default: 0x1000000): Maximum cache size in bytes
- `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level (-1 to 9)
- `chunk-size` (default: 0x10000): Maximum socket write size
##### FileSystem Cache
A storage backend that stores data in a folder on the disk
**Attributes:**
- `path`: Storage directory path
- `max-age` (default: P1D): Cache entry lifetime
- `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level
- `chunk-size` (default: 0x10000): Maximum in-memory cache value size
#### `<authorization>`
Configures user and group-based access control.
##### `<users>`
List of registered users.
- Contains `<user>` elements:
**Attributes:**
- `name` (required): Username
- `password` (optional): For basic authentication
- Can contain an `anonymous` element to allow for unauthenticated access
##### `<groups>`
List of user groups.
- Contains `<group>` elements:
**Attributes:**
- `name`: Group name
- Can contain:
- `users`: List of user references
- `roles`: List of roles (READER/WRITER)
- `user-quota`: Per-user quota
- `group-quota`: Group-wide quota
#### `<authentication>`
Configures authentication mechanism. Options:
- `<basic>`: HTTP basic authentication
- `<client-certificate>`: TLS certificate authentication, it uses attributes of the subject's X.500 name
to extract the username and group of the client.
Example:
```xml
<client-certificate>
<user-extractor attribute-name="CN" pattern="(.*)"/>
<group-extractor attribute-name="O" pattern="(.*)"/>
</client-certificate>
```
- `<none>`: No authentication
#### `<tls>`
Configures TLS encryption.
**Child Elements:**
- `<keystore>`: Server certificate configuration
**Attributes:**
- `file` (required): Keystore file path
- `password`: Keystore password
- `key-alias` (required): Private key alias
- `key-password`: Private key password
- `<truststore>`: Client certificate verification
**Attributes:**
- `file` (required): Truststore file path
- `password`: Truststore password
- `check-certificate-status`: Enable CRL/OCSP checking
- `require-client-certificate` (default: false): Require client certificates
----------------------------
# Complete configuration example
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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="0.0.0.0" port="8080" incoming-connections-backlog-size="1024"/>
<connection
max-request-size="67108864"
idle-timeout="PT10S"
read-idle-timeout="PT20S"
write-idle-timeout="PT20S"
read-timeout="PT5S"
write-timeout="PT5S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" />
<!--cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" path="${sys:java.io.tmpdir}/rbcs"/-->
<authorization>
<users>
<user name="user1" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
<quota calls="100" period="PT1S"/>
</user>
<user name="user2" password="v6T9+q6/VNpvLknji3ixPiyz2YZCQMXj2FN7hvzbfc2Ig+IzAHO0iiBCH9oWuBDq"/>
<anonymous>
<quota calls="10" period="PT60S" initial-available-calls="10" max-available-calls="10"/>
</anonymous>
</users>
<groups>
<group name="readers">
<users>
<anonymous/>
</users>
<roles>
<reader/>
</roles>
</group>
<group name="writers">
<users>
<user ref="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
</groups>
</authorization>
<authentication>
<basic/>
</authentication>
</rbcs:server>
```

View File

@@ -3,7 +3,7 @@ RUN adduser -D luser
USER luser USER luser
WORKDIR /home/luser WORKDIR /home/luser
FROM base-release AS release FROM base-release AS release-vanilla
ADD rbcs-cli-envelope-*.jar rbcs.jar ADD rbcs-cli-envelope-*.jar rbcs.jar
ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
@@ -15,3 +15,9 @@ RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distri
WORKDIR /home/luser WORKDIR /home/luser
ADD logback.xml . ADD logback.xml .
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
FROM scratch AS release-native
ADD rbcs-cli.upx /rbcs/rbcs-cli
ENV RBCS_CONFIGURATION_DIR="/rbcs"
WORKDIR /rbcs
ENTRYPOINT ["/rbcs/rbcs-cli"]

24
docker/README.md Normal file
View File

@@ -0,0 +1,24 @@
# RBCS Docker images
There are 3 image flavours:
- vanilla
- memcache
- native
The `vanilla` image only contains the envelope
jar file with no plugins and is based on `eclipse-temurin:21-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`
image and want to use memcache as the cache backend
The `native` image contains a native, statically-linked executable created with GraalVM
that has no userspace dependencies. It also embeds the memcache plugin inside the executable.
Use this image for maximum efficiency and minimal memory footprint.
## Which image shoud I use?
The `native` image uses Java's SerialGC, so it's ideal for constrained environment like containers or small servers,
if you have a lot of resources and want to squeeze out the maximum throughput you should consider the
`vanilla` or `memcache` image, then choose and fine tune the garbage collector.
Also the `native` image is only available for the `x86_64` architecture at the moment,
while `vanilla` and `memcache` also ship a `aarch64` variant.

View File

@@ -4,7 +4,7 @@ org.gradle.caching=true
rbcs.version = 0.2.0 rbcs.version = 0.2.0
lys.version = 2025.02.08 lys.version = 2025.02.26
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

View File

@@ -5,6 +5,7 @@ plugins {
} }
dependencies { dependencies {
api catalog.netty.common
api catalog.netty.buffer api catalog.netty.buffer
api catalog.netty.handler api catalog.netty.handler
} }

View File

@@ -4,6 +4,7 @@ module net.woggioni.rbcs.api {
requires io.netty.buffer; requires io.netty.buffer;
requires io.netty.handler; requires io.netty.handler;
requires io.netty.transport; requires io.netty.transport;
requires io.netty.common;
exports net.woggioni.rbcs.api; exports net.woggioni.rbcs.api;
exports net.woggioni.rbcs.api.exception; exports net.woggioni.rbcs.api.exception;
exports net.woggioni.rbcs.api.message; exports net.woggioni.rbcs.api.message;

View File

@@ -0,0 +1,13 @@
package net.woggioni.rbcs.api;
import java.util.concurrent.CompletableFuture;
public interface AsyncCloseable extends AutoCloseable {
CompletableFuture<Void> asyncClose();
@Override
default void close() throws Exception {
asyncClose().get();
}
}

View File

@@ -1,7 +1,15 @@
package net.woggioni.rbcs.api; package net.woggioni.rbcs.api;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
public interface CacheHandlerFactory extends AutoCloseable { public interface CacheHandlerFactory extends AsyncCloseable {
ChannelHandler newHandler(); ChannelHandler newHandler(
EventLoopGroup eventLoopGroup,
ChannelFactory<SocketChannel> socketChannelFactory,
ChannelFactory<DatagramChannel> datagramChannelFactory
);
} }

View File

@@ -35,8 +35,6 @@ public class Configuration {
@Value @Value
public static class Connection { public static class Connection {
Duration readTimeout;
Duration writeTimeout;
Duration idleTimeout; Duration idleTimeout;
Duration readIdleTimeout; Duration readIdleTimeout;
Duration writeIdleTimeout; Duration writeIdleTimeout;
@@ -85,17 +83,6 @@ public class Configuration {
Group extract(X509Certificate cert); Group extract(X509Certificate cert);
} }
@Value
public static class Throttling {
KeyStore keyStore;
TrustStore trustStore;
boolean verifyClients;
}
public enum ClientCertificate {
REQUIRED, OPTIONAL
}
@Value @Value
public static class Tls { public static class Tls {
KeyStore keyStore; KeyStore keyStore;

View File

@@ -1,5 +1,5 @@
package net.woggioni.rbcs.api; package net.woggioni.rbcs.api;
public enum Role { public enum Role {
Reader, Writer Reader, Writer, Healthcheck
} }

View File

@@ -9,20 +9,22 @@ plugins {
id 'maven-publish' id 'maven-publish'
} }
import net.woggioni.gradle.envelope.EnvelopePlugin
import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImagePlugin import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.NativeImageTask import net.woggioni.gradle.graalvm.UpxTask
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) sourceSets {
mainModuleName.set('net.woggioni.rbcs.cli') configureNativeImage {
Property<String> mainClassName = objects.property(String.class) java {
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli') }
kotlin {
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { }
options.javaModuleMainClass = mainClassName }
} }
configurations { configurations {
@@ -32,16 +34,25 @@ configurations {
canBeResolved = true canBeResolved = true
visible = true visible = true
} }
}
envelopeJar { configureNativeImageImplementation {
mainModule = mainModuleName extendsFrom implementation
mainClass = mainClassName }
configureNativeImageRuntimeOnly {
extendsFrom runtimeOnly
}
nativeImage {
extendsFrom runtimeClasspath
}
extraClasspath = ["plugins"]
} }
dependencies { dependencies {
configureNativeImageImplementation project
configureNativeImageImplementation project(':rbcs-server-memcache')
implementation catalog.jwo implementation catalog.jwo
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.picocli implementation catalog.picocli
@@ -52,32 +63,63 @@ dependencies {
// runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic runtimeOnly catalog.logback.classic
// runtimeOnly catalog.slf4j.simple // runtimeOnly catalog.slf4j.simple
nativeImage project(':rbcs-server-memcache')
} }
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.rbcs.LoggingConfig' Property<String> mainModuleName = objects.property(String.class)
// systemProperties['log.config.source'] = 'net/woggioni/rbcs/cli/logging.properties' mainModuleName.set('net.woggioni.rbcs.cli')
// systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/rbcs/cli/logging.properties' Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName
}
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME, Jar)
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.ENVELOPE_JAR_TASK_NAME, EnvelopeJarTask.class) {
mainModule = mainModuleName
mainClass = mainClassName
extraClasspath = ["plugins"]
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml' systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml'
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED' 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 javaLauncher = javaToolchains.launcherFor {
mainModule = mainModuleName languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ORACLE
}
mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration"
classpath = project.files(
configurations.configureNativeImageRuntimeClasspath,
sourceSets.configureNativeImage.output
)
mergeConfiguration = false
systemProperty('logback.configurationFile', 'classpath:net/woggioni/rbcs/cli/logback.xml')
systemProperty('io.netty.leakDetectionLevel', 'DISABLED')
modularity.inferModulePath = false
enabled = true
} }
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) { nativeImage {
mainClass = mainClassName mainClass = mainClassName
mainModule = mainModuleName // mainModule = mainModuleName
useMusl = true useMusl = true
buildStaticImage = true buildStaticImage = true
linkAtBuildTime = false
classpath = project.files(jarTaskProvider, configurations.nativeImage)
compressExecutable = true
compressionLevel = 10
useLZMA = false
}
Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME, UpxTask) {
} }
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) { tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
@@ -95,14 +137,20 @@ tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
artifacts { artifacts {
release(envelopeJarTaskProvider) release(envelopeJarTaskProvider)
release(upxTaskProvider)
} }
publishing { publishing {
publications { publications {
maven(MavenPublication) { maven(MavenPublication) {
artifact envelopeJar artifact envelopeJar
artifact(upxTaskProvider) {
classifier = "linux-x86_64"
extension = "exe"
}
} }
} }
} }

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
xs:schemaLocation="urn:net.woggioni.rbcs.client jpms://net.woggioni.rbcs.client/net/woggioni/rbcs/client/schema/rbcs-client.xsd"
>
<profile name="profile1" base-url="https://rbcs1.example.com/">
<no-auth/>
<connection write-idle-timeout="PT60S"
read-idle-timeout="PT60S"
idle-timeout="PT30S" />
</profile>
<profile name="profile2" base-url="https://rbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</rbcs-client:profiles>

View File

@@ -0,0 +1,6 @@
[
{
"name":"java.lang.Boolean",
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
}
]

View File

@@ -1,2 +1,2 @@
Args=-H:Optimize=3 --gc=serial --initialize-at-run-time=io.netty Args=-O3 --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
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils #-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils

View File

@@ -0,0 +1,8 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View File

@@ -0,0 +1,2 @@
[
]

View File

@@ -0,0 +1,756 @@
[
{
"name":"android.os.Build$VERSION"
},
{
"name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.joran.SerializedModelConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.util.DefaultJoranConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.ConsoleAppender",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.OutputStreamAppender",
"methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }]
},
{
"name":"ch.qos.logback.core.encoder.Encoder",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder",
"methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }]
},
{
"name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase",
"methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.spi.ContextAware",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.aayushatharva.brotli4j.Brotli4jLoader"
},
{
"name":"com.github.luben.zstd.Zstd"
},
{
"name":"com.sun.crypto.provider.AESCipher$General",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.ARCFOURCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DESCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DESedeCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DHParameters",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.HmacCore$HmacSHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.TlsMasterSecretGenerator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.ExtendedSchemaDVFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.SchemaDVFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"groovy.lang.Closure"
},
{
"name":"io.netty.bootstrap.ServerBootstrap$1"
},
{
"name":"io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.buffer.AbstractByteBufAllocator",
"queryAllDeclaredMethods":true
},
{
"name":"io.netty.buffer.AbstractReferenceCountedByteBuf",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.channel.AbstractChannelHandlerContext",
"fields":[{"name":"handlerState"}]
},
{
"name":"io.netty.channel.ChannelDuplexHandler",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.ChannelHandlerAdapter",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.channel.ChannelInboundHandlerAdapter",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"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":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.channel.ChannelInitializer",
"methods":[{"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.channel.ChannelOutboundBuffer",
"fields":[{"name":"totalPendingSize"}, {"name":"unwritable"}]
},
{
"name":"io.netty.channel.ChannelOutboundHandlerAdapter",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.channel.CombinedChannelDuplexHandler",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"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":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.DefaultChannelConfig",
"fields":[{"name":"autoRead"}, {"name":"writeBufferWaterMark"}]
},
{
"name":"io.netty.channel.DefaultChannelPipeline",
"fields":[{"name":"estimatorHandle"}]
},
{
"name":"io.netty.channel.DefaultChannelPipeline$HeadContext",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"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":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.DefaultChannelPipeline$TailContext",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"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":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"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"
},
{
"name":"io.netty.channel.socket.nio.NioSocketChannel",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"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"] }]
},
{
"name":"io.netty.handler.codec.MessageAggregator",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.MessageToByteEncoder",
"methods":[{"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.codec.MessageToMessageCodec",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.MessageToMessageDecoder",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.compression.JdkZlibDecoder"
},
{
"name":"io.netty.handler.codec.compression.JdkZlibEncoder",
"methods":[{"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.codec.http.HttpClientCodec"
},
{
"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.HttpContentDecompressor"
},
{
"name":"io.netty.handler.codec.http.HttpContentEncoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.http.HttpObjectAggregator"
},
{
"name":"io.netty.handler.codec.http.HttpServerCodec"
},
{
"name":"io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec"
},
{
"name":"io.netty.handler.stream.ChunkedWriteHandler",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.timeout.IdleStateHandler",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"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":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.internal.tcnative.SSLContext"
},
{
"name":"io.netty.util.AbstractReferenceCounted",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.util.DefaultAttributeMap",
"fields":[{"name":"attributes"}]
},
{
"name":"io.netty.util.DefaultAttributeMap$DefaultAttribute",
"fields":[{"name":"attributeMap"}]
},
{
"name":"io.netty.util.Recycler$DefaultHandle",
"fields":[{"name":"state"}]
},
{
"name":"io.netty.util.ReferenceCountUtil",
"queryAllDeclaredMethods":true
},
{
"name":"io.netty.util.concurrent.DefaultPromise",
"fields":[{"name":"result"}]
},
{
"name":"io.netty.util.concurrent.SingleThreadEventExecutor",
"fields":[{"name":"state"}, {"name":"threadProperties"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields",
"fields":[{"name":"producerLimit"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields",
"fields":[{"name":"consumerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields",
"fields":[{"name":"producerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField",
"fields":[{"name":"consumerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField",
"fields":[{"name":"producerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField",
"fields":[{"name":"producerLimit"}]
},
{
"name":"java.io.FilePermission"
},
{
"name":"java.lang.Object",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.lang.ProcessHandle",
"methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }]
},
{
"name":"java.lang.RuntimePermission"
},
{
"name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }]
},
{
"name":"java.lang.Thread",
"fields":[{"name":"threadLocalRandomProbe"}]
},
{
"name":"java.net.NetPermission"
},
{
"name":"java.net.SocketPermission"
},
{
"name":"java.net.URLPermission",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"java.nio.Bits",
"fields":[{"name":"MAX_MEMORY"}, {"name":"UNALIGNED"}]
},
{
"name":"java.nio.Buffer",
"fields":[{"name":"address"}]
},
{
"name":"java.nio.ByteBuffer",
"methods":[{"name":"alignedSlice","parameterTypes":["int"] }]
},
{
"name":"java.nio.DirectByteBuffer",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }]
},
{
"name":"java.nio.channels.spi.SelectorProvider",
"methods":[{"name":"openServerSocketChannel","parameterTypes":["java.net.ProtocolFamily"] }, {"name":"openSocketChannel","parameterTypes":["java.net.ProtocolFamily"] }]
},
{
"name":"java.nio.file.Path"
},
{
"name":"java.nio.file.Paths",
"methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }]
},
{
"name":"java.security.AlgorithmParametersSpi"
},
{
"name":"java.security.AllPermission"
},
{
"name":"java.security.KeyStoreSpi"
},
{
"name":"java.security.SecureRandomParameters"
},
{
"name":"java.security.SecurityPermission"
},
{
"name":"java.sql.Connection"
},
{
"name":"java.sql.Driver"
},
{
"name":"java.sql.DriverManager",
"methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.sql.Time",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.sql.Timestamp",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.Duration",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Instant",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDate",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.MonthDay",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Period",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Year",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.YearMonth",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.ZoneId",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZoneOffset",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZonedDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.util.PropertyPermission"
},
{
"name":"java.util.concurrent.ForkJoinTask",
"fields":[{"name":"aux"}, {"name":"status"}]
},
{
"name":"java.util.concurrent.atomic.AtomicBoolean",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.atomic.AtomicReference",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.atomic.Striped64",
"fields":[{"name":"base"}, {"name":"cellsBusy"}]
},
{
"name":"java.util.concurrent.atomic.Striped64$Cell",
"fields":[{"name":"value"}]
},
{
"name":"java.util.zip.Adler32",
"methods":[{"name":"update","parameterTypes":["java.nio.ByteBuffer"] }]
},
{
"name":"java.util.zip.CRC32",
"methods":[{"name":"update","parameterTypes":["java.nio.ByteBuffer"] }]
},
{
"name":"javax.security.auth.x500.X500Principal",
"fields":[{"name":"thisX500Name"}],
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
},
{
"name":"javax.smartcardio.CardPermission"
},
{
"name":"jdk.internal.misc.Unsafe",
"methods":[{"name":"getUnsafe","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli$VersionProvider",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.RbcsCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.ClientCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.GetCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.PasswordHashCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.PutCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.ServerCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.DurationConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.client.RemoteBuildCacheClient$sendRequest$1$operationComplete$responseHandler$1",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.client.RemoteBuildCacheClient$sendRequest$1$operationComplete$timeoutHandler$1",
"methods":[{"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$HttpChunkContentCompressor",
"methods":[{"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$NettyHttpBasicAuthenticator"
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$ServerInitializer"
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$ServerInitializer$initChannel$4",
"methods":[{"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.cache.FileSystemCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.cache.InMemoryCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.exception.ExceptionHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.CacheContentHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.MaxRequestSizeHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.ServerHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.TraceHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.memcache.MemcacheCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.memcache.client.MemcacheClient$sendRequest$1$operationComplete$handler$1",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.throttling.ThrottlingHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"sun.misc.Unsafe",
"fields":[{"name":"theUnsafe"}],
"methods":[{"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, {"name":"getAndAddLong","parameterTypes":["java.lang.Object","long","long"] }, {"name":"getAndSetObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] }, {"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }, {"name":"storeFence","parameterTypes":[] }]
},
{
"name":"sun.nio.ch.SelectorImpl",
"fields":[{"name":"publicSelectedKeys"}, {"name":"selectedKeys"}]
},
{
"name":"sun.security.pkcs12.PKCS12KeyStore",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.DSA$SHA224withDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.DSA$SHA256withDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.JavaKeyStore$JKS",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.MD5",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.NativePRNG",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.NativePRNG$NonBlocking",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.SHA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA2$SHA224",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA2$SHA256",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA5$SHA384",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA5$SHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.X509Factory",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.PSSParameters",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSAKeyFactory$Legacy",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSAPSSSignature",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSASignature$SHA224withRSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.SSLContextImpl$DefaultSSLContext",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.SSLContextImpl$TLSContext",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.x509.AuthorityInfoAccessExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.AuthorityKeyIdentifierExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.BasicConstraintsExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.CRLDistributionPointsExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.CertificatePoliciesExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.KeyUsageExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.NetscapeCertTypeExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.PrivateKeyUsageExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.SubjectAlternativeNameExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.SubjectKeyIdentifierExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
}
]

View File

@@ -0,0 +1,46 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/MANIFEST.MF\\E"
}, {
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/net.woggioni.rbcs.api.CacheProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qclasspath:net/woggioni/rbcs/cli/logback.xml\\E"
}, {
"pattern":"\\Qlogback-test.scmo\\E"
}, {
"pattern":"\\Qlogback.scmo\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/cli/logback.xml\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/client/schema/rbcs-client.xsd\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/rbcs-default.xml\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]},
"bundles":[{
"name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message",
"locales":[""]
}]
}

View File

@@ -0,0 +1,14 @@
{
"types":[
{
"name":"java.lang.String"
},
{
"name":"net.woggioni.rbcs.api.CacheValueMetadata"
}
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

View File

@@ -0,0 +1,181 @@
package net.woggioni.rbcs.cli.graal
import net.woggioni.jwo.NullOutputStream
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.User
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.cli.RemoteBuildCacheServerCli
import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
import net.woggioni.rbcs.cli.impl.commands.GetCommand
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
import net.woggioni.rbcs.cli.impl.commands.PutCommand
import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.RemoteBuildCacheServer
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
import java.io.ByteArrayInputStream
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.ExecutionException
import java.util.zip.Deflater
import net.woggioni.rbcs.client.Configuration as ClientConfiguration
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
object GraalNativeImageConfiguration {
@JvmStatic
fun main(vararg args : String) {
val serverDoc = RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL.openStream().use {
Xml.parseXml(RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL, it)
}
Parser.parse(serverDoc)
val url = URI.create("file:conf/rbcs-client.xml").toURL()
val clientDoc = url.openStream().use {
Xml.parseXml(url, it)
}
ClientConfigurationParser.parse(clientDoc)
val PASSWORD = "password"
val readersGroup = Configuration.Group("readers", setOf(Role.Reader, Role.Healthcheck), null, null)
val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null)
val users = listOf(
User("user1", hashPassword(PASSWORD), setOf(readersGroup), null),
User("user2", hashPassword(PASSWORD), setOf(writersGroup), null),
User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup), null),
User("", null, setOf(readersGroup), null),
User("user4", hashPassword(PASSWORD), setOf(readersGroup),
Configuration.Quota(1, Duration.of(1, ChronoUnit.DAYS), 0, 1)
),
User("user5", hashPassword(PASSWORD), setOf(readersGroup),
Configuration.Quota(1, Duration.of(5, ChronoUnit.SECONDS), 0, 1)
)
)
val serverPort = RBCS.getFreePort()
val caches = listOf<Configuration.Cache>(
InMemoryCacheConfiguration(
maxAge = Duration.ofSeconds(3600),
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false,
maxSize = 0x1000000,
chunkSize = 0x1000
),
FileSystemCacheConfiguration(
Path.of(System.getProperty("java.io.tmpdir")).resolve("rbcs"),
maxAge = Duration.ofSeconds(3600),
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false,
chunkSize = 0x1000
),
MemcacheCacheConfiguration(
listOf(MemcacheCacheConfiguration.Server(
HostAndPort("127.0.0.1", 11211),
1000,
4)
),
Duration.ofSeconds(60),
"MD5",
null,
1,
0x1000
)
)
for (cache in caches) {
val serverConfiguration = Configuration(
"127.0.0.1",
serverPort,
100,
null,
Configuration.EventExecutor(true),
Configuration.Connection(
Duration.ofSeconds(10),
Duration.ofSeconds(15),
Duration.ofSeconds(15),
0x10000,
),
users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
cache,
Configuration.BasicAuthentication(),
null,
)
MemcacheCacheConfiguration(
listOf(
MemcacheCacheConfiguration.Server(
HostAndPort("127.0.0.1", 11211),
1000,
4
)
),
Duration.ofSeconds(60),
"MD5",
null,
1,
0x1000
)
val serverHandle = RemoteBuildCacheServer(serverConfiguration).run()
val clientProfile = ClientConfiguration.Profile(
URI.create("http://127.0.0.1:$serverPort/"),
null,
ClientConfiguration.Authentication.BasicAuthenticationCredentials("user3", PASSWORD),
Duration.ofSeconds(3),
10,
true,
ClientConfiguration.RetryPolicy(
3,
1000,
1.2
),
ClientConfiguration.TrustStore(null, null, false, false)
)
HealthCheckCommand.execute(clientProfile)
BenchmarkCommand.execute(
clientProfile,
1000,
0x100,
true
)
PutCommand.execute(
clientProfile,
"some-file.bin",
ByteArrayInputStream(ByteArray(0x1000) { it.toByte() }),
"application/octet-setream",
"attachment; filename=\"some-file.bin\""
)
GetCommand.execute(
clientProfile,
"some-file.bin",
NullOutputStream()
)
serverHandle.sendShutdownSignal()
try {
serverHandle.get()
} catch (ee : ExecutionException) {
}
}
RemoteBuildCacheServerCli.main("--help")
}
}

View File

@@ -11,7 +11,7 @@ import net.woggioni.rbcs.cli.impl.commands.PasswordHashCommand
import net.woggioni.rbcs.cli.impl.commands.PutCommand import net.woggioni.rbcs.cli.impl.commands.PutCommand
import net.woggioni.rbcs.cli.impl.commands.ServerCommand import net.woggioni.rbcs.cli.impl.commands.ServerCommand
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
import net.woggioni.rbcs.common.contextLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Model.CommandSpec
@@ -23,15 +23,20 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
class VersionProvider : AbstractVersionProvider() class VersionProvider : AbstractVersionProvider()
companion object { companion object {
private fun setPropertyIfNotPresent(key: String, value: String) {
System.getProperty(key) ?: System.setProperty(key, value)
}
@JvmStatic @JvmStatic
fun main(vararg args: String) { fun main(vararg args: String) {
setPropertyIfNotPresent("logback.configurationFile", "net/woggioni/rbcs/cli/logback.xml")
setPropertyIfNotPresent("io.netty.leakDetectionLevel", "DISABLED")
val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader
Thread.currentThread().contextClassLoader = currentClassLoader Thread.currentThread().contextClassLoader = currentClassLoader
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") { if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
//We're running in an envelope jar and custom URL protocols won't work //We're running in an envelope jar and custom URL protocols won't work
RbcsUrlStreamHandlerFactory.install() RbcsUrlStreamHandlerFactory.install()
} }
val log = contextLogger() val log = createLogger<RemoteBuildCacheServerCli>()
val app = Application.builder("rbcs") val app = Application.builder("rbcs")
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR") .configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir") .configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")

View File

@@ -5,6 +5,7 @@ import net.woggioni.jwo.LongMath
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter import net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug import net.woggioni.rbcs.common.debug
@@ -26,8 +27,123 @@ import kotlin.random.Random
showDefaultValues = true showDefaultValues = true
) )
class BenchmarkCommand : RbcsCommand() { class BenchmarkCommand : RbcsCommand() {
companion object{ companion object {
private val log = createLogger<BenchmarkCommand>() private val log = createLogger<BenchmarkCommand>()
fun execute(profile : Configuration.Profile,
numberOfEntries : Int,
entrySize : Int,
useRandomValue : Boolean,
) {
val progressThreshold = LongMath.ceilDiv(numberOfEntries.toLong(), 20)
RemoteBuildCacheClient(profile).use { client ->
val entryGenerator = sequence {
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
while (true) {
val key = JWO.bytesToHex(random.nextBytes(16))
val value = if (useRandomValue) {
random.nextBytes(entrySize)
} else {
val byteValue = random.nextInt().toByte()
ByteArray(entrySize) { _ -> byteValue }
}
yield(key to value)
}
}
log.info {
"Starting insertion"
}
val entries = let {
val completionCounter = AtomicLong(0)
val completionQueue = LinkedBlockingQueue<Pair<String, ByteArray>>(numberOfEntries)
val start = Instant.now()
val semaphore = Semaphore(profile.maxConnections * 5)
val iterator = entryGenerator.take(numberOfEntries).iterator()
while (completionCounter.get() < numberOfEntries) {
if (iterator.hasNext()) {
val entry = iterator.next()
semaphore.acquire()
val future =
client.put(entry.first, entry.second, CacheValueMetadata(null, null)).thenApply { entry }
future.whenComplete { result, ex ->
if (ex != null) {
log.error(ex.message, ex)
} else {
completionQueue.put(result)
}
semaphore.release()
val completed = completionCounter.incrementAndGet()
if (completed.mod(progressThreshold) == 0L) {
log.debug {
"Inserted $completed / $numberOfEntries"
}
}
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val inserted = completionQueue.toList()
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000)
"Insertion rate: $opsPerSecond ops/s"
}
inserted
}
log.info {
"Inserted ${entries.size} entries"
}
log.info {
"Starting retrieval"
}
if (entries.isNotEmpty()) {
val completionCounter = AtomicLong(0)
val semaphore = Semaphore(profile.maxConnections * 5)
val start = Instant.now()
val it = entries.iterator()
while (completionCounter.get() < entries.size) {
if (it.hasNext()) {
val entry = it.next()
semaphore.acquire()
val future = client.get(entry.first).thenApply {
if (it == null) {
log.error {
"Missing entry for key '${entry.first}'"
}
} else if (!entry.second.contentEquals(it)) {
log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'"
}
}
}
future.whenComplete { _, _ ->
val completed = completionCounter.incrementAndGet()
if (completed.mod(progressThreshold) == 0L) {
log.debug {
"Retrieved $completed / ${entries.size}"
}
}
semaphore.release()
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000)
"Retrieval rate: $opsPerSecond ops/s"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
}
} }
@CommandLine.Spec @CommandLine.Spec
@@ -60,113 +176,11 @@ class BenchmarkCommand : RbcsCommand() {
clientCommand.configuration.profiles[profileName] clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
} }
val progressThreshold = LongMath.ceilDiv(numberOfEntries.toLong(), 20) execute(
RemoteBuildCacheClient(profile).use { client -> profile,
numberOfEntries,
val entryGenerator = sequence { size,
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong()) randomValues
while (true) { )
val key = JWO.bytesToHex(random.nextBytes(16))
val value = if(randomValues) {
random.nextBytes(size)
} else {
val byteValue = random.nextInt().toByte()
ByteArray(size) {_ -> byteValue}
}
yield(key to value)
}
}
log.info {
"Starting insertion"
}
val entries = let {
val completionCounter = AtomicLong(0)
val completionQueue = LinkedBlockingQueue<Pair<String, ByteArray>>(numberOfEntries)
val start = Instant.now()
val semaphore = Semaphore(profile.maxConnections * 5)
val iterator = entryGenerator.take(numberOfEntries).iterator()
while (completionCounter.get() < numberOfEntries) {
if (iterator.hasNext()) {
val entry = iterator.next()
semaphore.acquire()
val future = client.put(entry.first, entry.second, CacheValueMetadata(null, null)).thenApply { entry }
future.whenComplete { result, ex ->
if (ex != null) {
log.error(ex.message, ex)
} else {
completionQueue.put(result)
}
semaphore.release()
val completed = completionCounter.incrementAndGet()
if(completed.mod(progressThreshold) == 0L) {
log.debug {
"Inserted $completed / $numberOfEntries"
}
}
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val inserted = completionQueue.toList()
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000)
"Insertion rate: $opsPerSecond ops/s"
}
inserted
}
log.info {
"Inserted ${entries.size} entries"
}
log.info {
"Starting retrieval"
}
if (entries.isNotEmpty()) {
val completionCounter = AtomicLong(0)
val semaphore = Semaphore(profile.maxConnections * 5)
val start = Instant.now()
val it = entries.iterator()
while (completionCounter.get() < entries.size) {
if (it.hasNext()) {
val entry = it.next()
semaphore.acquire()
val future = client.get(entry.first).thenApply {
if (it == null) {
log.error {
"Missing entry for key '${entry.first}'"
}
} else if (!entry.second.contentEquals(it)) {
log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'"
}
}
}
future.whenComplete { _, _ ->
val completed = completionCounter.incrementAndGet()
if(completed.mod(progressThreshold) == 0L) {
log.debug {
"Retrieved $completed / ${entries.size}"
}
}
semaphore.release()
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000)
"Retrieval rate: $opsPerSecond ops/s"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
} }
} }

View File

@@ -2,8 +2,11 @@ package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import picocli.CommandLine import picocli.CommandLine
import java.lang.IllegalArgumentException
import java.nio.file.Path import java.nio.file.Path
@CommandLine.Command( @CommandLine.Command(
@@ -24,15 +27,20 @@ class ClientCommand(app : Application) : RbcsCommand() {
names = ["-p", "--profile"], names = ["-p", "--profile"],
description = ["Name of the client profile to be used"], description = ["Name of the client profile to be used"],
paramLabel = "PROFILE", paramLabel = "PROFILE",
required = true required = false
) )
var profileName : String? = null var profileName : String? = null
get() = field ?: throw IllegalArgumentException("A profile name must be specified using the '-p' command line parameter")
val configuration : RemoteBuildCacheClient.Configuration by lazy { val configuration : Configuration by lazy {
RemoteBuildCacheClient.Configuration.parse(configurationFile) Configuration.parse(configurationFile)
} }
override fun run() { override fun run() {
val log = createLogger<ClientCommand>()
log.debug {
"Using configuration file '$configurationFile'"
}
println("Available profiles:") println("Available profiles:")
configuration.profiles.forEach { (profileName, _) -> configuration.profiles.forEach { (profileName, _) ->
println(profileName) println(profileName)

View File

@@ -1,9 +1,11 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@@ -13,8 +15,20 @@ import java.nio.file.Path
showDefaultValues = true showDefaultValues = true
) )
class GetCommand : RbcsCommand() { class GetCommand : RbcsCommand() {
companion object{ companion object {
private val log = createLogger<GetCommand>() private val log = createLogger<GetCommand>()
fun execute(profile : Configuration.Profile, key : String, outputStream: OutputStream) {
RemoteBuildCacheClient(profile).use { client ->
client.get(key).thenApply { value ->
value?.let {
outputStream.use {
it.write(value)
}
} ?: throw NoSuchElementException("No value found for key $key")
}.get()
}
}
} }
@CommandLine.Spec @CommandLine.Spec
@@ -40,14 +54,6 @@ class GetCommand : RbcsCommand() {
clientCommand.configuration.profiles[profileName] clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
} }
RemoteBuildCacheClient(profile).use { client -> execute(profile, key, (output?.let(Files::newOutputStream) ?: System.out))
client.get(key).thenApply { value ->
value?.let {
(output?.let(Files::newOutputStream) ?: System.out).use {
it.write(value)
}
} ?: throw NoSuchElementException("No value found for key $key")
}.get()
}
} }
} }

View File

@@ -1,6 +1,7 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
@@ -15,6 +16,27 @@ import kotlin.random.Random
class HealthCheckCommand : RbcsCommand() { class HealthCheckCommand : RbcsCommand() {
companion object{ companion object{
private val log = createLogger<HealthCheckCommand>() private val log = createLogger<HealthCheckCommand>()
fun execute(profile : Configuration.Profile) {
RemoteBuildCacheClient(profile).use { client ->
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
val nonce = ByteArray(0xa0)
random.nextBytes(nonce)
client.healthCheck(nonce).thenApply { value ->
if(value == null) {
throw IllegalStateException("Empty response from server")
}
val offset = value.size - nonce.size
for(i in 0 until nonce.size) {
val a = nonce[i]
val b = value[offset + i]
if(a != b) {
throw IllegalStateException("Server nonce does not match")
}
}
}.get()
}
}
} }
@CommandLine.Spec @CommandLine.Spec
@@ -26,23 +48,6 @@ class HealthCheckCommand : RbcsCommand() {
clientCommand.configuration.profiles[profileName] clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
} }
RemoteBuildCacheClient(profile).use { client -> execute(profile)
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
val nonce = ByteArray(0xa0)
random.nextBytes(nonce)
client.healthCheck(nonce).thenApply { value ->
if(value == null) {
throw IllegalStateException("Empty response from server")
}
val offset = value.size - nonce.size
for(i in 0 until nonce.size) {
val a = nonce[i]
val b = value[offset + i]
if(a != b) {
throw IllegalStateException("Server nonce does not match")
}
}
}.get()
}
} }
} }

View File

@@ -5,6 +5,7 @@ import net.woggioni.jwo.JWO
import net.woggioni.jwo.NullOutputStream import net.woggioni.jwo.NullOutputStream
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
@@ -19,8 +20,22 @@ import java.util.UUID
showDefaultValues = true showDefaultValues = true
) )
class PutCommand : RbcsCommand() { class PutCommand : RbcsCommand() {
companion object{ companion object {
private val log = createLogger<PutCommand>() private val log = createLogger<PutCommand>()
fun execute(profile: Configuration.Profile,
actualKey : String,
inputStream: InputStream,
mimeType : String?,
contentDisposition: String?
) {
RemoteBuildCacheClient(profile).use { client ->
inputStream.use {
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
}.get()
println(profile.serverURI.resolve(actualKey))
}
}
} }
@@ -92,10 +107,7 @@ class PutCommand : RbcsCommand() {
} }
actualKey = key ?: UUID.randomUUID().toString() actualKey = key ?: UUID.randomUUID().toString()
} }
inputStream.use { execute(profile, actualKey, inputStream, mimeType, contentDisposition)
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
}.get()
println(profile.serverURI.resolve(actualKey))
} }
} }
} }

View File

@@ -59,6 +59,9 @@ class ServerCommand(app : Application) : RbcsCommand() {
createDefaultConfigurationFile(configurationFile) createDefaultConfigurationFile(configurationFile)
} }
log.debug {
"Using configuration file '$configurationFile'"
}
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile) val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile)
log.debug { log.debug {
ByteArrayOutputStream().also { ByteArrayOutputStream().also {

View File

@@ -0,0 +1,61 @@
package net.woggioni.rbcs.client
import net.woggioni.rbcs.client.impl.Parser
import net.woggioni.rbcs.common.Xml
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
data class Configuration(
val profiles: Map<String, Profile>
) {
sealed class Authentication {
data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
}
class TrustStore (
var file: Path?,
var password: String?,
var checkCertificateStatus: Boolean = false,
var verifyServerCertificate: Boolean = true,
)
class RetryPolicy(
val maxAttempts: Int,
val initialDelayMillis: Long,
val exp: Double
)
class Connection(
val readIdleTimeout: Duration,
val writeIdleTimeout: Duration,
val idleTimeout: Duration,
)
data class Profile(
val serverURI: URI,
val connection: Connection?,
val authentication: Authentication?,
val connectionTimeout: Duration?,
val maxConnections: Int,
val compressionEnabled: Boolean,
val retryPolicy: RetryPolicy?,
val tlsTruststore : TrustStore?
)
companion object {
fun parse(path: Path): Configuration {
return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse)
}
}
}

View File

@@ -36,24 +36,21 @@ import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.concurrent.Future import io.netty.util.concurrent.Future
import io.netty.util.concurrent.GenericFutureListener import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.client.impl.Parser import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.trace import net.woggioni.rbcs.common.trace
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration
import java.util.Base64 import java.util.Base64
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.random.Random import kotlin.random.Random
import io.netty.util.concurrent.Future as NettyFuture import io.netty.util.concurrent.Future as NettyFuture
@@ -63,62 +60,40 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
private val group: NioEventLoopGroup private val group: NioEventLoopGroup
private var sslContext: SslContext private val sslContext: SslContext
private val pool: ChannelPool private val pool: ChannelPool
data class Configuration(
val profiles: Map<String, Profile>
) {
sealed class Authentication {
data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
}
class RetryPolicy(
val maxAttempts: Int,
val initialDelayMillis: Long,
val exp: Double
)
class Connection(
val readTimeout: Duration,
val writeTimeout: Duration,
val idleTimeout: Duration,
val readIdleTimeout: Duration,
val writeIdleTimeout: Duration
)
data class Profile(
val serverURI: URI,
val connection: Connection?,
val authentication: Authentication?,
val connectionTimeout: Duration?,
val maxConnections: Int,
val compressionEnabled: Boolean,
val retryPolicy: RetryPolicy?,
)
companion object {
fun parse(path: Path): Configuration {
return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse)
}
}
}
init { init {
group = NioEventLoopGroup() group = NioEventLoopGroup()
sslContext = SslContextBuilder.forClient().also { builder -> sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials -> (profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.keyManager( builder.apply {
tlsClientAuthenticationCredentials.key, keyManager(
*tlsClientAuthenticationCredentials.certificateChain tlsClientAuthenticationCredentials.key,
) *tlsClientAuthenticationCredentials.certificateChain
)
profile.tlsTruststore?.let { trustStore ->
if(!trustStore.verifyServerCertificate) {
trustManager(object : X509TrustManager {
override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun checkServerTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun getAcceptedIssuers() = null
})
} else {
trustStore.file?.let {
val ts = loadKeystore(it, trustStore.password)
val trustManagerFactory: TrustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(ts)
trustManager(trustManagerFactory)
}
}
}
}
} }
}.build() }.build()
@@ -178,19 +153,6 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
val pipeline: ChannelPipeline = ch.pipeline() val pipeline: ChannelPipeline = ch.pipeline()
profile.connection?.also { conn -> profile.connection?.also { conn ->
val readTimeout = conn.readTimeout.toMillis()
val writeTimeout = conn.writeTimeout.toMillis()
if (readTimeout > 0 || writeTimeout > 0) {
pipeline.addLast(
IdleStateHandler(
false,
readTimeout,
writeTimeout,
0,
TimeUnit.MILLISECONDS
)
)
}
val readIdleTimeout = conn.readIdleTimeout.toMillis() val readIdleTimeout = conn.readIdleTimeout.toMillis()
val writeIdleTimeout = conn.writeIdleTimeout.toMillis() val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
val idleTimeout = conn.idleTimeout.toMillis() val idleTimeout = conn.idleTimeout.toMillis()

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.client.impl package net.woggioni.rbcs.client.impl
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.Xml.Companion.asIterable import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
@@ -16,9 +16,9 @@ import java.time.temporal.ChronoUnit
object Parser { object Parser {
fun parse(document: Document): RemoteBuildCacheClient.Configuration { fun parse(document: Document): Configuration {
val root = document.documentElement val root = document.documentElement
val profiles = mutableMapOf<String, RemoteBuildCacheClient.Configuration.Profile>() val profiles = mutableMapOf<String, Configuration.Profile>()
for (child in root.asIterable()) { for (child in root.asIterable()) {
val tagName = child.localName val tagName = child.localName
@@ -28,9 +28,10 @@ object Parser {
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required") child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI) val uri = child.renderAttribute("base-url")?.let(::URI)
?: throw ConfigurationException("base-url attribute is required") ?: throw ConfigurationException("base-url attribute is required")
var authentication: RemoteBuildCacheClient.Configuration.Authentication? = null var authentication: Configuration.Authentication? = null
var retryPolicy: RemoteBuildCacheClient.Configuration.RetryPolicy? = null var retryPolicy: Configuration.RetryPolicy? = null
var connection : RemoteBuildCacheClient.Configuration.Connection? = null var connection : Configuration.Connection? = null
var trustStore : Configuration.TrustStore? = null
for (gchild in child.asIterable()) { for (gchild in child.asIterable()) {
when (gchild.localName) { when (gchild.localName) {
"tls-client-auth" -> { "tls-client-auth" -> {
@@ -51,7 +52,7 @@ object Parser {
.toList() .toList()
.toTypedArray() .toTypedArray()
authentication = authentication =
RemoteBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials( Configuration.Authentication.TlsClientAuthenticationCredentials(
key, key,
certChain certChain
) )
@@ -63,7 +64,7 @@ object Parser {
val password = gchild.renderAttribute("password") val password = gchild.renderAttribute("password")
?: throw ConfigurationException("password attribute is required") ?: throw ConfigurationException("password attribute is required")
authentication = authentication =
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials( Configuration.Authentication.BasicAuthenticationCredentials(
username, username,
password password
) )
@@ -82,7 +83,7 @@ object Parser {
gchild.renderAttribute("exp") gchild.renderAttribute("exp")
?.let(String::toDouble) ?.let(String::toDouble)
?: 2.0f ?: 2.0f
retryPolicy = RemoteBuildCacheClient.Configuration.RetryPolicy( retryPolicy = Configuration.RetryPolicy(
maxAttempts, maxAttempts,
initialDelay.toMillis(), initialDelay.toMillis(),
exp.toDouble() exp.toDouble()
@@ -90,24 +91,29 @@ object Parser {
} }
"connection" -> { "connection" -> {
val writeTimeout = gchild.renderAttribute("write-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val readTimeout = gchild.renderAttribute("read-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val idleTimeout = gchild.renderAttribute("idle-timeout") val idleTimeout = gchild.renderAttribute("idle-timeout")
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout") val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout") val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
connection = RemoteBuildCacheClient.Configuration.Connection( connection = Configuration.Connection(
readTimeout,
writeTimeout,
idleTimeout,
readIdleTimeout, readIdleTimeout,
writeIdleTimeout, writeIdleTimeout,
idleTimeout,
) )
} }
"tls-trust-store" -> {
val file = gchild.renderAttribute("file")
?.let(Path::of)
val password = gchild.renderAttribute("password")
val checkCertificateStatus = gchild.renderAttribute("check-certificate-status")
?.let(String::toBoolean) ?: false
val verifyServerCertificate = gchild.renderAttribute("verify-server-certificate")
?.let(String::toBoolean) ?: true
trustStore = Configuration.TrustStore(file, password, checkCertificateStatus, verifyServerCertificate)
}
} }
} }
val maxConnections = child.renderAttribute("max-connections") val maxConnections = child.renderAttribute("max-connections")
@@ -119,18 +125,19 @@ object Parser {
?.let(String::toBoolean) ?.let(String::toBoolean)
?: true ?: true
profiles[name] = RemoteBuildCacheClient.Configuration.Profile( profiles[name] = Configuration.Profile(
uri, uri,
connection, connection,
authentication, authentication,
connectionTimeout, connectionTimeout,
maxConnections, maxConnections,
compressionEnabled, compressionEnabled,
retryPolicy retryPolicy,
trustStore
) )
} }
} }
} }
return RemoteBuildCacheClient.Configuration(profiles) return Configuration(profiles)
} }
} }

View File

@@ -15,46 +15,239 @@
<xs:complexType name="profileType"> <xs:complexType name="profileType">
<xs:sequence> <xs:sequence>
<xs:choice> <xs:choice>
<xs:element name="no-auth" type="rbcs-client:noAuthType"/> <xs:element name="no-auth" type="rbcs-client:noAuthType">
<xs:element name="basic-auth" type="rbcs-client:basicAuthType"/> <xs:annotation>
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType"/> <xs:documentation>
Disable authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="basic-auth" type="rbcs-client:basicAuthType">
<xs:annotation>
<xs:documentation>
Enable HTTP basic authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType">
<xs:annotation>
<xs:documentation>
Enable TLS certificate authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice> </xs:choice>
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" /> <xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" >
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0"/> <xs:annotation>
<xs:documentation>
Set inactivity timeouts for connections to this server,
if not present, connections are only closed on network errors.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0">
<xs:annotation>
<xs:documentation>
Set a retry policy for this server, if not present requests won't be retried
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tls-trust-store" type="rbcs-client:trustStoreType" minOccurs="0">
<xs:annotation>
<xs:documentation>
If set, specify an alternative truststore to validate the server certificate.
If not present the system truststore is used.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence> </xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/> <xs:attribute name="name" type="xs:token" use="required">
<xs:attribute name="base-url" type="xs:anyURI" use="required"/> <xs:annotation>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/> <xs:documentation>
<xs:attribute name="connection-timeout" type="xs:duration"/> Name of this server profile, to be referred to from rbcs-cli with the '-p' parameter
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/> </xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="base-url" type="xs:anyURI" use="required">
<xs:annotation>
<xs:documentation>
RBCs server URL
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50">
<xs:annotation>
<xs:documentation>
Maximum number of concurrent TCP connection to open with this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="connection-timeout" type="xs:duration">
<xs:annotation>
<xs:documentation>
Enable HTTP compression when communicating to this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
<xs:annotation>
<xs:documentation>
Enable HTTP compression when communicating to this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="connectionType"> <xs:complexType name="connectionType">
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT0S"/> <xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/> <xs:annotation>
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/> <xs:documentation>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/> The client will close the connection with the server
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/> when neither a read nor a write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The client will close the connection with the server
when no read was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The client will close the connection with the server
when no write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="noAuthType"/> <xs:complexType name="noAuthType">
<xs:annotation>
<xs:documentation>
Add this tag to not use any type of authentication when talking to the RBCS server
</xs:documentation>
</xs:annotation>
</xs:complexType>
<xs:complexType name="basicAuthType"> <xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/> <xs:annotation>
<xs:attribute name="password" type="xs:string" use="required"/> <xs:documentation>
Add this tag to enable HTTP basic authentication for the communication to this server,
mind that HTTP basic authentication sends credentials directly over the network, so make sure
your communication is protected by TLS (i.e. your server's URL starts with "https")
</xs:documentation>
</xs:annotation>
<xs:attribute name="user" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
Username for HTTP basic authentication
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Password used for HTTP basic authentication
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="tlsClientAuthType"> <xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/> <xs:attribute name="key-store-file" type="xs:anyURI" use="required">
<xs:attribute name="key-store-password" type="xs:string" use="required"/> <xs:annotation>
<xs:attribute name="key-alias" type="xs:token" use="required"/> <xs:documentation>
<xs:attribute name="key-password" type="xs:string" use="optional"/> System path to the keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-store-password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Password to open they keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-alias" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
Alias of the keystore entry containing the private key
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-password" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
Private key entry's encryption password
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="retryType"> <xs:complexType name="retryType">
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required"/> <xs:annotation>
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S"/> <xs:documentation>
<xs:attribute name="exp" type="xs:double" default="2.0"/> Retry policy to use in case of failures, based on exponential backoff
https://en.wikipedia.org/wiki/Exponential_backoff
</xs:documentation>
</xs:annotation>
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required">
<xs:annotation>
<xs:documentation>
Maximum number of attempts, after which the call will result in an error,
throwing an exception related to the last received failure
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S">
<xs:annotation>
<xs:documentation>
Delay to apply before retrying after the first failed call
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="exp" type="xs:double" default="2.0">
<xs:annotation>
<xs:documentation>
Exponent to apply to compute the next delay
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Path to the truststore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string">
<xs:annotation>
<xs:documentation>
Truststore file password
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="check-certificate-status" type="xs:boolean">
<xs:annotation>
<xs:documentation>
Whether or not check the server certificate validity using CRL/OCSP
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="verify-server-certificate" type="xs:boolean" use="optional" default="true">
<xs:annotation>
<xs:documentation>
If false, the client will blindly trust the certificate provided by the server
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:schema> </xs:schema>

View File

@@ -129,7 +129,7 @@ class RetryTest {
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6 previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
val actualTimestamp = timestamp val actualTimestamp = timestamp
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
Assertions.assertTrue(err < 1e-2) Assertions.assertTrue(err < 0.1)
} }
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) { if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
/* /*

View File

@@ -9,6 +9,8 @@
key-store-password="password" key-store-password="password"
key-alias="woggioni@c962475fa38" key-alias="woggioni@c962475fa38"
key-password="key-password"/> key-password="key-password"/>
<connection write-idle-timeout="PT60S" read-idle-timeout="PT60S" write-timeout="PT0S" read-timeout="PT0S" idle-timeout="PT30S" />
<tls-trust-store file="file.pfx" password="password" check-certificate-status="false" verify-server-certificate="true"/>
</profile> </profile>
<profile name="profile2" base-url="https://rbcs2.example.com/"> <profile name="profile2" base-url="https://rbcs2.example.com/">
<basic-auth user="user" password="password"/> <basic-auth user="user" password="password"/>

View File

@@ -7,7 +7,18 @@ import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.PBEKeySpec
object PasswordSecurity { object PasswordSecurity {
private const val KEY_LENGTH = 256
enum class Algorithm(
val codeName : String,
val keyLength : Int,
val iterations : Int) {
PBEWithHmacSHA512_224AndAES_256("PBEWithHmacSHA512/224AndAES_256", 64, 1),
PBEWithHmacSHA1AndAES_256("PBEWithHmacSHA1AndAES_256",64, 1),
PBEWithHmacSHA384AndAES_128("PBEWithHmacSHA384AndAES_128", 64,1),
PBEWithHmacSHA384AndAES_256("PBEWithHmacSHA384AndAES_256",64,1),
PBKDF2WithHmacSHA512("PBKDF2WithHmacSHA512",512, 1),
PBKDF2WithHmacSHA384("PBKDF2WithHmacSHA384",384, 1);
}
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray { private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size) val result = ByteArray(arr1.size + arr2.size)
@@ -23,22 +34,22 @@ object PasswordSecurity {
return result return result
} }
fun hashPassword(password : String, salt : String? = null) : String { fun hashPassword(password : String, salt : String? = null, algorithm : Algorithm = Algorithm.PBKDF2WithHmacSHA512) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run { val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16) val result = ByteArray(16)
nextBytes(result) nextBytes(result)
result result
} }
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH) val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, algorithm.iterations, algorithm.keyLength)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val factory = SecretKeyFactory.getInstance(algorithm.codeName)
val hash = factory.generateSecret(spec).encoded val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt))) return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
} }
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> { fun decodePasswordHash(encodedPasswordHash : String, algorithm: Algorithm = Algorithm.PBKDF2WithHmacSHA512) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash) val decoded = Base64.getDecoder().decode(encodedPasswordHash)
val hash = ByteArray(KEY_LENGTH / 8) val hash = ByteArray(algorithm.keyLength / 8)
val salt = ByteArray(decoded.size - KEY_LENGTH / 8) val salt = ByteArray(decoded.size - algorithm.keyLength / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size) System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size) System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt return hash to salt

View File

@@ -1,9 +1,26 @@
package net.woggioni.rbcs.common package net.woggioni.rbcs.common
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
import java.io.IOException
import java.net.InetAddress
import java.net.ServerSocket
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import java.util.EnumSet
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
object RBCS { object RBCS {
fun String.toUrl() : URL = URL.of(URI(this), null) fun String.toUrl() : URL = URL.of(URI(this), null)
@@ -32,7 +49,7 @@ object RBCS {
fun digest( fun digest(
data: ByteArray, data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5") md: MessageDigest
): ByteArray { ): ByteArray {
md.update(data) md.update(data)
return md.digest() return md.digest()
@@ -40,7 +57,7 @@ object RBCS {
fun digestString( fun digestString(
data: ByteArray, data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5") md: MessageDigest
): String { ): String {
return JWO.bytesToHex(digest(data, md)) return JWO.bytesToHex(digest(data, md))
} }
@@ -58,4 +75,86 @@ object RBCS {
null null
} }
} }
fun getFreePort(): Int {
var count = 0
while (count < 50) {
try {
ServerSocket(0, 50, InetAddress.getLocalHost()).use { serverSocket ->
val candidate = serverSocket.localPort
if (candidate > 0) {
return candidate
} else {
throw RuntimeException("Got invalid port number: $candidate")
}
}
} catch (ignored: IOException) {
++count
}
}
throw RuntimeException("Error trying to find an open port")
}
fun loadKeystore(file: Path, password: String?): KeyStore {
val ext = JWO.splitExtension(file)
.map(Tuple2<String, String>::get_2)
.orElseThrow {
IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
val keystore = when (ext.substring(1).lowercase()) {
"jks" -> KeyStore.getInstance("JKS")
"p12", "pfx" -> KeyStore.getInstance("PKCS12")
else -> throw IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
Files.newInputStream(file).use {
keystore.load(it, password?.let(String::toCharArray))
}
return keystore
}
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
return if (trustStore != null) {
val certificateFactory = CertificateFactory.getInstance("X.509")
val validator = CertPathValidator.getInstance("PKIX").apply {
val rc = revocationChecker as PKIXRevocationChecker
rc.options = EnumSet.of(
PKIXRevocationChecker.Option.NO_FALLBACK
)
}
val params = PKIXParameters(trustStore).apply {
isRevocationEnabled = certificateRevocationEnabled
}
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
try {
validator.validate(clientCertificateChain, params)
} catch (ex: CertPathValidatorException) {
throw CertificateException(ex)
}
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
throw NotImplementedError()
}
private val acceptedIssuers = trustStore.aliases().asSequence()
.filter(trustStore::isCertificateEntry)
.map(trustStore::getCertificate)
.map { it as X509Certificate }
.toList()
.toTypedArray()
override fun getAcceptedIssuers() = acceptedIssuers
}
} else {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
.single() as X509TrustManager
}
}
} }

View File

@@ -0,0 +1,38 @@
package net.woggioni.rbcs.common
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import java.security.Provider
import java.security.Security
import java.util.Base64
class PasswordHashingTest {
@EnumSource(PasswordSecurity.Algorithm::class)
@ParameterizedTest
fun test(algo: PasswordSecurity.Algorithm) {
val password = "password"
val encoded = hashPassword(password, algorithm = algo)
val (_, salt) = decodePasswordHash(encoded, algo)
Assertions.assertEquals(encoded,
hashPassword(password, salt = salt.let(Base64.getEncoder()::encodeToString), algorithm = algo)
)
}
@Test
fun listAvailableAlgorithms() {
Security.getProviders().asSequence()
.flatMap { provider: Provider -> provider.services.asSequence() }
.filter { service: Provider.Service -> "SecretKeyFactory" == service.type }
.map(Provider.Service::getAlgorithm)
.forEach {
println(it)
}
}
}

View File

@@ -0,0 +1,46 @@
# RBCS Memcache plugins
This plugins allows RBCs to store and retrieve data from a memcache cluster.
The memcache server selection is simply based on the hash of the key,
deflate compression is also supported and performed by the RBCS server
## Quickstart
The plugin can be built with
```bash
./gradlew rbcs-server-memcache:bundle
```
which creates a `.tar` archive in the `build/distributions` folder.
The archive is supposed to be extracted inside the RBCS server's `plugins` directory.
## Configuration
The plugin can be enabled setting the `xs:type` attribute of the `cache` element
to `memcacheCacheType`.
The plugins currently supports the following configuration attributes:
- `max-age`: the amount of time cache entries will be retained on memcache
- `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,
currently only `deflate` is supported (optionla, if omitted compression is disabled)
- `compression-level`: compression level to use, deflate supports compression levels from 1 to 9,
where 1 is for fast compression at the expense of speed (optional, 6 is used if omitted)
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server"
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache"
xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd"
>
...
<cache xs:type="rbcs-memcache:memcacheCacheType"
max-age="P7D"
digest="SHA-256"
compression-mode="deflate"
compression-level="6"
chunk-size="0x10000">
<server host="127.0.0.1" port="11211" max-connections="256"/>
<server host="127.0.0.1" port="11212" max-connections="256"/>
</cache>
...
```

View File

@@ -1,10 +1,20 @@
package net.woggioni.rbcs.server.memcache package net.woggioni.rbcs.server.memcache
import io.netty.channel.ChannelFactory
import io.netty.channel.ChannelHandler
import io.netty.channel.EventLoopGroup
import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.HostAndPort import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.server.memcache.client.MemcacheClient import net.woggioni.rbcs.server.memcache.client.MemcacheClient
import java.time.Duration import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
data class MemcacheCacheConfiguration( data class MemcacheCacheConfiguration(
val servers: List<Server>, val servers: List<Server>,
@@ -12,7 +22,7 @@ data class MemcacheCacheConfiguration(
val digestAlgorithm: String? = null, val digestAlgorithm: String? = null,
val compressionMode: CompressionMode? = null, val compressionMode: CompressionMode? = null,
val compressionLevel: Int, val compressionLevel: Int,
val chunkSize : Int val chunkSize: Int
) : Configuration.Cache { ) : Configuration.Cache {
enum class CompressionMode { enum class CompressionMode {
@@ -23,19 +33,58 @@ data class MemcacheCacheConfiguration(
} }
data class Server( data class Server(
val endpoint : HostAndPort, val endpoint: HostAndPort,
val connectionTimeoutMillis : Int?, val connectionTimeoutMillis: Int?,
val maxConnections : Int val maxConnections: Int
) )
override fun materialize() = object : CacheHandlerFactory { override fun materialize() = object : CacheHandlerFactory {
private val client = MemcacheClient(this@MemcacheCacheConfiguration.servers, chunkSize)
override fun close() { private val connectionPoolMap = ConcurrentHashMap<HostAndPort, FixedChannelPool>()
client.close()
override fun newHandler(
eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel>
): ChannelHandler {
return MemcacheCacheHandler(
MemcacheClient(
this@MemcacheCacheConfiguration.servers,
chunkSize,
eventLoop,
socketChannelFactory,
connectionPoolMap
),
digestAlgorithm,
compressionMode != null,
compressionLevel,
chunkSize,
maxAge
)
}
override fun asyncClose() = object : CompletableFuture<Void>() {
init {
val failure = AtomicReference<Throwable>(null)
val pools = connectionPoolMap.values.toList()
val npools = pools.size
val finished = AtomicInteger(0)
pools.forEach { pool ->
pool.closeAsync().addListener {
if (!it.isSuccess) {
failure.compareAndSet(null, it.cause())
}
if(finished.incrementAndGet() == npools) {
when(val ex = failure.get()) {
null -> complete(null)
else -> completeExceptionally(ex)
}
}
}
}
}
} }
override fun newHandler() = MemcacheCacheHandler(client, digestAlgorithm, compressionMode != null, compressionLevel, chunkSize, maxAge)
} }
override fun getNamespaceURI() = "urn:net.woggioni.rbcs.server.memcache" override fun getNamespaceURI() = "urn:net.woggioni.rbcs.server.memcache"

View File

@@ -4,16 +4,17 @@ package net.woggioni.rbcs.server.memcache.client
import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelFactory
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline import io.netty.channel.ChannelPipeline
import io.netty.channel.EventLoopGroup
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.pool.AbstractChannelPoolHandler import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.nio.NioSocketChannel import io.netty.channel.socket.SocketChannel
import io.netty.handler.codec.memcache.LastMemcacheContent import io.netty.handler.codec.memcache.LastMemcacheContent
import io.netty.handler.codec.memcache.MemcacheContent import io.netty.handler.codec.memcache.MemcacheContent
import io.netty.handler.codec.memcache.MemcacheObject import io.netty.handler.codec.memcache.MemcacheObject
@@ -33,23 +34,22 @@ import java.util.concurrent.ConcurrentHashMap
import io.netty.util.concurrent.Future as NettyFuture import io.netty.util.concurrent.Future as NettyFuture
class MemcacheClient(private val servers: List<MemcacheCacheConfiguration.Server>, private val chunkSize : Int) : AutoCloseable { class MemcacheClient(
private val servers: List<MemcacheCacheConfiguration.Server>,
private val chunkSize : Int,
private val group: EventLoopGroup,
private val channelFactory: ChannelFactory<SocketChannel>,
private val connectionPool: ConcurrentHashMap<HostAndPort, FixedChannelPool>
) : AutoCloseable {
private companion object { private companion object {
private val log = createLogger<MemcacheCacheHandler>() private val log = createLogger<MemcacheCacheHandler>()
} }
private val group: NioEventLoopGroup
private val connectionPool: MutableMap<HostAndPort, ChannelPool> = ConcurrentHashMap()
init {
group = NioEventLoopGroup()
}
private fun newConnectionPool(server: MemcacheCacheConfiguration.Server): FixedChannelPool { private fun newConnectionPool(server: MemcacheCacheConfiguration.Server): FixedChannelPool {
val bootstrap = Bootstrap().apply { val bootstrap = Bootstrap().apply {
group(group) group(group)
channel(NioSocketChannel::class.java) channelFactory(channelFactory)
option(ChannelOption.SO_KEEPALIVE, true) option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(server.endpoint.host, server.endpoint.port)) remoteAddress(InetSocketAddress(server.endpoint.host, server.endpoint.port))
server.connectionTimeoutMillis?.let { server.connectionTimeoutMillis?.let {

View File

@@ -4,7 +4,7 @@
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:rbcs="urn:net.woggioni.rbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"> xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd" namespace="urn:net.woggioni.rbcs.server"/> <xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd" namespace="urn:net.woggioni.rbcs.server"/>
<xs:complexType name="memcacheServerType"> <xs:complexType name="memcacheServerType">
<xs:attribute name="host" type="xs:token" use="required"/> <xs:attribute name="host" type="xs:token" use="required"/>
@@ -21,7 +21,7 @@
</xs:sequence> </xs:sequence>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/> <xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
<xs:attribute name="digest" type="xs:token" /> <xs:attribute name="digest" type="xs:token"/>
<xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/> <xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/>
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/> <xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
</xs:extension> </xs:extension>

View File

@@ -3,6 +3,7 @@ package net.woggioni.rbcs.server
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelFactory
import io.netty.channel.ChannelFuture import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
@@ -11,7 +12,12 @@ import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPromise import io.netty.channel.ChannelPromise
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.ServerSocketChannel
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioDatagramChannel
import io.netty.channel.socket.nio.NioServerSocketChannel 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.compression.CompressionOptions
import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.HttpContentCompressor import io.netty.handler.codec.http.HttpContentCompressor
@@ -29,12 +35,13 @@ import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.AttributeKey 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.jwo.JWO import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.jwo.Tuple2
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS.getTrustManager
import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.RBCS.toUrl import net.woggioni.rbcs.common.RBCS.toUrl
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
@@ -42,7 +49,6 @@ import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.info import net.woggioni.rbcs.common.info
import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator
import net.woggioni.rbcs.server.auth.Authorizer import net.woggioni.rbcs.server.auth.Authorizer
import net.woggioni.rbcs.server.auth.ClientCertificateValidator
import net.woggioni.rbcs.server.auth.RoleAuthorizer import net.woggioni.rbcs.server.auth.RoleAuthorizer
import net.woggioni.rbcs.server.configuration.Parser import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.rbcs.server.configuration.Serializer
@@ -56,7 +62,6 @@ import java.io.OutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration import java.time.Duration
@@ -80,7 +85,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user") val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group") val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/rbcs/server/rbcs-default.xml".toUrl() } 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" private const val SSL_HANDLER_NAME = "sslHandler"
fun loadConfiguration(configurationFile: Path): Configuration { fun loadConfiguration(configurationFile: Path): Configuration {
@@ -200,8 +205,10 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private class ServerInitializer( private class ServerInitializer(
private val cfg: Configuration, private val cfg: Configuration,
private val channelFactory : ChannelFactory<SocketChannel>,
private val datagramChannelFactory : ChannelFactory<DatagramChannel>,
private val eventExecutorGroup: EventExecutorGroup private val eventExecutorGroup: EventExecutorGroup
) : ChannelInitializer<Channel>(), AutoCloseable { ) : ChannelInitializer<Channel>(), AsyncCloseable {
companion object { companion object {
private fun createSslCtx(tls: Configuration.Tls): SslContext { private fun createSslCtx(tls: Configuration.Tls): SslContext {
@@ -221,7 +228,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val clientAuth = tls.trustStore?.let { trustStore -> val clientAuth = tls.trustStore?.let { trustStore ->
val ts = loadKeystore(trustStore.file, trustStore.password) val ts = loadKeystore(trustStore.file, trustStore.password)
trustManager( trustManager(
ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus) getTrustManager(ts, trustStore.isCheckCertificateStatus)
) )
if (trustStore.isRequireClientCertificate) ClientAuth.REQUIRE if (trustStore.isRequireClientCertificate) ClientAuth.REQUIRE
else ClientAuth.OPTIONAL else ClientAuth.OPTIONAL
@@ -231,27 +238,6 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
} }
} }
fun loadKeystore(file: Path, password: String?): KeyStore {
val ext = JWO.splitExtension(file)
.map(Tuple2<String, String>::get_2)
.orElseThrow {
IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
val keystore = when (ext.substring(1).lowercase()) {
"jks" -> KeyStore.getInstance("JKS")
"p12", "pfx" -> KeyStore.getInstance("PKCS12")
else -> throw IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
Files.newInputStream(file).use {
keystore.load(it, password?.let(String::toCharArray))
}
return keystore
}
private val log = createLogger<ServerInitializer>() private val log = createLogger<ServerInitializer>()
} }
@@ -314,19 +300,6 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
} }
val pipeline = ch.pipeline() val pipeline = ch.pipeline()
cfg.connection.also { conn -> cfg.connection.also { conn ->
val readTimeout = conn.readTimeout.toMillis()
val writeTimeout = conn.writeTimeout.toMillis()
if (readTimeout > 0 || writeTimeout > 0) {
pipeline.addLast(
IdleStateHandler(
false,
readTimeout,
writeTimeout,
0,
TimeUnit.MILLISECONDS
)
)
}
val readIdleTimeout = conn.readIdleTimeout.toMillis() val readIdleTimeout = conn.readIdleTimeout.toMillis()
val writeIdleTimeout = conn.writeIdleTimeout.toMillis() val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
val idleTimeout = conn.idleTimeout.toMillis() val idleTimeout = conn.idleTimeout.toMillis()
@@ -381,21 +354,20 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
ServerHandler(prefix) ServerHandler(prefix)
} }
pipeline.addLast(eventExecutorGroup, ServerHandler.NAME, serverHandler) pipeline.addLast(eventExecutorGroup, ServerHandler.NAME, serverHandler)
pipeline.addLast(cacheHandlerFactory.newHandler())
pipeline.addLast(cacheHandlerFactory.newHandler(ch.eventLoop(), channelFactory, datagramChannelFactory))
pipeline.addLast(TraceHandler) pipeline.addLast(TraceHandler)
pipeline.addLast(ExceptionHandler) pipeline.addLast(ExceptionHandler)
} }
override fun close() { override fun asyncClose() = cacheHandlerFactory.asyncClose()
cacheHandlerFactory.close()
}
} }
class ServerHandle( class ServerHandle(
closeFuture: ChannelFuture, closeFuture: ChannelFuture,
private val bossGroup: EventExecutorGroup, private val bossGroup: EventExecutorGroup,
private val executorGroups: Iterable<EventExecutorGroup>, private val executorGroups: Iterable<EventExecutorGroup>,
private val serverInitializer: AutoCloseable, private val serverInitializer: AsyncCloseable,
) : Future<Void> by from(closeFuture, executorGroups, serverInitializer) { ) : Future<Void> by from(closeFuture, executorGroups, serverInitializer) {
companion object { companion object {
@@ -404,42 +376,53 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private fun from( private fun from(
closeFuture: ChannelFuture, closeFuture: ChannelFuture,
executorGroups: Iterable<EventExecutorGroup>, executorGroups: Iterable<EventExecutorGroup>,
serverInitializer: AutoCloseable serverInitializer: AsyncCloseable
): CompletableFuture<Void> { ): CompletableFuture<Void> {
val result = CompletableFuture<Void>() val result = CompletableFuture<Void>()
closeFuture.addListener { closeFuture.addListener {
val errors = mutableListOf<Throwable>() val errors = mutableListOf<Throwable>()
val deadline = Instant.now().plusSeconds(20) val deadline = Instant.now().plusSeconds(20)
for (executorGroup in executorGroups) {
val future = executorGroup.terminationFuture()
try {
val now = Instant.now()
if (now > deadline) {
future.get(0, TimeUnit.SECONDS)
} else {
future.get(Duration.between(now, deadline).toMillis(), TimeUnit.MILLISECONDS)
}
}
catch (te: TimeoutException) {
errors.addLast(te)
log.warn("Timeout while waiting for shutdown of $executorGroup", te)
} catch (ex: Throwable) {
log.warn(ex.message, ex)
errors.addLast(ex)
}
}
try { try {
serverInitializer.close() serverInitializer.close()
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.error(ex.message, ex) log.error(ex.message, ex)
errors.addLast(ex) errors.addLast(ex)
} }
if(errors.isEmpty()) {
result.complete(null) serverInitializer.asyncClose().whenComplete { _, ex ->
} else { if(ex != null) {
result.completeExceptionally(errors.first()) log.error(ex.message, ex)
errors.addLast(ex)
}
executorGroups.map {
it.shutdownGracefully()
}
for (executorGroup in executorGroups) {
val future = executorGroup.terminationFuture()
try {
val now = Instant.now()
if (now > deadline) {
future.get(0, TimeUnit.SECONDS)
} else {
future.get(Duration.between(now, deadline).toMillis(), TimeUnit.MILLISECONDS)
}
}
catch (te: TimeoutException) {
errors.addLast(te)
log.warn("Timeout while waiting for shutdown of $executorGroup", te)
} catch (ex: Throwable) {
log.warn(ex.message, ex)
errors.addLast(ex)
}
}
if(errors.isEmpty()) {
result.complete(null)
} else {
result.completeExceptionally(errors.first())
}
} }
} }
@@ -454,16 +437,15 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
fun sendShutdownSignal() { fun sendShutdownSignal() {
bossGroup.shutdownGracefully() bossGroup.shutdownGracefully()
executorGroups.map {
it.shutdownGracefully()
}
} }
} }
fun run(): ServerHandle { fun run(): ServerHandle {
// Create the multithreaded event loops for the server // Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup(1) val bossGroup = NioEventLoopGroup(1)
val serverSocketChannel = NioServerSocketChannel::class.java val channelFactory = ChannelFactory<SocketChannel> { NioSocketChannel() }
val datagramChannelFactory = ChannelFactory<DatagramChannel> { NioDatagramChannel() }
val serverChannelFactory = ChannelFactory<ServerSocketChannel> { NioServerSocketChannel() }
val workerGroup = NioEventLoopGroup(0) val workerGroup = NioEventLoopGroup(0)
val eventExecutorGroup = run { val eventExecutorGroup = run {
val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) { val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) {
@@ -473,11 +455,11 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
} }
DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory) DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory)
} }
val serverInitializer = ServerInitializer(cfg, eventExecutorGroup) val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory, workerGroup)
val bootstrap = ServerBootstrap().apply { val bootstrap = ServerBootstrap().apply {
// Configure the server // Configure the server
group(bossGroup, workerGroup) group(bossGroup, workerGroup)
channel(serverSocketChannel) channelFactory(serverChannelFactory)
childHandler(serverInitializer) childHandler(serverInitializer)
option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize) option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize)
childOption(ChannelOption.SO_KEEPALIVE, true) childOption(ChannelOption.SO_KEEPALIVE, true)

View File

@@ -1,90 +0,0 @@
package net.woggioni.rbcs.server.auth
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslHandshakeCompletionEvent
import java.security.KeyStore
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import java.util.EnumSet
import javax.net.ssl.SSLSession
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class ClientCertificateValidator private constructor(
private val sslHandler: SslHandler,
private val x509TrustManager: X509TrustManager
) : ChannelInboundHandlerAdapter() {
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is SslHandshakeCompletionEvent) {
if (evt.isSuccess) {
val session: SSLSession = sslHandler.engine().session
val clientCertificateChain = session.peerCertificates as Array<X509Certificate>
val authType: String = clientCertificateChain[0].publicKey.algorithm
x509TrustManager.checkClientTrusted(clientCertificateChain, authType)
} else {
// Handle the failure, for example by closing the channel.
}
}
super.userEventTriggered(ctx, evt)
}
companion object {
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
return if (trustStore != null) {
val certificateFactory = CertificateFactory.getInstance("X.509")
val validator = CertPathValidator.getInstance("PKIX").apply {
val rc = revocationChecker as PKIXRevocationChecker
rc.options = EnumSet.of(
PKIXRevocationChecker.Option.NO_FALLBACK
)
}
val params = PKIXParameters(trustStore).apply {
isRevocationEnabled = certificateRevocationEnabled
}
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
try {
validator.validate(clientCertificateChain, params)
} catch (ex: CertPathValidatorException) {
throw CertificateException(ex)
}
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
throw NotImplementedError()
}
private val acceptedIssuers = trustStore.aliases().asSequence()
.filter(trustStore::isCertificateEntry)
.map(trustStore::getCertificate)
.map { it as X509Certificate }
.toList()
.toTypedArray()
override fun getAcceptedIssuers() = acceptedIssuers
}
} else {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
.single() as X509TrustManager
}
}
fun of(
sslHandler: SslHandler,
trustStore: KeyStore?,
certificateRevocationEnabled: Boolean
): ClientCertificateValidator {
return ClientCertificateValidator(sslHandler, getTrustManager(trustStore, certificateRevocationEnabled))
}
}
}

View File

@@ -8,8 +8,9 @@ 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, HttpMethod.TRACE), Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD),
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST) Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST),
Role.Healthcheck to setOf(HttpMethod.TRACE)
) )
} }

View File

@@ -1,6 +1,7 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -18,11 +19,12 @@ import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture
class FileSystemCache( class FileSystemCache(
val root: Path, val root: Path,
val maxAge: Duration val maxAge: Duration
) : AutoCloseable { ) : AsyncCloseable {
class EntryValue(val metadata: CacheValueMetadata, val channel : FileChannel, val offset : Long, val size : Long) : Serializable class EntryValue(val metadata: CacheValueMetadata, val channel : FileChannel, val offset : Long, val size : Long) : Serializable
@@ -112,9 +114,18 @@ class FileSystemCache(
return FileSink(metadata, file, tmpFile) return FileSink(metadata, file, tmpFile)
} }
private val garbageCollector = Thread.ofVirtual().name("file-system-cache-gc").start { private val closeFuture = object : CompletableFuture<Void>() {
while (running) { init {
gc() Thread.ofVirtual().name("file-system-cache-gc").start {
try {
while (running) {
gc()
}
complete(null)
} catch (ex : Throwable) {
completeExceptionally(ex)
}
}
} }
} }
@@ -151,8 +162,8 @@ class FileSystemCache(
return result return result
} }
override fun close() { override fun asyncClose() : CompletableFuture<Void> {
running = false running = false
garbageCollector.join() return closeFuture
} }
} }

View File

@@ -1,5 +1,9 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import io.netty.channel.ChannelFactory
import io.netty.channel.EventLoopGroup
import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
@@ -19,11 +23,13 @@ data class FileSystemCacheConfiguration(
override fun materialize() = object : CacheHandlerFactory { override fun materialize() = object : CacheHandlerFactory {
private val cache = FileSystemCache(root ?: Application.builder("rbcs").build().computeCacheDirectory(), maxAge) private val cache = FileSystemCache(root ?: Application.builder("rbcs").build().computeCacheDirectory(), maxAge)
override fun close() { override fun asyncClose() = cache.asyncClose()
cache.close()
}
override fun newHandler() = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, chunkSize) override fun newHandler(
eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel>
) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, chunkSize)
} }
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI

View File

@@ -12,7 +12,7 @@ import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> { class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd" override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
override fun getXmlType() = "fileSystemCacheType" override fun getXmlType() = "fileSystemCacheType"
@@ -30,7 +30,7 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
val compressionLevel = el.renderAttribute("compression-level") val compressionLevel = el.renderAttribute("compression-level")
?.let(String::toInt) ?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION ?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5" val digestAlgorithm = el.renderAttribute("digest")
val chunkSize = el.renderAttribute("chunk-size") val chunkSize = el.renderAttribute("chunk-size")
?.let(Integer::decode) ?.let(Integer::decode)
?: 0x10000 ?: 0x10000

View File

@@ -1,10 +1,12 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -26,7 +28,7 @@ class CacheEntry(
class InMemoryCache( class InMemoryCache(
private val maxAge: Duration, private val maxAge: Duration,
private val maxSize: Long private val maxSize: Long
) : AutoCloseable { ) : AsyncCloseable {
companion object { companion object {
private val log = createLogger<InMemoryCache>() private val log = createLogger<InMemoryCache>()
@@ -45,26 +47,35 @@ class InMemoryCache(
@Volatile @Volatile
private var running = true private var running = true
private val garbageCollector = Thread.ofVirtual().name("in-memory-cache-gc").start { private val closeFuture = object : CompletableFuture<Void>() {
while (running) { init {
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue Thread.ofVirtual().name("in-memory-cache-gc").start {
val value = el.value try {
val now = Instant.now() while (running) {
if (now > el.expiry) { val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue
val removed = map.remove(el.key, value) val value = el.value
if (removed) { val now = Instant.now()
updateSizeAfterRemoval(value.content) if (now > el.expiry) {
//Decrease the reference count for map val removed = map.remove(el.key, value)
value.content.release() if (removed) {
updateSizeAfterRemoval(value.content)
//Decrease the reference count for map
value.content.release()
}
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
}
}
complete(null)
} catch (ex: Throwable) {
completeExceptionally(ex)
} }
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
} }
} }
} }
private fun removeEldest(): Long { fun removeEldest(): Long {
while (true) { while (true) {
val el = removalQueue.take() val el = removalQueue.take()
val value = el.value val value = el.value
@@ -84,9 +95,9 @@ class InMemoryCache(
} }
} }
override fun close() { override fun asyncClose() : CompletableFuture<Void> {
running = false running = false
garbageCollector.join() return closeFuture
} }
fun get(key: ByteArray) = map[CacheKey(key)]?.run { fun get(key: ByteArray) = map[CacheKey(key)]?.run {

View File

@@ -1,5 +1,10 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import io.netty.channel.ChannelFactory
import io.netty.channel.EventLoopGroup
import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel
import io.netty.util.concurrent.Future
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
@@ -16,11 +21,13 @@ data class InMemoryCacheConfiguration(
override fun materialize() = object : CacheHandlerFactory { override fun materialize() = object : CacheHandlerFactory {
private val cache = InMemoryCache(maxAge, maxSize) private val cache = InMemoryCache(maxAge, maxSize)
override fun close() { override fun asyncClose() = cache.asyncClose()
cache.close()
}
override fun newHandler() = InMemoryCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel) override fun newHandler(
eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel>
) = InMemoryCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel)
} }
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI

View File

@@ -91,6 +91,7 @@ class InMemoryCacheHandler(
val buf = ctx.alloc().heapBuffer() val buf = ctx.alloc().heapBuffer()
InflaterOutputStream(ByteBufOutputStream(buf)).use { InflaterOutputStream(ByteBufOutputStream(buf)).use {
value.content.readBytes(it, value.content.readableBytes()) value.content.readBytes(it, value.content.readableBytes())
value.content.release()
buf.retain() buf.retain()
} }
ctx.writeAndFlush(LastCacheContent(buf)) ctx.writeAndFlush(LastCacheContent(buf))

View File

@@ -11,7 +11,7 @@ import java.util.zip.Deflater
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> { class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd" override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
override fun getXmlType() = "inMemoryCacheType" override fun getXmlType() = "inMemoryCacheType"
@@ -30,7 +30,7 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
val compressionLevel = el.renderAttribute("compression-level") val compressionLevel = el.renderAttribute("compression-level")
?.let(String::toInt) ?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION ?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5" val digestAlgorithm = el.renderAttribute("digest")
val chunkSize = el.renderAttribute("chunk-size") val chunkSize = el.renderAttribute("chunk-size")
?.let(Integer::decode) ?.let(Integer::decode)
?: 0x10000 ?: 0x10000

View File

@@ -27,8 +27,6 @@ object Parser {
val root = document.documentElement val root = document.documentElement
val anonymousUser = User("", null, emptySet(), null) 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(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
@@ -113,10 +111,6 @@ object Parser {
} }
"connection" -> { "connection" -> {
val writeTimeout = child.renderAttribute("write-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val readTimeout = child.renderAttribute("read-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val idleTimeout = child.renderAttribute("idle-timeout") val idleTimeout = child.renderAttribute("idle-timeout")
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
val readIdleTimeout = child.renderAttribute("read-idle-timeout") val readIdleTimeout = child.renderAttribute("read-idle-timeout")
@@ -126,8 +120,6 @@ object Parser {
val maxRequestSize = child.renderAttribute("max-request-size") val maxRequestSize = child.renderAttribute("max-request-size")
?.let(Integer::decode) ?: 0x4000000 ?.let(Integer::decode) ?: 0x4000000
connection = Configuration.Connection( connection = Configuration.Connection(
readTimeout,
writeTimeout,
idleTimeout, idleTimeout,
readIdleTimeout, readIdleTimeout,
writeIdleTimeout, writeIdleTimeout,
@@ -201,6 +193,7 @@ object Parser {
when (it.localName) { when (it.localName) {
"reader" -> Role.Reader "reader" -> Role.Reader
"writer" -> Role.Writer "writer" -> Role.Writer
"healthcheck" -> Role.Healthcheck
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'") else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
} }
}.toSet() }.toSet()

View File

@@ -36,8 +36,6 @@ object Serializer {
} }
node("connection") { node("connection") {
conf.connection.let { connection -> conf.connection.let { connection ->
attr("read-timeout", connection.readTimeout.toString())
attr("write-timeout", connection.writeTimeout.toString())
attr("idle-timeout", connection.idleTimeout.toString()) attr("idle-timeout", connection.idleTimeout.toString())
attr("read-idle-timeout", connection.readIdleTimeout.toString()) attr("read-idle-timeout", connection.readIdleTimeout.toString())
attr("write-idle-timeout", connection.writeIdleTimeout.toString()) attr("write-idle-timeout", connection.writeIdleTimeout.toString())

View File

@@ -135,13 +135,13 @@ class ServerHandler(private val serverPrefix: Path) :
setRequestMetadata(msg) setRequestMetadata(msg)
val method = msg.method() val method = msg.method()
if (method === HttpMethod.GET) { if (method === HttpMethod.GET) {
val path = Path.of(msg.uri()) val path = Path.of(msg.uri()).normalize()
val prefix = path.parent if (path.startsWith(serverPrefix)) {
if (serverPrefix == prefix) { val relativePath = serverPrefix.relativize(path)
val key = relativePath.toString()
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler) ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
path.fileName?.toString() key.let(::CacheGetRequest)
?.let(::CacheGetRequest) .let(ctx::fireChannelRead)
?.let(ctx::fireChannelRead)
?: ctx.channel().write(CacheValueNotFoundResponse()) ?: ctx.channel().write(CacheValueNotFoundResponse())
} else { } else {
log.warn(ctx) { log.warn(ctx) {
@@ -152,11 +152,10 @@ class ServerHandler(private val serverPrefix: Path) :
ctx.writeAndFlush(response) ctx.writeAndFlush(response)
} }
} else if (method === HttpMethod.PUT) { } else if (method === HttpMethod.PUT) {
val path = Path.of(msg.uri()) val path = Path.of(msg.uri()).normalize()
val prefix = path.parent if (path.startsWith(serverPrefix)) {
val key = path.fileName.toString() val relativePath = serverPrefix.relativize(path)
val key = relativePath.toString()
if (serverPrefix == prefix) {
log.debug(ctx) { log.debug(ctx) {
"Added value for key '$key' to build cache" "Added value for key '$key' to build cache"
} }

View File

@@ -2,18 +2,7 @@
<rbcs:server <rbcs:server
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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="8080" incoming-connections-backlog-size="1024"/> <bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
<connection <cache xs:type="rbcs:fileSystemCacheType" path="${sys:java.io.tmpdir}/rbcs" max-age="P7D"/>
max-request-size="67108864"
idle-timeout="PT30S"
read-timeout="PT10S"
write-timeout="PT10S"
read-idle-timeout="PT60S"
write-idle-timeout="PT60S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authentication>
<none/>
</authentication>
</rbcs:server> </rbcs:server>

View File

@@ -0,0 +1,659 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:rbcs="urn:net.woggioni.rbcs.server"
elementFormDefault="unqualified">
<xs:element name="server" type="rbcs:serverType">
<xs:annotation>
<xs:documentation>
Root element containing the server configuration
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:complexType name="serverType">
<xs:sequence minOccurs="0">
<xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="rbcs:cacheType" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Cache storage backend implementation to use, more implementations can be added through
the use of plugins
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="authorization" type="rbcs:authorizationType" minOccurs="0">
<xs:key name="userId">
<xs:selector xpath="users/user"/>
<xs:field xpath="@name"/>
</xs:key>
<xs:keyref name="userRef" refer="rbcs:userId">
<xs:selector xpath="groups/group/users/user"/>
<xs:field xpath="@ref"/>
</xs:keyref>
</xs:element>
<xs:element name="authentication" type="rbcs:authenticationType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Mechanism to use to assign a username to a specific client
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tls" type="rbcs:tlsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Use TLS to encrypt all the communications
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="path" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
URI path prefix, if your rbcs is hosted at "http://www.example.com"
and this parameter is set to "cache", then all the requests will need to be sent at
"http://www.example.com/cache/KEY", where "KEY" is the cache entry KEY
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="bindType">
<xs:attribute name="host" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>Server bind address</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="port" type="xs:unsignedShort" use="required">
<xs:annotation>
<xs:documentation>Server port number</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024">
<xs:annotation>
<xs:documentation>
The maximum queue length for incoming connection indications (a request to connect) is set to
the backlog parameter. If a connection indication arrives when the queue is full,
the connection is refused.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
<xs:annotation>
<xs:documentation>
The server will close the connection with the client
when neither a read nor a write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The server will close the connection with the client
when no read was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The server will close the connection with the client
when no write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-request-size" type="rbcs:byteSizeType" use="optional" default="0x4000000">
<xs:annotation>
<xs:documentation>
The maximum request body size the server will accept from a client
(if exceeded the server returns 413 HTTP status code)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="eventExecutorType">
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true">
<xs:annotation>
<xs:documentation>
Whether or not to use virtual threads for the execution of the core server handler
(not for the I/O operations)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="cacheType" abstract="true"/>
<xs:complexType name="inMemoryCacheType">
<xs:annotation>
<xs:documentation>
A simple cache implementation that uses a java.util.ConcurrentHashMap as a storage backend
</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="rbcs:cacheType">
<xs:attribute name="max-age" type="xs:duration" default="P1D">
<xs:annotation>
<xs:documentation>
Values will be removed from the cache after this amount of time
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-size" type="rbcs:byteSizeType" default="0x1000000">
<xs:annotation>
<xs:documentation>
The maximum allowed total size of the cache in bytes, old values will be purged from the cache
when the insertion of a new value causes this limit to be exceeded
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="digest" type="xs:token">
<xs:annotation>
<xs:documentation>
Hashing algorithm to apply to the key. If omitted, no hashing is performed.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
<xs:annotation>
<xs:documentation>
Enable deflate compression for stored cache elements
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1">
<xs:annotation>
<xs:documentation>
Deflate compression level to use for cache compression,
use -1 to use the default compression level of java.util.zip.Deflater
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
<xs:annotation>
<xs:documentation>
Maximum byte size of socket write calls
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="fileSystemCacheType">
<xs:annotation>
<xs:documentation>
A simple cache implementation that stores data in a folder on the filesystem
</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="rbcs:cacheType">
<xs:attribute name="path" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
File system path that will be used to store the cache data files
(it will be created if it doesn't already exist)
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-age" type="xs:duration" default="P1D">
<xs:annotation>
<xs:documentation>
Values will be removed from the cache after this amount of time
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="digest" type="xs:token" default="SHA3-224">
<xs:annotation>
<xs:documentation>
Hashing algorithm to apply to the key. If omitted, no hashing is performed.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
<xs:annotation>
<xs:documentation>
Enable deflate compression for stored cache elements
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1">
<xs:annotation>
<xs:documentation>
Deflate compression level to use for cache compression,
use -1 to use the default compression level of java.util.zip.Deflater
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
<xs:annotation>
<xs:documentation>
Maximum byte size of a cache value that will be stored in memory
(reduce it to reduce memory consumption, increase it for increased throughput)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence>
<xs:element name="group-extractor" type="rbcs:X500NameExtractorType" minOccurs="0">
<xs:annotation>
<xs:documentation>
A regex based extractor that will be used to determine which group the client belongs to,
based on the X.500 name of the subject field in the client's TLS certificate.
When this is set RBAC works even if the user isn't listed in the &lt;users/&gt; section as
the client will be assigned role solely based on the group he is found to belong to.
Note that this does not allow for a client to be part of multiple groups.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="user-extractor" type="rbcs:X500NameExtractorType" minOccurs="0">
<xs:annotation>
<xs:documentation>
A regex based extractor that will be used to assign a user to a connected client,
based on the X.500 name of the subject field in the client's TLS certificate.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="X500NameExtractorType">
<xs:annotation>
<xs:documentation>
Extract informations from a client TLS certificates using
regular expressions applied to the X.500 name "Subject" field
</xs:documentation>
</xs:annotation>
<xs:attribute name="attribute-name" type="xs:token">
<xs:annotation>
<xs:documentation>
X.500 name attribute to apply the regex
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="pattern" type="xs:token">
<xs:annotation>
<xs:documentation>
Regex that wil be applied to the attribute value,
use regex groups to extract relevant data
(note that only the first group that appears in the regex is used)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="authorizationType">
<xs:all>
<xs:element name="users" type="rbcs:usersType">
<xs:annotation>
<xs:documentation>
List of users registered in the application
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="groups" type="rbcs:groupsType">
<xs:annotation>
<xs:documentation>
List of user groups registered in the application
</xs:documentation>
</xs:annotation>
<xs:unique name="groupKey">
<xs:selector xpath="group"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="authenticationType">
<xs:annotation>
<xs:documentation>
Authentication mechanism to assign usernames and groups to clients
</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="basic">
<xs:annotation>
<xs:documentation>
Enable HTTP basic authentication
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="client-certificate" type="rbcs:tlsCertificateAuthorizationType">
<xs:annotation>
<xs:documentation>
Enable TLS certificate authentication
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="none">
<xs:annotation>
<xs:documentation>
Disable authentication altogether
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:complexType name="quotaType">
<xs:annotation>
<xs:documentation>
Defines a quota for a user or a group
</xs:documentation>
</xs:annotation>
<xs:attribute name="calls" type="xs:positiveInteger" use="required">
<xs:annotation>
<xs:documentation>
Maximum number of allowed calls in a given period
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="period" type="xs:duration" use="required">
<xs:annotation>
<xs:documentation>
The period length
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-available-calls" type="xs:positiveInteger" use="optional">
<xs:annotation>
<xs:documentation>
Maximum number of available calls that can be accumulated
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="initial-available-calls" type="xs:unsignedInt" use="optional">
<xs:annotation>
<xs:documentation>
Number of available calls for users at their first call
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="anonymousUserType">
<xs:annotation>
<xs:documentation>
Placeholder for a client that is not authenticated
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Calls quota for the user
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userType">
<xs:annotation>
<xs:documentation>
An authenticated user
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Calls quota for the user
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
User's name
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
User's password hash used for HTTP basic authentication, this has to be generated with
the `password` subcommand of `rbcs-cli`
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="usersType">
<xs:annotation>
<xs:documentation>
List of registered users, add an &lt;anonymous&gt; tag to enable authenticated user access
when authentication is enabled
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="user" type="rbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="anonymous" type="rbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupsType">
<xs:annotation>
<xs:documentation>
List of registered user groups
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="group" type="rbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupType">
<xs:annotation>
<xs:documentation>
The definition of a user group, with the list of its member users
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="users" type="rbcs:userRefsType" maxOccurs="1" minOccurs="0">
<xs:unique name="userRefWriterKey">
<xs:selector xpath="user"/>
<xs:field xpath="@ref"/>
</xs:unique>
</xs:element>
<xs:element name="roles" type="rbcs:rolesType" maxOccurs="1" minOccurs="0">
<xs:annotation>
<xs:documentation>
The list of application roles awarded to all the members of this group
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="user-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
The call quota for each user in this group
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="group-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
The cumulative call quota for all users in this group
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:token">
<xs:annotation>
<xs:documentation>
The group's name
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="rolesType">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="writer"/>
<xs:element name="reader"/>
<xs:element name="healthcheck"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefsType">
<xs:annotation>
<xs:documentation>
A list of references to users in the &lt;users&gt; section
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="user" type="rbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefType">
<xs:annotation>
<xs:documentation>
A reference to a user in the &lt;users&gt; section
</xs:documentation>
</xs:annotation>
<xs:attribute name="ref" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Name of the referenced user
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="tlsType">
<xs:annotation>
<xs:documentation>
Enable TLS protocol
</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="keystore" type="rbcs:keyStoreType" >
<xs:annotation>
<xs:documentation>
Path to the keystore file that contains the server's key and certificate
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="truststore" type="rbcs:trustStoreType" minOccurs="0">
<xs:annotation>
<xs:documentation>
Path to the truststore file that contains the trusted CAs
for TLS client certificate verification
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="keyStoreType">
<xs:attribute name="file" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
System path to the keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string">
<xs:annotation>
<xs:documentation>
Password to open they keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Alias of the keystore entry containing the private key
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-password" type="xs:string">
<xs:annotation>
<xs:documentation>
Private key entry's encryption password
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Path to the trustore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string">
<xs:annotation>
<xs:documentation>
Trustore file password
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="check-certificate-status" type="xs:boolean">
<xs:annotation>
<xs:documentation>
Whether or not check the certificate validity using CRL/OCSP
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="require-client-certificate" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>
If true, the server requires a TLS client certificate from the client and simply refuses to connect
when a client certificate isn't provided
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="propertiesType">
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="rbcs:propertyType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="propertyType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="key" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="hostAndPortType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType>
<xs:simpleType name="byteSizeType">
<xs:restriction base="xs:token">
<xs:pattern value="(0x[a-f0-9]+|[0-9]+)"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="compressionLevelType">
<xs:restriction base="xs:integer">
<xs:minInclusive value="-1"/>
<xs:maxInclusive value="9"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -1,238 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:rbcs="urn:net.woggioni.rbcs.server"
elementFormDefault="unqualified">
<xs:element name="server" type="rbcs:serverType"/>
<xs:complexType name="serverType">
<xs:sequence minOccurs="0">
<xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="rbcs:cacheType" maxOccurs="1"/>
<xs:element name="authorization" type="rbcs:authorizationType" minOccurs="0">
<xs:key name="userId">
<xs:selector xpath="users/user"/>
<xs:field xpath="@name"/>
</xs:key>
<xs:keyref name="userRef" refer="rbcs:userId">
<xs:selector xpath="groups/group/users/user"/>
<xs:field xpath="@ref"/>
</xs:keyref>
</xs:element>
<xs:element name="authentication" type="rbcs:authenticationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls" type="rbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="path" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="bindType">
<xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024"/>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="max-request-size" type="rbcs:byteSizeType" use="optional" default="0x4000000"/>
</xs:complexType>
<xs:complexType name="eventExecutorType">
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true"/>
</xs:complexType>
<xs:complexType name="cacheType" abstract="true"/>
<xs:complexType name="inMemoryCacheType">
<xs:complexContent>
<xs:extension base="rbcs:cacheType">
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="max-size" type="rbcs:byteSizeType" default="0x1000000"/>
<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="rbcs:compressionLevelType" default="-1"/>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="fileSystemCacheType">
<xs:complexContent>
<xs:extension base="rbcs:cacheType">
<xs:attribute name="path" type="xs:string" use="optional"/>
<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="rbcs:compressionLevelType" default="-1"/>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence>
<xs:element name="group-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/>
<xs:element name="user-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="X500NameExtractorType">
<xs:attribute name="attribute-name" type="xs:token"/>
<xs:attribute name="pattern" type="xs:token"/>
</xs:complexType>
<xs:complexType name="authorizationType">
<xs:all>
<xs:element name="users" type="rbcs:usersType"/>
<xs:element name="groups" type="rbcs:groupsType">
<xs:unique name="groupKey">
<xs:selector xpath="group"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="authenticationType">
<xs:choice>
<xs:element name="basic"/>
<xs:element name="client-certificate" type="rbcs:tlsCertificateAuthorizationType"/>
<xs:element name="none"/>
</xs:choice>
</xs:complexType>
<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:unsignedInt" use="optional"/>
</xs:complexType>
<xs:complexType name="anonymousUserType">
<xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userType">
<xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="usersType">
<xs:sequence>
<xs:element name="user" type="rbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="anonymous" type="rbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupsType">
<xs:sequence>
<xs:element name="group" type="rbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupType">
<xs:sequence>
<xs:element name="users" type="rbcs:userRefsType" maxOccurs="1" minOccurs="0">
<xs:unique name="userRefWriterKey">
<xs:selector xpath="user"/>
<xs:field xpath="@ref"/>
</xs:unique>
</xs:element>
<xs:element name="roles" type="rbcs:rolesType" maxOccurs="1" minOccurs="0"/>
<xs:element name="user-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
<xs:element name="group-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
</xs:complexType>
<xs:simpleType name="role" final="restriction" >
<xs:restriction base="xs:token">
<xs:enumeration value="READER" />
<xs:enumeration value="WRITER" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="rolesType">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="writer"/>
<xs:element name="reader"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefsType">
<xs:sequence>
<xs:element name="user" type="rbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefType">
<xs:attribute name="ref" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsType">
<xs:all>
<xs:element name="keystore" type="rbcs:keyStoreType" />
<xs:element name="truststore" type="rbcs:trustStoreType" minOccurs="0"/>
</xs:all>
</xs:complexType>
<xs:complexType name="keyStoreType">
<xs:attribute name="file" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string"/>
<xs:attribute name="key-alias" type="xs:string" use="required"/>
<xs:attribute name="key-password" type="xs:string"/>
</xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string"/>
<xs:attribute name="check-certificate-status" type="xs:boolean"/>
<xs:attribute name="require-client-certificate" type="xs:boolean" use="optional" default="false"/>
</xs:complexType>
<xs:complexType name="propertiesType">
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="rbcs:propertyType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="propertyType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="key" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="hostAndPortType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType>
<xs:simpleType name="byteSizeType">
<xs:restriction base="xs:token">
<xs:pattern value="(0x[a-f0-9]+|[0-9]+)"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="compressionLevelType">
<xs:restriction base="xs:integer">
<xs:minInclusive value="-1"/>
<xs:maxInclusive value="9"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -1,30 +0,0 @@
package net.woggioni.rbcs.server.test.utils;
import net.woggioni.jwo.JWO;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
public class NetworkUtils {
private static final int MAX_ATTEMPTS = 50;
public static int getFreePort() {
int count = 0;
while(count < MAX_ATTEMPTS) {
try (ServerSocket serverSocket = new ServerSocket(0, 50, InetAddress.getLocalHost())) {
final var candidate = serverSocket.getLocalPort();
if (candidate > 0) {
return candidate;
} else {
JWO.newThrowable(RuntimeException.class, "Got invalid port number: %d", candidate);
throw new RuntimeException("Error trying to find an open port");
}
} catch (IOException ignored) {
++count;
}
}
throw new RuntimeException("Error trying to find an open port");
}
}

View File

@@ -2,10 +2,10 @@ package net.woggioni.rbcs.server.test
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.NetworkUtils
import java.net.URI import java.net.URI
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -33,13 +33,11 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
this.cacheDir = testDir.resolve("cache") this.cacheDir = testDir.resolve("cache")
cfg = Configuration.of( cfg = Configuration.of(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), getFreePort(),
50, 50,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.Connection( Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),

View File

@@ -2,12 +2,12 @@ package net.woggioni.rbcs.server.test
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.CertificateUtils import net.woggioni.rbcs.server.test.utils.CertificateUtils
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials
import net.woggioni.rbcs.server.test.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
@@ -47,6 +47,7 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null, null) protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null, null)
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null) protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null)
protected val healthCheckGroup = Configuration.Group("healthcheckers", setOf(Role.Healthcheck), null, 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
@@ -138,13 +139,11 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
createKeyStoreAndTrustStore() createKeyStoreAndTrustStore()
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), getFreePort(),
100, 100,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.Connection( Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),

View File

@@ -2,10 +2,10 @@ package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.NetworkUtils
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -33,13 +33,11 @@ class NoAuthServerTest : AbstractServerTest() {
this.cacheDir = testDir.resolve("cache") this.cacheDir = testDir.resolve("cache")
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
NetworkUtils.getFreePort(), getFreePort(),
100, 100,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.Connection( Configuration.Connection(
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(10, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
@@ -120,6 +118,56 @@ class NoAuthServerTest : AbstractServerTest() {
@Test @Test
@Order(4) @Order(4)
fun getUnhandledPath() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, _) = newEntry(random)
val requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/some/other/path/$key"))
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.BAD_REQUEST.code(), response.statusCode())
}
@Test
@Order(5)
fun putUnhandledPath() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = newEntry(random)
val requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/some/other/path/$key"))
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.BAD_REQUEST.code(), response.statusCode())
}
@Test
@Order(6)
fun getRelativeUnhandledPath() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, _) = newEntry(random)
val requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/some/nested/path/../../../some/other/path/$key"))
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.BAD_REQUEST.code(), response.statusCode())
}
@Test
@Order(7)
fun getRelativePath() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/some/other/path/../../nested/path/$key"))
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
Assertions.assertArrayEquals(value, response.body())
}
@Test
@Order(10)
fun traceTest() { fun traceTest() {
val client: HttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build() val client: HttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build()
val requestBuilder = newRequestBuilder("").method( val requestBuilder = newRequestBuilder("").method(

View File

@@ -18,6 +18,7 @@ class TlsServerTest : AbstractTlsServerTest() {
Configuration.User("user1", null, setOf(readersGroup), null), Configuration.User("user1", null, setOf(readersGroup), null),
Configuration.User("user2", null, setOf(writersGroup), null), Configuration.User("user2", null, setOf(writersGroup), null),
Configuration.User("user3", null, setOf(readersGroup, writersGroup), null), Configuration.User("user3", null, setOf(readersGroup, writersGroup), null),
Configuration.User("user4", null, setOf(healthCheckGroup), null),
Configuration.User("", null, setOf(readersGroup), null) Configuration.User("", null, setOf(readersGroup), null)
) )
@@ -140,7 +141,24 @@ class TlsServerTest : AbstractTlsServerTest() {
val client: HttpClient = getHttpClient(null) val client: HttpClient = getHttpClient(null)
val requestBuilder = newRequestBuilder("").method( val requestBuilder = newRequestBuilder("").method(
"TRACE", "TRACE",
HttpRequest.BodyPublishers.ofByteArray("sfgsdgfaiousfiuhsd".toByteArray()) HttpRequest.BodyPublishers.ofByteArray("this is an healthcheck".toByteArray())
)
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
}
@Test
@Order(9)
fun traceAsHealthcheckUser() {
val user = cfg.users.values.find {
Role.Healthcheck in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder("").method(
"TRACE",
HttpRequest.BodyPublishers.ofByteArray("this is an healthcheck".toByteArray())
) )
val response: HttpResponse<ByteArray> = val response: HttpResponse<ByteArray> =

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authorization> <authorization>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authorization> <authorization>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authorization> <authorization>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authorization> <authorization>

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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"/>
<connection <connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"

View File

@@ -2,16 +2,14 @@
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:rbcs="urn:net.woggioni.rbcs.server"
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache" xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache"
xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd" xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd 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="8443" incoming-connections-backlog-size="4096"/> <bind host="0.0.0.0" port="8443" incoming-connections-backlog-size="4096"/>
<connection <connection
max-request-size="67108864" max-request-size="67108864"
idle-timeout="PT30S" idle-timeout="PT30S"
read-idle-timeout="PT60S" read-idle-timeout="PT60S"
write-idle-timeout="PT60S" write-idle-timeout="PT60S"/>
read-timeout="PT5M"
write-timeout="PT5M"/>
<event-executor use-virtual-threads="true"/> <event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="123"> <cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="123">
<server host="memcached" port="11211"/> <server host="memcached" port="11211"/>

View File

@@ -2,11 +2,9 @@
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:rbcs="urn:net.woggioni.rbcs.server"
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache" xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache"
xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd 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="50"/> <bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="50"/>
<connection <connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" 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.xsd"> 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="180"/> <bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="180"/>
<connection <connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"

View File

@@ -1,3 +1,10 @@
# RBCS servlet
This is a minimal implementation of RBCs using Jakarta servlet API, it relies
on the servlet container (Tomcat in this example) for authentication, authorization,
throttling, encryption and compression. It only supports in-memory caching.
The main purpose is to provide a performance comparison for RBCS Netty implementation.
## How to run ## How to run
```bash ```bash