Skip to content

Commit bba0bee

Browse files
committed
minimal impl
1 parent 5567e0d commit bba0bee

33 files changed

Lines changed: 2021 additions & 101 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.server;
18+
19+
import com.linecorp.armeria.common.annotation.Nullable;
20+
import com.linecorp.armeria.common.annotation.UnstableApi;
21+
22+
@UnstableApi
23+
interface ConnectionLevelSetters {
24+
25+
@Nullable
26+
ConnectionAcceptor connectionAcceptor();
27+
28+
ConnectionLevelSetters connectionAcceptor(ConnectionAcceptor connectionAcceptor);
29+
30+
@Nullable
31+
ServerTlsProvider tlsProvider();
32+
33+
ConnectionLevelSetters tlsProvider(ServerTlsProvider serverTlsProvider);
34+
}

core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@
172172
*
173173
* @see VirtualHostBuilder
174174
*/
175-
public final class ServerBuilder implements TlsSetters, ServiceConfigsBuilder<ServerBuilder> {
175+
public final class ServerBuilder implements ConnectionLevelSetters, TlsSetters,
176+
ServiceConfigsBuilder<ServerBuilder> {
176177
private static final Logger logger = LoggerFactory.getLogger(ServerBuilder.class);
177178

178179
// Defaults to no graceful shutdown.
@@ -531,12 +532,22 @@ public ServerBuilder childChannelPipelineCustomizer(
531532
* Sets the {@link ConnectionAcceptor} that is called once per connection, before TLS
532533
* negotiation, to decide whether to accept the connection.
533534
*/
535+
@Override
534536
@UnstableApi
535537
public ServerBuilder connectionAcceptor(ConnectionAcceptor connectionAcceptor) {
536538
this.connectionAcceptor = requireNonNull(connectionAcceptor, "connectionAcceptor");
537539
return this;
538540
}
539541

542+
/**
543+
* Returns the {@link ConnectionAcceptor} configured so far.
544+
*/
545+
@Override
546+
@UnstableApi
547+
public ConnectionAcceptor connectionAcceptor() {
548+
return connectionAcceptor;
549+
}
550+
540551
/**
541552
* Adds a {@link ServerPlugin} that will be installed during {@link Server} construction
542553
* and during {@link Server#reconfigure(ServerConfigurator)}.
@@ -1243,6 +1254,16 @@ public ServerBuilder tls(KeyManagerFactory keyManagerFactory) {
12431254
return this;
12441255
}
12451256

1257+
/**
1258+
* Returns the {@link ServerTlsProvider} configured so far, or {@code null} if not set.
1259+
*/
1260+
@Override
1261+
@UnstableApi
1262+
@Nullable
1263+
public ServerTlsProvider tlsProvider() {
1264+
return serverTlsProviderBuilder.serverTlsProvider;
1265+
}
1266+
12461267
/**
12471268
* Sets the specified {@link TlsProvider} which will be used for building an {@link SslContext} of
12481269
* a hostname.
@@ -1322,6 +1343,7 @@ public ServerBuilder tlsProvider(TlsProvider tlsProvider, ServerTlsConfig tlsCon
13221343
* the {@code tlsProvider} takes priority. The static TLS settings are used as a fallback
13231344
* when the provider returns {@code null}.
13241345
*/
1346+
@Override
13251347
@UnstableApi
13261348
public ServerBuilder tlsProvider(ServerTlsProvider serverTlsProvider) {
13271349
requireNonNull(serverTlsProvider, "serverTlsProvider");

core/src/main/java/com/linecorp/armeria/server/ServerTlsProvider.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ static ServerTlsProvider of(Function<? super ConnectionContext, @Nullable Server
5656
}
5757

5858
/**
59-
* Returns a {@link ServerTlsSpec} for the given {@link ConnectionContext}, or {@code null}
60-
* if this provider cannot handle the connection. When {@code null} is returned,
61-
* the server falls back to per-VirtualHost TLS settings.
59+
* Returns a {@link CompletableFuture} that completes with a {@link ServerTlsSpec} for the
60+
* given {@link ConnectionContext}, or with {@code null} if this provider cannot handle the
61+
* connection. When the future completes with {@code null}, the server falls back to
62+
* per-VirtualHost TLS settings.
6263
*
6364
* <p>This method is called by the server pipeline for each new TLS connection.
6465
* Implementations can inspect connection properties such as SNI hostname, ALPN protocols,
6566
* and custom attributes to determine the appropriate TLS configuration.
67+
*
68+
* @return a non-null {@link CompletableFuture} that may complete with a {@code null}
69+
* {@link ServerTlsSpec}
6670
*/
6771
CompletableFuture<@Nullable ServerTlsSpec> serverTlsSpec(ConnectionContext ctx);
6872
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.xds.it;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.nio.file.Path;
22+
23+
import org.junit.jupiter.api.Order;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.RegisterExtension;
26+
27+
import com.linecorp.armeria.client.ClientFactory;
28+
import com.linecorp.armeria.client.WebClient;
29+
import com.linecorp.armeria.common.AggregatedHttpResponse;
30+
import com.linecorp.armeria.common.HttpResponse;
31+
import com.linecorp.armeria.common.HttpStatus;
32+
import com.linecorp.armeria.server.ServerBuilder;
33+
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
34+
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
35+
import com.linecorp.armeria.xds.XdsBootstrap;
36+
import com.linecorp.armeria.xds.server.XdsServerPlugin;
37+
38+
import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap;
39+
40+
/**
41+
* Tests the server-side xDS flow: {@link XdsConnectionConfig} subscribes to a
42+
* statically-configured listener with a {@code DownstreamTlsContext}, and the
43+
* Armeria server uses the xDS-provided TLS certificate.
44+
*
45+
* <p>The client trusts only the xDS cert. If a wrong cert were presented,
46+
* the TLS handshake would fail.
47+
*/
48+
class ServerConnectionConfigTest {
49+
50+
// xDS cert — pushed via DownstreamTlsContext. The client trusts only this cert.
51+
// Uses 127.0.0.1 as the CN so it passes hostname verification when connecting to localhost.
52+
@RegisterExtension
53+
@Order(0)
54+
static final SelfSignedCertificateExtension xdsCert =
55+
new SelfSignedCertificateExtension("127.0.0.1");
56+
57+
@RegisterExtension
58+
@Order(1)
59+
static final ServerExtension server = new ServerExtension() {
60+
@Override
61+
protected void configure(ServerBuilder sb) throws Exception {
62+
final Path certPath = xdsCert.certificateFile().toPath();
63+
final Path keyPath = xdsCert.privateKeyFile().toPath();
64+
65+
//language=YAML
66+
final String bootstrapYaml =
67+
"""
68+
static_resources:
69+
listeners:
70+
- name: server-listener
71+
filter_chains: []
72+
default_filter_chain:
73+
transport_socket:
74+
name: envoy.transport_sockets.downstream_tls
75+
typed_config:
76+
"@type": type.googleapis.com/envoy.extensions.transport_sockets\
77+
.tls.v3.DownstreamTlsContext
78+
common_tls_context:
79+
tls_certificates:
80+
- certificate_chain:
81+
filename: "%s"
82+
private_key:
83+
filename: "%s"
84+
""".formatted(certPath, keyPath);
85+
86+
final Bootstrap bootstrap = XdsResourceReader.fromYaml(bootstrapYaml, Bootstrap.class);
87+
final XdsBootstrap xdsBootstrap = XdsBootstrap.builder(bootstrap).build();
88+
89+
// The plugin registers the port, TlsProvider, and server listener.
90+
sb.plugin(XdsServerPlugin.of(xdsBootstrap, "server-listener"));
91+
92+
sb.service("/hello", (ctx, req) -> HttpResponse.of("hello from xds"));
93+
}
94+
};
95+
96+
@Test
97+
void serverPresentsXdsCert() {
98+
// The client trusts only the xDS cert.
99+
// If the server were to present a different cert, the TLS handshake would fail.
100+
final ClientFactory factory =
101+
ClientFactory.builder()
102+
.tlsCustomizer(b -> b.trustManager(xdsCert.certificateFile()))
103+
.build();
104+
try {
105+
final AggregatedHttpResponse res =
106+
WebClient.builder(server.httpsUri())
107+
.factory(factory)
108+
.build()
109+
.blocking()
110+
.get("/hello");
111+
assertThat(res.status()).isEqualTo(HttpStatus.OK);
112+
assertThat(res.contentUtf8()).isEqualTo("hello from xds");
113+
} finally {
114+
factory.close();
115+
}
116+
}
117+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.xds.it;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.nio.file.Path;
22+
23+
import org.junit.jupiter.api.Order;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.RegisterExtension;
26+
27+
import com.linecorp.armeria.client.ClientFactory;
28+
import com.linecorp.armeria.client.WebClient;
29+
import com.linecorp.armeria.common.AggregatedHttpResponse;
30+
import com.linecorp.armeria.common.HttpResponse;
31+
import com.linecorp.armeria.common.HttpStatus;
32+
import com.linecorp.armeria.server.ServerBuilder;
33+
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
34+
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
35+
import com.linecorp.armeria.xds.XdsBootstrap;
36+
import com.linecorp.armeria.xds.server.XdsServerPlugin;
37+
38+
import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap;
39+
40+
/**
41+
* Tests that server-side xDS HTTP filter decorators are applied.
42+
* The {@code test.header_filter} is registered via SPI and adds an
43+
* {@code x-xds-decorator: applied} response header.
44+
*/
45+
class ServerDecoratorTest {
46+
47+
@RegisterExtension
48+
@Order(0)
49+
static final SelfSignedCertificateExtension cert =
50+
new SelfSignedCertificateExtension("127.0.0.1");
51+
52+
@RegisterExtension
53+
@Order(1)
54+
static final ServerExtension server = new ServerExtension() {
55+
@Override
56+
protected void configure(ServerBuilder sb) throws Exception {
57+
final Path certPath = cert.certificateFile().toPath();
58+
final Path keyPath = cert.privateKeyFile().toPath();
59+
60+
//language=YAML
61+
final String bootstrapYaml =
62+
"""
63+
static_resources:
64+
listeners:
65+
- name: decorator-listener
66+
filter_chains: []
67+
default_filter_chain:
68+
filters:
69+
- name: envoy.filters.network.http_connection_manager
70+
typed_config:
71+
"@type": type.googleapis.com/envoy.extensions.filters\
72+
.network.http_connection_manager.v3.HttpConnectionManager
73+
stat_prefix: ingress_http
74+
route_config:
75+
name: local_route
76+
virtual_hosts:
77+
- name: local_service
78+
domains: ["*"]
79+
routes:
80+
- match:
81+
prefix: "/"
82+
route:
83+
cluster: local
84+
http_filters:
85+
- name: test.header_filter
86+
- name: envoy.filters.http.router
87+
typed_config:
88+
"@type": type.googleapis.com/envoy.extensions\
89+
.filters.http.router.v3.Router
90+
transport_socket:
91+
name: envoy.transport_sockets.downstream_tls
92+
typed_config:
93+
"@type": type.googleapis.com/envoy.extensions.transport_sockets\
94+
.tls.v3.DownstreamTlsContext
95+
common_tls_context:
96+
tls_certificates:
97+
- certificate_chain:
98+
filename: "%s"
99+
private_key:
100+
filename: "%s"
101+
clusters:
102+
- name: local
103+
type: STATIC
104+
""".formatted(certPath, keyPath);
105+
106+
final Bootstrap bootstrap = XdsResourceReader.fromYaml(bootstrapYaml, Bootstrap.class);
107+
final XdsBootstrap xdsBootstrap = XdsBootstrap.builder(bootstrap).build();
108+
109+
// The plugin registers the port, TlsProvider, and server listener.
110+
sb.plugin(XdsServerPlugin.of(xdsBootstrap, "decorator-listener"));
111+
112+
sb.service("/hello", (ctx, req) -> HttpResponse.of("hello"));
113+
}
114+
};
115+
116+
@Test
117+
void xdsDecoratorAddsResponseHeader() {
118+
final ClientFactory factory =
119+
ClientFactory.builder()
120+
.tlsCustomizer(b -> b.trustManager(cert.certificateFile()))
121+
.build();
122+
try {
123+
final AggregatedHttpResponse res =
124+
WebClient.builder(server.httpsUri())
125+
.factory(factory)
126+
.build()
127+
.blocking()
128+
.get("/hello");
129+
130+
assertThat(res.status()).isEqualTo(HttpStatus.OK);
131+
assertThat(res.contentUtf8()).isEqualTo("hello");
132+
assertThat(res.headers().get("x-xds-decorator")).isEqualTo("applied");
133+
} finally {
134+
factory.close();
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)