Create spring-boot-rsocket module
This commit is contained in:
committed by
Phillip Webb
parent
5715b90af9
commit
0d5a141a41
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a {@link RSocketMessageHandler}.
|
||||
*
|
||||
* @author Aarti Gupta
|
||||
* @author Madhura Bhave
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RSocketMessageHandlerCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link RSocketMessageHandler}.
|
||||
* @param messageHandler the message handler to customize
|
||||
*/
|
||||
void customize(RSocketMessageHandler messageHandler);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring RSocket support in Spring
|
||||
* Messaging.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
|
||||
@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, TcpServerTransport.class })
|
||||
public class RSocketMessagingAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RSocketMessageHandler messageHandler(RSocketStrategies rSocketStrategies,
|
||||
ObjectProvider<RSocketMessageHandlerCustomizer> customizers) {
|
||||
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
|
||||
messageHandler.setRSocketStrategies(rSocketStrategies);
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(messageHandler));
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties Properties} for RSocket support.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Chris Bono
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.rsocket")
|
||||
public class RSocketProperties {
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Server server = new Server();
|
||||
|
||||
public Server getServer() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
public static class Server {
|
||||
|
||||
/**
|
||||
* Server port.
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* Network address to which the server should bind.
|
||||
*/
|
||||
private InetAddress address;
|
||||
|
||||
/**
|
||||
* RSocket transport protocol.
|
||||
*/
|
||||
private RSocketServer.Transport transport = RSocketServer.Transport.TCP;
|
||||
|
||||
/**
|
||||
* Path under which RSocket handles requests (only works with websocket
|
||||
* transport).
|
||||
*/
|
||||
private String mappingPath;
|
||||
|
||||
/**
|
||||
* Maximum transmission unit. Frames larger than the specified value are
|
||||
* fragmented.
|
||||
*/
|
||||
private DataSize fragmentSize;
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private Ssl ssl;
|
||||
|
||||
private final Spec spec = new Spec();
|
||||
|
||||
public Integer getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(Integer port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public void setAddress(InetAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public RSocketServer.Transport getTransport() {
|
||||
return this.transport;
|
||||
}
|
||||
|
||||
public void setTransport(RSocketServer.Transport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public String getMappingPath() {
|
||||
return this.mappingPath;
|
||||
}
|
||||
|
||||
public void setMappingPath(String mappingPath) {
|
||||
this.mappingPath = mappingPath;
|
||||
}
|
||||
|
||||
public DataSize getFragmentSize() {
|
||||
return this.fragmentSize;
|
||||
}
|
||||
|
||||
public void setFragmentSize(DataSize fragmentSize) {
|
||||
this.fragmentSize = fragmentSize;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public void setSsl(Ssl ssl) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
public Spec getSpec() {
|
||||
return this.spec;
|
||||
}
|
||||
|
||||
public static class Spec {
|
||||
|
||||
/**
|
||||
* Sub-protocols to use in websocket handshake signature.
|
||||
*/
|
||||
private String protocols;
|
||||
|
||||
/**
|
||||
* Maximum allowable frame payload length.
|
||||
*/
|
||||
private DataSize maxFramePayloadLength = DataSize.ofBytes(65536);
|
||||
|
||||
/**
|
||||
* Whether to proxy websocket ping frames or respond to them.
|
||||
*/
|
||||
private boolean handlePing;
|
||||
|
||||
/**
|
||||
* Whether the websocket compression extension is enabled.
|
||||
*/
|
||||
private boolean compress;
|
||||
|
||||
public String getProtocols() {
|
||||
return this.protocols;
|
||||
}
|
||||
|
||||
public void setProtocols(String protocols) {
|
||||
this.protocols = protocols;
|
||||
}
|
||||
|
||||
public DataSize getMaxFramePayloadLength() {
|
||||
return this.maxFramePayloadLength;
|
||||
}
|
||||
|
||||
public void setMaxFramePayloadLength(DataSize maxFramePayloadLength) {
|
||||
this.maxFramePayloadLength = maxFramePayloadLength;
|
||||
}
|
||||
|
||||
public boolean isHandlePing() {
|
||||
return this.handlePing;
|
||||
}
|
||||
|
||||
public void setHandlePing(boolean handlePing) {
|
||||
this.handlePing = handlePing;
|
||||
}
|
||||
|
||||
public boolean isCompress() {
|
||||
return this.compress;
|
||||
}
|
||||
|
||||
public void setCompress(boolean compress) {
|
||||
this.compress = compress;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.messaging.rsocket.RSocketConnectorConfigurer;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester.Builder;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for
|
||||
* {@link org.springframework.messaging.rsocket.RSocketRequester}. This auto-configuration
|
||||
* creates {@link org.springframework.messaging.rsocket.RSocketRequester.Builder}
|
||||
* prototype beans, as the builders are stateful and should not be reused to build
|
||||
* requester instances with different configurations.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
|
||||
@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, HttpServer.class, TcpServerTransport.class })
|
||||
public class RSocketRequesterAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@ConditionalOnMissingBean
|
||||
public RSocketRequester.Builder rSocketRequesterBuilder(RSocketStrategies strategies,
|
||||
ObjectProvider<RSocketConnectorConfigurer> connectorConfigurers) {
|
||||
Builder builder = RSocketRequester.builder().rsocketStrategies(strategies);
|
||||
connectorConfigurers.orderedStream().forEach(builder::rsocketConnector);
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.rsocket.core.RSocketServer;
|
||||
import io.rsocket.frame.decoder.PayloadDecoder;
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
import reactor.netty.http.server.WebsocketServerSpec.Builder;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.reactor.netty.autoconfigure.ReactorNettyConfigurations;
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketProperties.Server.Spec;
|
||||
import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
|
||||
import org.springframework.boot.rsocket.netty.NettyRSocketServerFactory;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for RSocket servers. In the case of
|
||||
* {@link org.springframework.boot.WebApplicationType#REACTIVE}, the RSocket server is
|
||||
* added as a WebSocket endpoint on the existing
|
||||
* {@link org.springframework.boot.reactor.netty.NettyWebServer}. If a specific server
|
||||
* port is configured, a new standalone RSocket server is created.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
|
||||
@ConditionalOnClass({ RSocketServer.class, RSocketStrategies.class, HttpServer.class, TcpServerTransport.class })
|
||||
@ConditionalOnBean(RSocketMessageHandler.class)
|
||||
@EnableConfigurationProperties(RSocketProperties.class)
|
||||
public class RSocketServerAutoConfiguration {
|
||||
|
||||
@Conditional(OnRSocketWebServerCondition.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class WebFluxServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RSocketWebSocketNettyRouteProvider rSocketWebsocketRouteProvider(RSocketProperties properties,
|
||||
RSocketMessageHandler messageHandler, ObjectProvider<RSocketServerCustomizer> customizers) {
|
||||
return new RSocketWebSocketNettyRouteProvider(properties.getServer().getMappingPath(),
|
||||
messageHandler.responder(), customizeWebsocketServerSpec(properties.getServer().getSpec()),
|
||||
customizers.orderedStream());
|
||||
}
|
||||
|
||||
private Consumer<Builder> customizeWebsocketServerSpec(Spec spec) {
|
||||
return (builder) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(spec.getProtocols()).to(builder::protocols);
|
||||
map.from(spec.getMaxFramePayloadLength()).asInt(DataSize::toBytes).to(builder::maxFramePayloadLength);
|
||||
map.from(spec.isHandlePing()).to(builder::handlePing);
|
||||
map.from(spec.isCompress()).to(builder::compress);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty("spring.rsocket.server.port")
|
||||
@ConditionalOnClass(ReactorResourceFactory.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
|
||||
static class EmbeddedServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RSocketServerFactory rSocketServerFactory(RSocketProperties properties, ReactorResourceFactory resourceFactory,
|
||||
ObjectProvider<RSocketServerCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {
|
||||
NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
|
||||
factory.setResourceFactory(resourceFactory);
|
||||
factory.setTransport(properties.getServer().getTransport());
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties.getServer().getAddress()).to(factory::setAddress);
|
||||
map.from(properties.getServer().getPort()).to(factory::setPort);
|
||||
map.from(properties.getServer().getFragmentSize()).to(factory::setFragmentSize);
|
||||
map.from(properties.getServer().getSsl()).to(factory::setSsl);
|
||||
factory.setSslBundles(sslBundles.getIfAvailable());
|
||||
factory.setRSocketServerCustomizers(customizers.orderedStream().toList());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RSocketServerBootstrap rSocketServerBootstrap(RSocketServerFactory rSocketServerFactory,
|
||||
RSocketMessageHandler rSocketMessageHandler) {
|
||||
return new RSocketServerBootstrap(rSocketServerFactory, rSocketMessageHandler.responder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
RSocketServerCustomizer frameDecoderRSocketServerCustomizer(RSocketMessageHandler rSocketMessageHandler) {
|
||||
return (server) -> {
|
||||
if (rSocketMessageHandler.getRSocketStrategies()
|
||||
.dataBufferFactory() instanceof NettyDataBufferFactory) {
|
||||
server.payloadDecoder(PayloadDecoder.ZERO_COPY);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class OnRSocketWebServerCondition extends AllNestedConditions {
|
||||
|
||||
OnRSocketWebServerCondition() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
static class IsReactiveWebApplication {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(name = "spring.rsocket.server.port", matchIfMissing = true)
|
||||
static class HasNoPortConfigured {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty("spring.rsocket.server.mapping-path")
|
||||
static class HasMappingPathConfigured {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(name = "spring.rsocket.server.transport", havingValue = "websocket")
|
||||
static class HasWebsocketTransportConfigured {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link RSocketStrategies}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
|
||||
@ConditionalOnClass({ io.rsocket.RSocket.class, RSocketStrategies.class, PooledByteBufAllocator.class })
|
||||
public class RSocketStrategiesAutoConfiguration {
|
||||
|
||||
private static final String PATHPATTERN_ROUTEMATCHER_CLASS = "org.springframework.web.util.pattern.PathPatternRouteMatcher";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RSocketStrategies rSocketStrategies(ObjectProvider<RSocketStrategiesCustomizer> customizers) {
|
||||
RSocketStrategies.Builder builder = RSocketStrategies.builder();
|
||||
if (ClassUtils.isPresent(PATHPATTERN_ROUTEMATCHER_CLASS, null)) {
|
||||
builder.routeMatcher(new PathPatternRouteMatcher());
|
||||
}
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ ObjectMapper.class, CBORFactory.class })
|
||||
@SuppressWarnings({ "removal", "deprecation" })
|
||||
protected static class JacksonCborStrategyConfiguration {
|
||||
|
||||
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR };
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
@ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
|
||||
public RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(
|
||||
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
|
||||
return (strategy) -> {
|
||||
ObjectMapper objectMapper = builder.createXmlMapper(false).factory(new CBORFactory()).build();
|
||||
strategy.decoder(
|
||||
new org.springframework.http.codec.cbor.Jackson2CborDecoder(objectMapper, SUPPORTED_TYPES));
|
||||
strategy.encoder(
|
||||
new org.springframework.http.codec.cbor.Jackson2CborEncoder(objectMapper, SUPPORTED_TYPES));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
@SuppressWarnings({ "removal", "deprecation" })
|
||||
protected static class JacksonJsonStrategyConfiguration {
|
||||
|
||||
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON,
|
||||
new MediaType("application", "*+json") };
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
@ConditionalOnBean(ObjectMapper.class)
|
||||
public RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(ObjectMapper objectMapper) {
|
||||
return (strategy) -> {
|
||||
strategy.decoder(
|
||||
new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper, SUPPORTED_TYPES));
|
||||
strategy.encoder(
|
||||
new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper, SUPPORTED_TYPES));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import io.rsocket.core.RSocketServer;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
import io.rsocket.transport.netty.server.WebsocketRouteTransport;
|
||||
import reactor.netty.http.server.HttpServerRoutes;
|
||||
import reactor.netty.http.server.WebsocketServerSpec;
|
||||
import reactor.netty.http.server.WebsocketServerSpec.Builder;
|
||||
|
||||
import org.springframework.boot.reactor.netty.NettyRouteProvider;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
|
||||
/**
|
||||
* {@link NettyRouteProvider} that configures an RSocket Websocket endpoint.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Leo Li
|
||||
*/
|
||||
class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider {
|
||||
|
||||
private final String mappingPath;
|
||||
|
||||
private final SocketAcceptor socketAcceptor;
|
||||
|
||||
private final List<RSocketServerCustomizer> customizers;
|
||||
|
||||
private final Consumer<Builder> serverSpecCustomizer;
|
||||
|
||||
RSocketWebSocketNettyRouteProvider(String mappingPath, SocketAcceptor socketAcceptor,
|
||||
Consumer<Builder> serverSpecCustomizer, Stream<RSocketServerCustomizer> customizers) {
|
||||
this.mappingPath = mappingPath;
|
||||
this.socketAcceptor = socketAcceptor;
|
||||
this.serverSpecCustomizer = serverSpecCustomizer;
|
||||
this.customizers = customizers.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpServerRoutes apply(HttpServerRoutes httpServerRoutes) {
|
||||
RSocketServer server = RSocketServer.create(this.socketAcceptor);
|
||||
this.customizers.forEach((customizer) -> customizer.customize(server));
|
||||
ServerTransport.ConnectionAcceptor connectionAcceptor = server.asConnectionAcceptor();
|
||||
return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor),
|
||||
createWebsocketServerSpec());
|
||||
}
|
||||
|
||||
private WebsocketServerSpec createWebsocketServerSpec() {
|
||||
WebsocketServerSpec.Builder builder = WebsocketServerSpec.builder();
|
||||
this.serverSpecCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for RSocket.
|
||||
*/
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.context;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
/**
|
||||
* {@link ApplicationContextInitializer} that sets {@link Environment} properties for the
|
||||
* ports that {@link RSocketServer} servers are actually listening on. The property
|
||||
* {@literal "local.rsocket.server.port"} can be injected directly into tests using
|
||||
* {@link Value @Value} or obtained through the {@link Environment}.
|
||||
* <p>
|
||||
* Properties are automatically propagated up to any parent context.
|
||||
*
|
||||
* @author Verónica Vásquez
|
||||
* @author Eddú Meléndez
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class RSocketPortInfoApplicationContextInitializer
|
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
applicationContext.addApplicationListener(new Listener(applicationContext));
|
||||
}
|
||||
|
||||
private static class Listener implements ApplicationListener<RSocketServerInitializedEvent> {
|
||||
|
||||
private static final String PROPERTY_NAME = "local.rsocket.server.port";
|
||||
|
||||
private static final String PROPERTY_SOURCE_NAME = "server.ports";
|
||||
|
||||
private final ConfigurableApplicationContext applicationContext;
|
||||
|
||||
Listener(ConfigurableApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(RSocketServerInitializedEvent event) {
|
||||
if (event.getServer().address() != null) {
|
||||
setPortProperty(this.applicationContext, event.getServer().address().getPort());
|
||||
}
|
||||
}
|
||||
|
||||
private void setPortProperty(ApplicationContext context, int port) {
|
||||
if (context instanceof ConfigurableApplicationContext configurableContext) {
|
||||
setPortProperty(configurableContext.getEnvironment(), port);
|
||||
}
|
||||
if (context.getParent() != null) {
|
||||
setPortProperty(context.getParent(), port);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPortProperty(ConfigurableEnvironment environment, int port) {
|
||||
MutablePropertySources sources = environment.getPropertySources();
|
||||
PropertySource<?> source = sources.get(PROPERTY_SOURCE_NAME);
|
||||
if (source == null) {
|
||||
source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>());
|
||||
sources.addFirst(source);
|
||||
}
|
||||
setPortProperty(port, source);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void setPortProperty(int port, PropertySource<?> source) {
|
||||
((Map<String, Object>) source.getSource()).put(PROPERTY_NAME, port);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.context;
|
||||
|
||||
import io.rsocket.SocketAcceptor;
|
||||
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Bootstrap an {@link RSocketServer} and start it with the application context.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class RSocketServerBootstrap implements ApplicationEventPublisherAware, SmartLifecycle {
|
||||
|
||||
private final RSocketServer server;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public RSocketServerBootstrap(RSocketServerFactory serverFactory, SocketAcceptor socketAcceptor) {
|
||||
Assert.notNull(serverFactory, "'serverFactory' must not be null");
|
||||
this.server = serverFactory.create(socketAcceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.server.start();
|
||||
this.eventPublisher.publishEvent(new RSocketServerInitializedEvent(this.server));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.server.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
RSocketServer server = this.server;
|
||||
if (server != null) {
|
||||
return server.address() != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.context;
|
||||
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Event to be published after the application context is refreshed and the
|
||||
* {@link RSocketServer} is ready. Useful for obtaining the local port of a running
|
||||
* server.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class RSocketServerInitializedEvent extends ApplicationEvent {
|
||||
|
||||
public RSocketServerInitializedEvent(RSocketServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the {@link RSocketServer}.
|
||||
* @return the embedded RSocket server
|
||||
*/
|
||||
public RSocketServer getServer() {
|
||||
return getSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the source of the event (an {@link RSocketServer}).
|
||||
* @return the embedded web server
|
||||
*/
|
||||
@Override
|
||||
public RSocketServer getSource() {
|
||||
return (RSocketServer) super.getSource();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* RSocket integrations with Spring Framework's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}.
|
||||
*/
|
||||
package org.springframework.boot.rsocket.context;
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.messaging;
|
||||
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize codecs configuration for an RSocket
|
||||
* client and/or server with {@link RSocketStrategies}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RSocketStrategiesCustomizer {
|
||||
|
||||
/**
|
||||
* Callback to customize a {@link RSocketStrategies#builder()} instance.
|
||||
* @param strategies rSocket codec strategies to customize
|
||||
*/
|
||||
void customize(RSocketStrategies.Builder strategies);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for RSocket-based messaging.
|
||||
*/
|
||||
package org.springframework.boot.rsocket.messaging;
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.netty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
|
||||
import io.rsocket.transport.netty.server.CloseableChannel;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RSocketServer} that is based on a Reactor Netty server. Usually this class
|
||||
* should be created using the {@link NettyRSocketServerFactory} and not directly.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class NettyRSocketServer implements RSocketServer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(NettyRSocketServer.class);
|
||||
|
||||
private final Mono<CloseableChannel> starter;
|
||||
|
||||
private final Duration lifecycleTimeout;
|
||||
|
||||
private CloseableChannel channel;
|
||||
|
||||
public NettyRSocketServer(Mono<CloseableChannel> starter, Duration lifecycleTimeout) {
|
||||
Assert.notNull(starter, "'starter' must not be null");
|
||||
this.starter = starter;
|
||||
this.lifecycleTimeout = lifecycleTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress address() {
|
||||
if (this.channel != null) {
|
||||
return this.channel.address();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws RSocketServerException {
|
||||
this.channel = block(this.starter, this.lifecycleTimeout);
|
||||
logger.info("Netty RSocket started on port " + address().getPort());
|
||||
startDaemonAwaitThread(this.channel);
|
||||
}
|
||||
|
||||
private void startDaemonAwaitThread(CloseableChannel channel) {
|
||||
Thread awaitThread = new Thread(() -> channel.onClose().block(), "rsocket");
|
||||
awaitThread.setContextClassLoader(getClass().getClassLoader());
|
||||
awaitThread.setDaemon(false);
|
||||
awaitThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws RSocketServerException {
|
||||
if (this.channel != null) {
|
||||
this.channel.dispose();
|
||||
this.channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T block(Mono<T> mono, Duration timeout) {
|
||||
return (timeout != null) ? mono.block(timeout) : mono.block();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.netty;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.netty.handler.ssl.ClientAuth;
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
import io.rsocket.transport.netty.server.CloseableChannel;
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
import io.rsocket.transport.netty.server.WebsocketServerTransport;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.Http11SslContextSpec;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
import reactor.netty.tcp.AbstractProtocolSslContextSpec;
|
||||
import reactor.netty.tcp.SslProvider;
|
||||
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
|
||||
import reactor.netty.tcp.SslProvider.SslContextSpec;
|
||||
import reactor.netty.tcp.TcpServer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.rsocket.server.ConfigurableRSocketServerFactory;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link RSocketServerFactory} that can be used to create {@link RSocketServer}s backed
|
||||
* by Netty.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Chris Bono
|
||||
* @author Scott Frederick
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory {
|
||||
|
||||
private int port = 9898;
|
||||
|
||||
private DataSize fragmentSize;
|
||||
|
||||
private InetAddress address;
|
||||
|
||||
private RSocketServer.Transport transport = RSocketServer.Transport.TCP;
|
||||
|
||||
private ReactorResourceFactory resourceFactory;
|
||||
|
||||
private Duration lifecycleTimeout;
|
||||
|
||||
private List<RSocketServerCustomizer> rSocketServerCustomizers = new ArrayList<>();
|
||||
|
||||
private Ssl ssl;
|
||||
|
||||
private SslBundles sslBundles;
|
||||
|
||||
@Override
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFragmentSize(DataSize fragmentSize) {
|
||||
this.fragmentSize = fragmentSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAddress(InetAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransport(RSocketServer.Transport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSsl(Ssl ssl) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSslBundles(SslBundles sslBundles) {
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ReactorResourceFactory} to get the shared resources from.
|
||||
* @param resourceFactory the server resources
|
||||
*/
|
||||
public void setResourceFactory(ReactorResourceFactory resourceFactory) {
|
||||
this.resourceFactory = resourceFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link RSocketServerCustomizer}s that should be called to configure the
|
||||
* {@link io.rsocket.core.RSocketServer} while building the server. Calling this
|
||||
* method will replace any existing customizers.
|
||||
* @param rSocketServerCustomizers customizers to apply before the server starts
|
||||
* @since 2.2.7
|
||||
*/
|
||||
public void setRSocketServerCustomizers(Collection<? extends RSocketServerCustomizer> rSocketServerCustomizers) {
|
||||
Assert.notNull(rSocketServerCustomizers, "'rSocketServerCustomizers' must not be null");
|
||||
this.rSocketServerCustomizers = new ArrayList<>(rSocketServerCustomizers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link RSocketServerCustomizer}s that should be called to configure the
|
||||
* {@link io.rsocket.core.RSocketServer}.
|
||||
* @param rSocketServerCustomizers customizers to apply before the server starts
|
||||
* @since 2.2.7
|
||||
*/
|
||||
public void addRSocketServerCustomizers(RSocketServerCustomizer... rSocketServerCustomizers) {
|
||||
Assert.notNull(rSocketServerCustomizers, "'rSocketServerCustomizers' must not be null");
|
||||
this.rSocketServerCustomizers.addAll(Arrays.asList(rSocketServerCustomizers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum amount of time that should be waited when starting or stopping the
|
||||
* server.
|
||||
* @param lifecycleTimeout the lifecycle timeout
|
||||
*/
|
||||
public void setLifecycleTimeout(Duration lifecycleTimeout) {
|
||||
this.lifecycleTimeout = lifecycleTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NettyRSocketServer create(SocketAcceptor socketAcceptor) {
|
||||
ServerTransport<CloseableChannel> transport = createTransport();
|
||||
io.rsocket.core.RSocketServer server = io.rsocket.core.RSocketServer.create(socketAcceptor);
|
||||
configureServer(server);
|
||||
Mono<CloseableChannel> starter = server.bind(transport);
|
||||
return new NettyRSocketServer(starter, this.lifecycleTimeout);
|
||||
}
|
||||
|
||||
private void configureServer(io.rsocket.core.RSocketServer server) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.fragmentSize).asInt(DataSize::toBytes).to(server::fragment);
|
||||
this.rSocketServerCustomizers.forEach((customizer) -> customizer.customize(server));
|
||||
}
|
||||
|
||||
private ServerTransport<CloseableChannel> createTransport() {
|
||||
if (this.transport == RSocketServer.Transport.WEBSOCKET) {
|
||||
return createWebSocketTransport();
|
||||
}
|
||||
return createTcpTransport();
|
||||
}
|
||||
|
||||
private ServerTransport<CloseableChannel> createWebSocketTransport() {
|
||||
HttpServer httpServer = HttpServer.create();
|
||||
if (this.resourceFactory != null) {
|
||||
httpServer = httpServer.runOn(this.resourceFactory.getLoopResources());
|
||||
}
|
||||
if (Ssl.isEnabled(this.ssl)) {
|
||||
httpServer = customizeSslConfiguration(httpServer);
|
||||
}
|
||||
return WebsocketServerTransport.create(httpServer.bindAddress(this::getListenAddress));
|
||||
}
|
||||
|
||||
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
||||
return new HttpServerSslCustomizer(this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles())
|
||||
.apply(httpServer);
|
||||
}
|
||||
|
||||
private ServerTransport<CloseableChannel> createTcpTransport() {
|
||||
TcpServer tcpServer = TcpServer.create();
|
||||
if (this.resourceFactory != null) {
|
||||
tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources());
|
||||
}
|
||||
if (Ssl.isEnabled(this.ssl)) {
|
||||
tcpServer = new TcpServerSslCustomizer(this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles())
|
||||
.apply(tcpServer);
|
||||
}
|
||||
return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress));
|
||||
}
|
||||
|
||||
private SslBundle getSslBundle() {
|
||||
return WebServerSslBundle.get(this.ssl, this.sslBundles);
|
||||
}
|
||||
|
||||
protected final Map<String, SslBundle> getServerNameSslBundles() {
|
||||
return this.ssl.getServerNameBundles()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Ssl.ServerNameSslBundle::serverName, this::getBundle));
|
||||
}
|
||||
|
||||
private SslBundle getBundle(ServerNameSslBundle serverNameSslBundle) {
|
||||
return this.sslBundles.getBundle(serverNameSslBundle.bundle());
|
||||
}
|
||||
|
||||
private InetSocketAddress getListenAddress() {
|
||||
if (this.address != null) {
|
||||
return new InetSocketAddress(this.address.getHostAddress(), this.port);
|
||||
}
|
||||
return new InetSocketAddress(this.port);
|
||||
}
|
||||
|
||||
private abstract static class SslCustomizer {
|
||||
|
||||
private final ClientAuth clientAuth;
|
||||
|
||||
protected SslCustomizer(ClientAuth clientAuth) {
|
||||
this.clientAuth = clientAuth;
|
||||
}
|
||||
|
||||
protected final AbstractProtocolSslContextSpec<?> createSslContextSpec(SslBundle sslBundle) {
|
||||
AbstractProtocolSslContextSpec<?> sslContextSpec = Http11SslContextSpec
|
||||
.forServer(sslBundle.getManagers().getKeyManagerFactory());
|
||||
return sslContextSpec.configure((builder) -> {
|
||||
builder.trustManager(sslBundle.getManagers().getTrustManagerFactory());
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
builder.protocols(options.getEnabledProtocols());
|
||||
builder.ciphers(SslOptions.asSet(options.getCiphers()));
|
||||
builder.clientAuth(this.clientAuth);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TcpServerSslCustomizer extends SslCustomizer {
|
||||
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
private TcpServerSslCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle,
|
||||
Map<String, SslBundle> serverNameSslBundles) {
|
||||
super(Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
|
||||
this.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
private TcpServer apply(TcpServer server) {
|
||||
GenericSslContextSpec<?> sslContextSpec = createSslContextSpec(this.sslBundle);
|
||||
return server.secure((spec) -> spec.sslContext(sslContextSpec));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class HttpServerSslCustomizer extends SslCustomizer {
|
||||
|
||||
private final SslProvider sslProvider;
|
||||
|
||||
private final Map<String, SslProvider> serverNameSslProviders;
|
||||
|
||||
private HttpServerSslCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle,
|
||||
Map<String, SslBundle> serverNameSslBundles) {
|
||||
super(Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
|
||||
this.sslProvider = createSslProvider(sslBundle);
|
||||
this.serverNameSslProviders = createServerNameSslProviders(serverNameSslBundles);
|
||||
}
|
||||
|
||||
private HttpServer apply(HttpServer server) {
|
||||
return server.secure(this::applySecurity);
|
||||
}
|
||||
|
||||
private void applySecurity(SslContextSpec spec) {
|
||||
spec.sslContext(this.sslProvider.getSslContext()).setSniAsyncMappings((serverName, promise) -> {
|
||||
SslProvider provider = (serverName != null) ? this.serverNameSslProviders.get(serverName)
|
||||
: this.sslProvider;
|
||||
return promise.setSuccess(provider);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, SslProvider> createServerNameSslProviders(Map<String, SslBundle> serverNameSslBundles) {
|
||||
Map<String, SslProvider> serverNameSslProviders = new HashMap<>();
|
||||
serverNameSslBundles.forEach(
|
||||
(serverName, sslBundle) -> serverNameSslProviders.put(serverName, createSslProvider(sslBundle)));
|
||||
return serverNameSslProviders;
|
||||
}
|
||||
|
||||
private SslProvider createSslProvider(SslBundle sslBundle) {
|
||||
return SslProvider.builder().sslContext((GenericSslContextSpec<?>) createSslContextSpec(sslBundle)).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reactor Netty based RSocket server implementation.
|
||||
*/
|
||||
package org.springframework.boot.rsocket.netty;
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.server;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* A configurable {@link RSocketServerFactory}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public interface ConfigurableRSocketServerFactory {
|
||||
|
||||
/**
|
||||
* Set the port that the server should listen on. If not specified port '9898' will be
|
||||
* used.
|
||||
* @param port the port to set
|
||||
*/
|
||||
void setPort(int port);
|
||||
|
||||
/**
|
||||
* Specify the maximum transmission unit. Frames larger than the specified
|
||||
* {@code fragmentSize} are fragmented.
|
||||
* @param fragmentSize the fragment size
|
||||
* @since 2.4.0
|
||||
*/
|
||||
void setFragmentSize(DataSize fragmentSize);
|
||||
|
||||
/**
|
||||
* Set the specific network address that the server should bind to.
|
||||
* @param address the address to set (defaults to {@code null})
|
||||
*/
|
||||
void setAddress(InetAddress address);
|
||||
|
||||
/**
|
||||
* Set the transport that the RSocket server should use.
|
||||
* @param transport the transport protocol to use
|
||||
*/
|
||||
void setTransport(RSocketServer.Transport transport);
|
||||
|
||||
/**
|
||||
* Sets the SSL configuration that will be applied to the server's default connector.
|
||||
* @param ssl the SSL configuration
|
||||
*/
|
||||
void setSsl(Ssl ssl);
|
||||
|
||||
/**
|
||||
* Sets an SSL bundle that can be used to get SSL configuration.
|
||||
* @param sslBundles the SSL bundles
|
||||
* @since 3.1.0
|
||||
*/
|
||||
void setSslBundles(SslBundles sslBundles);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.server;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
/**
|
||||
* Simple interface that represents a fully configured RSocket server. Allows the server
|
||||
* to be {@link #start() started} and {@link #stop() stopped}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public interface RSocketServer {
|
||||
|
||||
/**
|
||||
* Starts the RSocket server. Calling this method on an already started server has no
|
||||
* effect.
|
||||
* @throws RSocketServerException if the server cannot be started
|
||||
*/
|
||||
void start() throws RSocketServerException;
|
||||
|
||||
/**
|
||||
* Stops the RSocket server. Calling this method on an already stopped server has no
|
||||
* effect.
|
||||
* @throws RSocketServerException if the server cannot be stopped
|
||||
*/
|
||||
void stop() throws RSocketServerException;
|
||||
|
||||
/**
|
||||
* Return the address this server is listening on.
|
||||
* @return the address
|
||||
*/
|
||||
InetSocketAddress address();
|
||||
|
||||
/**
|
||||
* Choice of transport protocol for the RSocket server.
|
||||
*/
|
||||
enum Transport {
|
||||
|
||||
/**
|
||||
* TCP transport protocol.
|
||||
*/
|
||||
TCP,
|
||||
|
||||
/**
|
||||
* WebSocket transport protocol.
|
||||
*/
|
||||
WEBSOCKET
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.server;
|
||||
|
||||
import io.rsocket.core.RSocketServer;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a {@link RSocketServer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.3.0
|
||||
* @see RSocketServer
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RSocketServerCustomizer {
|
||||
|
||||
/**
|
||||
* Callback to customize a {@link RSocketServer} instance.
|
||||
* @param rSocketServer the RSocket server to customize
|
||||
*/
|
||||
void customize(RSocketServer rSocketServer);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.server;
|
||||
|
||||
/**
|
||||
* Exceptions thrown by an RSocket server.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class RSocketServerException extends RuntimeException {
|
||||
|
||||
public RSocketServerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.server;
|
||||
|
||||
import io.rsocket.SocketAcceptor;
|
||||
|
||||
/**
|
||||
* Factory interface that can be used to create a reactive {@link RSocketServer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RSocketServerFactory {
|
||||
|
||||
/**
|
||||
* Gets a new fully configured but paused {@link RSocketServer} instance. Clients
|
||||
* should not be able to connect to the returned server until
|
||||
* {@link RSocketServer#start()} is called (which happens when the
|
||||
* {@code ApplicationContext} has been fully refreshed).
|
||||
* @param socketAcceptor the socket acceptor
|
||||
* @return a fully configured and started {@link RSocketServer}
|
||||
* @see RSocketServer#stop()
|
||||
*/
|
||||
RSocketServer create(SocketAcceptor socketAcceptor);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for RSocket servers.
|
||||
*/
|
||||
package org.springframework.boot.rsocket.server;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"properties": [
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Application Context Initializers
|
||||
org.springframework.context.ApplicationContextInitializer=\
|
||||
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer
|
||||
@@ -0,0 +1,4 @@
|
||||
org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration
|
||||
org.springframework.boot.rsocket.autoconfigure.RSocketRequesterAutoConfiguration
|
||||
org.springframework.boot.rsocket.autoconfigure.RSocketServerAutoConfiguration
|
||||
org.springframework.boot.rsocket.autoconfigure.RSocketStrategiesAutoConfiguration
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketMessagingAutoConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class RSocketMessagingAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RSocketMessagingAutoConfiguration.class))
|
||||
.withUserConfiguration(BaseConfiguration.class);
|
||||
|
||||
@Test
|
||||
void shouldCreateDefaultBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBeans(RSocketMessageHandler.class).hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailOnMissingStrategies() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RSocketMessagingAutoConfiguration.class))
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure().getMessage()).contains("No qualifying bean of type "
|
||||
+ "'org.springframework.messaging.rsocket.RSocketStrategies' available");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomSocketAcceptor() {
|
||||
this.contextRunner.withUserConfiguration(CustomMessageHandler.class)
|
||||
.run((context) -> assertThat(context).getBeanNames(RSocketMessageHandler.class)
|
||||
.containsOnly("customMessageHandler"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyMessageHandlerCustomizers() {
|
||||
this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> {
|
||||
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
|
||||
assertThat(handler.getDefaultDataMimeType()).isEqualTo(MimeType.valueOf("application/json"));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
RSocketStrategies rSocketStrategies() {
|
||||
return RSocketStrategies.builder()
|
||||
.encoder(CharSequenceEncoder.textPlainOnly())
|
||||
.decoder(StringDecoder.allMimeTypes())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomMessageHandler {
|
||||
|
||||
@Bean
|
||||
RSocketMessageHandler customMessageHandler() {
|
||||
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.encoder(CharSequenceEncoder.textPlainOnly())
|
||||
.decoder(StringDecoder.allMimeTypes())
|
||||
.build();
|
||||
messageHandler.setRSocketStrategies(strategies);
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
RSocketMessageHandlerCustomizer customizer() {
|
||||
return (messageHandler) -> messageHandler.setDefaultDataMimeType(MimeType.valueOf("application/json"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.server.WebsocketServerSpec;
|
||||
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketProperties.Server.Spec;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketProperties}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class RSocketPropertiesTests {
|
||||
|
||||
@Test
|
||||
void defaultServerSpecValuesAreConsistent() {
|
||||
WebsocketServerSpec spec = WebsocketServerSpec.builder().build();
|
||||
Spec properties = new RSocketProperties().getServer().getSpec();
|
||||
assertThat(properties.getProtocols()).isEqualTo(spec.protocols());
|
||||
assertThat(properties.getMaxFramePayloadLength().toBytes()).isEqualTo(spec.maxFramePayloadLength());
|
||||
assertThat(properties.isHandlePing()).isEqualTo(spec.handlePing());
|
||||
assertThat(properties.isCompress()).isEqualTo(spec.compress());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.rsocket.RSocketConnectorConfigurer;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketRequesterAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Nguyen Bao Sach
|
||||
*/
|
||||
class RSocketRequesterAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class, RSocketRequesterAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldCreateBuilder() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RSocketRequester.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetPrototypeScopedBean() {
|
||||
this.contextRunner.run((context) -> {
|
||||
RSocketRequester.Builder first = context.getBean(RSocketRequester.Builder.class);
|
||||
RSocketRequester.Builder second = context.getBean(RSocketRequester.Builder.class);
|
||||
assertThat(first).isNotEqualTo(second);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCreateBuilderIfAlreadyPresent() {
|
||||
this.contextRunner.withUserConfiguration(CustomRSocketRequesterBuilder.class).run((context) -> {
|
||||
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
|
||||
assertThat(builder).isInstanceOf(MyRSocketRequesterBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateBuilderWithAvailableRSocketConnectorConfigurers() {
|
||||
RSocketConnectorConfigurer first = mock(RSocketConnectorConfigurer.class);
|
||||
RSocketConnectorConfigurer second = mock(RSocketConnectorConfigurer.class);
|
||||
this.contextRunner.withBean("first", RSocketConnectorConfigurer.class, () -> first)
|
||||
.withBean("second", RSocketConnectorConfigurer.class, () -> second)
|
||||
.run((context) -> {
|
||||
assertThat(context).getBeans(RSocketConnectorConfigurer.class).hasSize(2);
|
||||
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
|
||||
assertThat(builder).extracting("rsocketConnectorConfigurers", as(InstanceOfAssertFactories.LIST))
|
||||
.containsExactly(first, second);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRSocketRequesterBuilder {
|
||||
|
||||
@Bean
|
||||
MyRSocketRequesterBuilder myRSocketRequesterBuilder() {
|
||||
return mock(MyRSocketRequesterBuilder.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface MyRSocketRequesterBuilder extends RSocketRequester.Builder {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketServerAutoConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Verónica Vásquez
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class RSocketServerAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void shouldNotCreateBeansByDefault() {
|
||||
contextRunner().run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
|
||||
.doesNotHaveBean(RSocketServerFactory.class)
|
||||
.doesNotHaveBean(RSocketServerBootstrap.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCreateDefaultBeansForReactiveWebAppWithoutMapping() {
|
||||
reactiveWebContextRunner()
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
|
||||
.doesNotHaveBean(RSocketServerFactory.class)
|
||||
.doesNotHaveBean(RSocketServerBootstrap.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCreateDefaultBeansForReactiveWebAppWithWrongTransport() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.transport=tcp", "spring.rsocket.server.mapping-path=/rsocket")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
|
||||
.doesNotHaveBean(RSocketServerFactory.class)
|
||||
.doesNotHaveBean(RSocketServerBootstrap.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateDefaultBeansForReactiveWebApp() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.transport=websocket",
|
||||
"spring.rsocket.server.mapping-path=/rsocket")
|
||||
.run((context) -> assertThat(context).hasSingleBean(RSocketWebSocketNettyRouteProvider.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateDefaultBeansForRSocketServerWhenPortIsSet() {
|
||||
reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
|
||||
.hasSingleBean(RSocketServerBootstrap.class)
|
||||
.hasSingleBean(RSocketServerCustomizer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetLocalServerPortWhenRSocketServerPortIsSet() {
|
||||
reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0")
|
||||
.withInitializer(new RSocketPortInfoApplicationContextInitializer())
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RSocketServerFactory.class)
|
||||
.hasSingleBean(RSocketServerBootstrap.class)
|
||||
.hasSingleBean(RSocketServerCustomizer.class);
|
||||
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetFragmentWhenRSocketServerFragmentSizeIsSet() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=12KB")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RSocketServerFactory.class);
|
||||
RSocketServerFactory factory = context.getBean(RSocketServerFactory.class);
|
||||
assertThat(factory).hasFieldOrPropertyWithValue("fragmentSize", DataSize.ofKilobytes(12));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToSetFragmentWhenRSocketServerFragmentSizeIsBelow64() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=60B")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure())
|
||||
.hasMessageContaining("The smallest allowed mtu size is 64 bytes, provided: 60");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void shouldUseSslWhenRocketServerSslIsConfigured() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.ssl.keyStore=classpath:test.jks",
|
||||
"spring.rsocket.server.ssl.keyPassword=password", "spring.rsocket.server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
|
||||
.hasSingleBean(RSocketServerBootstrap.class)
|
||||
.hasSingleBean(RSocketServerCustomizer.class)
|
||||
.getBean(RSocketServerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("ssl.keyStore", "classpath:test.jks")
|
||||
.hasFieldOrPropertyWithValue("ssl.keyPassword", "password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
@WithPackageResources("test.jks")
|
||||
void shouldUseSslWhenRocketServerSslIsConfiguredWithSslBundle() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle",
|
||||
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks",
|
||||
"spring.ssl.bundle.jks.test-bundle.key.password=password")
|
||||
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
|
||||
.hasSingleBean(RSocketServerBootstrap.class)
|
||||
.hasSingleBean(RSocketServerCustomizer.class)
|
||||
.getBean(RSocketServerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("sslBundle.details.keyStore", "classpath:test.jks")
|
||||
.hasFieldOrPropertyWithValue("sslBundle.details.keyPassword", "password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSslIsConfiguredWithMissingBundle() {
|
||||
reactiveWebContextRunner()
|
||||
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(NoSuchSslBundleException.class)
|
||||
.withFailMessage("SSL bundle name 'test-bundle' is not valid");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomServerBootstrap() {
|
||||
contextRunner().withUserConfiguration(CustomServerBootstrapConfig.class)
|
||||
.run((context) -> assertThat(context).getBeanNames(RSocketServerBootstrap.class)
|
||||
.containsExactly("customServerBootstrap"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomNettyRouteProvider() {
|
||||
reactiveWebContextRunner().withUserConfiguration(CustomNettyRouteProviderConfig.class)
|
||||
.withPropertyValues("spring.rsocket.server.transport=websocket",
|
||||
"spring.rsocket.server.mapping-path=/rsocket")
|
||||
.run((context) -> assertThat(context).getBeanNames(RSocketWebSocketNettyRouteProvider.class)
|
||||
.containsExactly("customNettyRouteProvider"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSpringWebIsNotPresentThenEmbeddedServerConfigurationBacksOff() {
|
||||
contextRunner().withClassLoader(new FilteredClassLoader(ReactorResourceFactory.class))
|
||||
.withPropertyValues("spring.rsocket.server.port=0")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(RSocketServerFactory.class));
|
||||
}
|
||||
|
||||
private ApplicationContextRunner contextRunner() {
|
||||
return new ApplicationContextRunner().withUserConfiguration(BaseConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class));
|
||||
}
|
||||
|
||||
private ReactiveWebApplicationContextRunner reactiveWebContextRunner() {
|
||||
return new ReactiveWebApplicationContextRunner().withUserConfiguration(BaseConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
RSocketMessageHandler messageHandler() {
|
||||
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
|
||||
messageHandler.setRSocketStrategies(RSocketStrategies.builder()
|
||||
.encoder(CharSequenceEncoder.textPlainOnly())
|
||||
.decoder(StringDecoder.allMimeTypes())
|
||||
.build());
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomServerBootstrapConfig {
|
||||
|
||||
@Bean
|
||||
RSocketServerBootstrap customServerBootstrap() {
|
||||
return mock(RSocketServerBootstrap.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomNettyRouteProviderConfig {
|
||||
|
||||
@Bean
|
||||
RSocketWebSocketNettyRouteProvider customNettyRouteProvider() {
|
||||
return mock(RSocketWebSocketNettyRouteProvider.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketStrategiesAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class RSocketStrategiesAutoConfigurationTests {
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class))
|
||||
.withBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
|
||||
.withBean(ObjectMapper.class);
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void shouldCreateDefaultBeans() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
|
||||
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
|
||||
assertThat(strategies.decoders())
|
||||
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class)
|
||||
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class);
|
||||
assertThat(strategies.encoders())
|
||||
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class)
|
||||
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class);
|
||||
assertThat(strategies.routeMatcher()).isInstanceOf(PathPatternRouteMatcher.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomStrategies() {
|
||||
this.contextRunner.withUserConfiguration(UserStrategies.class).run((context) -> {
|
||||
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
|
||||
assertThat(context.getBeanNamesForType(RSocketStrategies.class)).contains("customRSocketStrategies");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseStrategiesCustomizer() {
|
||||
this.contextRunner.withUserConfiguration(StrategiesCustomizer.class).run((context) -> {
|
||||
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
|
||||
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
|
||||
assertThat(strategies.decoders()).hasAtLeastOneElementOfType(CustomDecoder.class);
|
||||
assertThat(strategies.encoders()).hasAtLeastOneElementOfType(CustomEncoder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class UserStrategies {
|
||||
|
||||
@Bean
|
||||
RSocketStrategies customRSocketStrategies() {
|
||||
return RSocketStrategies.builder()
|
||||
.encoder(CharSequenceEncoder.textPlainOnly())
|
||||
.decoder(StringDecoder.textPlainOnly())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StrategiesCustomizer {
|
||||
|
||||
@Bean
|
||||
RSocketStrategiesCustomizer myCustomizer() {
|
||||
return (strategies) -> strategies.encoder(mock(CustomEncoder.class)).decoder(mock(CustomDecoder.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface CustomEncoder extends Encoder<String> {
|
||||
|
||||
}
|
||||
|
||||
interface CustomDecoder extends Decoder<String> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
|
||||
import org.springframework.boot.logging.LogLevel;
|
||||
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketWebSocketNettyRouteProvider}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class RSocketWebSocketNettyRouteProviderTests {
|
||||
|
||||
@Test
|
||||
void webEndpointsShouldWork() {
|
||||
new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
|
||||
RSocketStrategiesAutoConfiguration.class, RSocketServerAutoConfiguration.class,
|
||||
RSocketMessagingAutoConfiguration.class, RSocketRequesterAutoConfiguration.class))
|
||||
.withUserConfiguration(WebConfiguration.class)
|
||||
.withPropertyValues("spring.rsocket.server.transport=websocket",
|
||||
"spring.rsocket.server.mapping-path=/rsocket")
|
||||
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
|
||||
.run((context) -> {
|
||||
ReactiveWebServerApplicationContext serverContext = (ReactiveWebServerApplicationContext) context
|
||||
.getSourceApplicationContext();
|
||||
RSocketRequester requester = createRSocketRequester(context, serverContext.getWebServer());
|
||||
TestProtocol rsocketResponse = requester.route("websocket")
|
||||
.data(new TestProtocol("rsocket"))
|
||||
.retrieveMono(TestProtocol.class)
|
||||
.block(Duration.ofSeconds(3));
|
||||
assertThat(rsocketResponse.getName()).isEqualTo("rsocket");
|
||||
WebTestClient client = createWebTestClient(serverContext.getWebServer());
|
||||
client.get()
|
||||
.uri("/protocol")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("name")
|
||||
.isEqualTo("http");
|
||||
});
|
||||
}
|
||||
|
||||
private WebTestClient createWebTestClient(WebServer server) {
|
||||
return WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:" + server.getPort())
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
private RSocketRequester createRSocketRequester(ApplicationContext context, WebServer server) {
|
||||
int port = server.getPort();
|
||||
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
|
||||
return builder.dataMimeType(MediaType.APPLICATION_CBOR)
|
||||
.websocket(URI.create("ws://localhost:" + port + "/rsocket"));
|
||||
}
|
||||
|
||||
@EnableWebFlux
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class WebConfiguration {
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler(ApplicationContext context) {
|
||||
return WebHttpHandlerBuilder.applicationContext(context).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebController webController() {
|
||||
return new WebController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
NettyReactiveWebServerFactory customServerFactory(RSocketWebSocketNettyRouteProvider routeProvider) {
|
||||
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
|
||||
serverFactory.addRouteProviders(routeProvider);
|
||||
return serverFactory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings({ "removal", "deprecation" })
|
||||
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder objectMapperBuilder() {
|
||||
return new org.springframework.http.converter.json.Jackson2ObjectMapperBuilder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class WebController {
|
||||
|
||||
@GetMapping(path = "/protocol", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
TestProtocol testWebEndpoint() {
|
||||
return new TestProtocol("http");
|
||||
}
|
||||
|
||||
@MessageMapping("websocket")
|
||||
TestProtocol testRSocketEndpoint() {
|
||||
return new TestProtocol("rsocket");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestProtocol {
|
||||
|
||||
private String name;
|
||||
|
||||
TestProtocol() {
|
||||
}
|
||||
|
||||
TestProtocol(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.context;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RSocketPortInfoApplicationContextInitializer}.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class RSocketPortInfoApplicationContextInitializerTests {
|
||||
|
||||
@Test
|
||||
void whenServerHasAddressThenInitializerSetsPortProperty() {
|
||||
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) {
|
||||
context.getBean(RSocketPortInfoApplicationContextInitializer.class).initialize(context);
|
||||
RSocketServer server = mock(RSocketServer.class);
|
||||
given(server.address()).willReturn(new InetSocketAddress(65535));
|
||||
context.publishEvent(new RSocketServerInitializedEvent(server));
|
||||
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isEqualTo("65535");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerHasNoAddressThenInitializerDoesNotSetPortProperty() {
|
||||
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) {
|
||||
context.getBean(RSocketPortInfoApplicationContextInitializer.class).initialize(context);
|
||||
RSocketServer server = mock(RSocketServer.class);
|
||||
context.publishEvent(new RSocketServerInitializedEvent(server));
|
||||
then(server).should().address();
|
||||
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
RSocketPortInfoApplicationContextInitializer rSocketPortInfoApplicationContextInitializer() {
|
||||
return new RSocketPortInfoApplicationContextInitializer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.rsocket.netty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import io.rsocket.ConnectionSetupPayload;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import io.rsocket.transport.netty.client.TcpClientTransport;
|
||||
import io.rsocket.transport.netty.client.WebsocketClientTransport;
|
||||
import io.rsocket.util.DefaultPayload;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.Http11SslContextSpec;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
|
||||
import reactor.netty.tcp.TcpClient;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer.Transport;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.will;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link NettyRSocketServerFactory}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Leo Li
|
||||
* @author Chris Bono
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class NettyRSocketServerFactoryTests {
|
||||
|
||||
private NettyRSocketServer server;
|
||||
|
||||
private RSocketRequester requester;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (this.requester != null) {
|
||||
this.requester.rsocketClient().dispose();
|
||||
}
|
||||
if (this.server != null) {
|
||||
try {
|
||||
this.server.stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NettyRSocketServerFactory getFactory() {
|
||||
NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
|
||||
factory.setPort(0);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificPort() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
int specificPort = doWithRetry(() -> {
|
||||
factory.setPort(0);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
return this.server.address().getPort();
|
||||
});
|
||||
this.requester = createRSocketTcpClient();
|
||||
assertThat(this.server.address().getPort()).isEqualTo(specificPort);
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransport() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(RSocketServer.Transport.WEBSOCKET);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = createRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransportWithReactorResource() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(RSocketServer.Transport.WEBSOCKET);
|
||||
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
|
||||
resourceFactory.afterPropertiesSet();
|
||||
factory.setResourceFactory(resourceFactory);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = createRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverCustomizers() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
RSocketServerCustomizer[] customizers = new RSocketServerCustomizer[2];
|
||||
for (int i = 0; i < customizers.length; i++) {
|
||||
customizers[i] = mock(RSocketServerCustomizer.class);
|
||||
will((invocation) -> invocation.getArgument(0)).given(customizers[i])
|
||||
.customize(any(io.rsocket.core.RSocketServer.class));
|
||||
}
|
||||
factory.setRSocketServerCustomizers(Arrays.asList(customizers));
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (RSocketServerCustomizer customizer : customizers) {
|
||||
ordered.verify(customizer).customize(any(io.rsocket.core.RSocketServer.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void tcpTransportBasicSslFromClassPath() {
|
||||
testBasicSslWithKeyStore("classpath:test.jks", "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void tcpTransportBasicSslFromFileSystem(@ResourcePath("test.jks") String keyStore) {
|
||||
testBasicSslWithKeyStore(keyStore, "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void websocketTransportBasicSslFromClassPath() {
|
||||
testBasicSslWithKeyStore("classpath:test.jks", "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void websocketTransportBasicSslFromFileSystem(@ResourcePath("test.jks") String keyStore) {
|
||||
testBasicSslWithKeyStore(keyStore, "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void tcpTransportBasicSslCertificateFromClassPath() {
|
||||
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
|
||||
Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void tcpTransportBasicSslCertificateFromFileSystem(@ResourcePath("test-cert.pem") String testCert,
|
||||
@ResourcePath("test-key.pem") String testKey) {
|
||||
testBasicSslWithPemCertificate(testCert, testKey, testCert, Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void websocketTransportBasicSslCertificateFromClassPath() {
|
||||
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
|
||||
Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void websocketTransportBasicSslCertificateFromFileSystem(@ResourcePath("test-cert.pem") String testCert,
|
||||
@ResourcePath("test-key.pem") String testKey) {
|
||||
testBasicSslWithPemCertificate(testCert, testKey, testCert, Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void tcpTransportBasicSslFromClassPathWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void tcpTransportBasicSslFromFileSystemWithBundle(@ResourcePath("test.jks") String keyStore) {
|
||||
testBasicSslWithKeyStoreFromBundle(keyStore, "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void websocketTransportBasicSslFromClassPathWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void websocketTransportBasicSslFromFileSystemWithBundle(@ResourcePath("test.jks") String keyStore) {
|
||||
testBasicSslWithKeyStoreFromBundle(keyStore, "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void tcpTransportBasicSslCertificateFromClassPathWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
|
||||
"classpath:test-cert.pem", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void tcpTransportBasicSslCertificateFromFileSystemWithBundle(@ResourcePath("test-cert.pem") String testCert,
|
||||
@ResourcePath("test-key.pem") String testKey) {
|
||||
testBasicSslWithPemCertificateFromBundle(testCert, testKey, testCert, Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void websocketTransportBasicSslCertificateFromClassPathWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
|
||||
"classpath:test-cert.pem", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
|
||||
void websocketTransportBasicSslCertificateFromFileSystemWithBundle(@ResourcePath("test-cert.pem") String testCert,
|
||||
@ResourcePath("test-key.pem") String testKey) {
|
||||
testBasicSslWithPemCertificateFromBundle(testCert, testKey, testCert, Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
private void checkEchoRequest() {
|
||||
String payload = "test payload";
|
||||
Mono<String> response = this.requester.route("test").data(payload).retrieveMono(String.class);
|
||||
StepVerifier.create(response).expectNext(payload).expectComplete().verify(Duration.ofSeconds(30));
|
||||
}
|
||||
|
||||
private void testBasicSslWithKeyStore(String keyStore, String keyPassword, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore(keyStore);
|
||||
ssl.setKeyPassword(keyPassword);
|
||||
factory.setSsl(ssl);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
private void testBasicSslWithPemCertificate(String certificate, String certificatePrivateKey,
|
||||
String trustCertificate, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setCertificate(certificate);
|
||||
ssl.setCertificatePrivateKey(certificatePrivateKey);
|
||||
ssl.setTrustCertificate(trustCertificate);
|
||||
ssl.setKeyStorePassword("");
|
||||
factory.setSsl(ssl);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
private void testBasicSslWithKeyStoreFromBundle(String keyStore, String keyPassword, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(keyStore);
|
||||
JksSslStoreDetails trustStoreDetails = null;
|
||||
SslBundle sslBundle = SslBundle.of(new JksSslStoreBundle(keyStoreDetails, trustStoreDetails),
|
||||
SslBundleKey.of(keyPassword));
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
private void testBasicSslWithPemCertificateFromBundle(String certificate, String certificatePrivateKey,
|
||||
String trustCertificate, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(certificate)
|
||||
.withPrivateKey(certificatePrivateKey);
|
||||
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(trustCertificate);
|
||||
SslBundle sslBundle = SslBundle.of(new PemSslStoreBundle(keyStoreDetails, trustStoreDetails));
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportSslRejectsInsecureClient() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(Transport.TCP);
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:org/springframework/boot/rsocket/netty/test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
factory.setSsl(ssl);
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = createRSocketTcpClient();
|
||||
String payload = "test payload";
|
||||
Mono<String> responseMono = this.requester.route("test").data(payload).retrieveMono(String.class);
|
||||
StepVerifier.create(responseMono)
|
||||
.verifyErrorSatisfies((ex) -> assertThat(ex).isInstanceOf(ClosedChannelException.class));
|
||||
}
|
||||
|
||||
private RSocketRequester createRSocketTcpClient() {
|
||||
return createRSocketRequesterBuilder().transport(TcpClientTransport.create(createTcpClient()));
|
||||
}
|
||||
|
||||
private RSocketRequester createRSocketWebSocketClient() {
|
||||
return createRSocketRequesterBuilder().transport(WebsocketClientTransport.create(createHttpClient(), "/"));
|
||||
}
|
||||
|
||||
private RSocketRequester createSecureRSocketTcpClient() {
|
||||
return createRSocketRequesterBuilder().transport(TcpClientTransport.create(createSecureTcpClient()));
|
||||
}
|
||||
|
||||
private RSocketRequester createSecureRSocketWebSocketClient() {
|
||||
return createRSocketRequesterBuilder()
|
||||
.transport(WebsocketClientTransport.create(createSecureHttpClient(), "/"));
|
||||
}
|
||||
|
||||
private HttpClient createSecureHttpClient() {
|
||||
HttpClient httpClient = createHttpClient();
|
||||
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
|
||||
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
|
||||
.trustManager(InsecureTrustManagerFactory.INSTANCE));
|
||||
return httpClient.secure((spec) -> spec.sslContext(sslContextSpec));
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient() {
|
||||
Assertions.assertThat(this.server).isNotNull();
|
||||
InetSocketAddress address = this.server.address();
|
||||
return HttpClient.create().host(address.getHostName()).port(address.getPort());
|
||||
}
|
||||
|
||||
private TcpClient createSecureTcpClient() {
|
||||
TcpClient tcpClient = createTcpClient();
|
||||
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
|
||||
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
|
||||
.trustManager(InsecureTrustManagerFactory.INSTANCE));
|
||||
return tcpClient.secure((spec) -> spec.sslContext(sslContextSpec));
|
||||
}
|
||||
|
||||
private TcpClient createTcpClient() {
|
||||
Assertions.assertThat(this.server).isNotNull();
|
||||
InetSocketAddress address = this.server.address();
|
||||
return TcpClient.create().host(address.getHostName()).port(address.getPort());
|
||||
}
|
||||
|
||||
private RSocketRequester.Builder createRSocketRequesterBuilder() {
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.decoder(StringDecoder.allMimeTypes())
|
||||
.encoder(CharSequenceEncoder.allMimeTypes())
|
||||
.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
|
||||
.build();
|
||||
return RSocketRequester.builder().rsocketStrategies(strategies);
|
||||
}
|
||||
|
||||
private <T> T doWithRetry(Callable<T> action) {
|
||||
Exception lastFailure = null;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
try {
|
||||
return action.call();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
lastFailure = ex;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure);
|
||||
}
|
||||
|
||||
static class EchoRequestResponseAcceptor implements SocketAcceptor {
|
||||
|
||||
@Override
|
||||
public Mono<RSocket> accept(ConnectionSetupPayload setupPayload, RSocket rSocket) {
|
||||
return Mono.just(new RSocket() {
|
||||
|
||||
@Override
|
||||
public Mono<Payload> requestResponse(Payload payload) {
|
||||
return Mono.just(DefaultPayload.create(payload));
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
|
||||
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
|
||||
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
|
||||
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
|
||||
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
|
||||
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
|
||||
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
|
||||
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
|
||||
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
|
||||
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
|
||||
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
|
||||
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
|
||||
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
|
||||
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
|
||||
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
|
||||
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
|
||||
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
|
||||
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
|
||||
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,59 @@
|
||||
Bag Attributes
|
||||
friendlyName: test-alias
|
||||
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD8B1saBN8Y0ZjX
|
||||
Q/pvnv2hZa+cBBkFHVkgw+tdzgdcO5Kjv2rnRKd3Oh8Qwdxj3BC9GpicF4GOgkDG
|
||||
LrMrYbmV/xGUnNh3YpUwbv+U/pm3VnuSZCk4aYgNEWHrnEHyrge87+MqYqGuMIke
|
||||
A5z7GV44PeThfKQs2BLLL/ME+Pb/jN6IVlIHxdBT5TBQkqNGFU7swdZoQ5Viqv44
|
||||
UNdVqU5pP3UF/perbOm2CHCbeLiLRvmuvGuBbrbko2XUwBNH+UjmEQaRh/epoy8B
|
||||
D31vGY87ym/YOEdpIrqD8bwg7NWP/Fdsryqi8p+U8fcfw4xZjotA0Sv04BC7zXHg
|
||||
m1AyZoVzAgMBAAECggEAfEqiZqANaF+BqXQIb4Dw42ZTJzWsIyYYnPySOGZRoe5t
|
||||
QJ03uwtULYv34xtANe1DQgd6SMyc46ugBzzjtprQ3ET5Jhn99U6kdcjf+dpf85dO
|
||||
hOEppP0CkDNI39nleinSfh6uIOqYgt/D143/nqQhn8oCdSOzkbwT9KnWh1bC9T7I
|
||||
vFjGfElvt1/xl88qYgrWgYLgXaencNGgiv/4/M0FNhiHEGsVC7SCu6kapC/WIQpE
|
||||
5IdV+HR+tiLoGZhXlhqorY7QC4xKC4wwafVSiFxqDOQAuK+SMD4TCEv0Aop+c+SE
|
||||
YBigVTmgVeJkjK7IkTEhKkAEFmRF5/5w+bZD9FhTNQKBgQD+4fNG1ChSU8RdizZT
|
||||
5dPlDyAxpETSCEXFFVGtPPh2j93HDWn7XugNyjn5FylTH507QlabC+5wZqltdIjK
|
||||
GRB5MIinQ9/nR2fuwGc9s+0BiSEwNOUB1MWm7wWL/JUIiKq6sTi6sJIfsYg79zco
|
||||
qxl5WE94aoINx9Utq1cdWhwJTQKBgQD9IjPksd4Jprz8zMrGLzR8k1gqHyhv24qY
|
||||
EJ7jiHKKAP6xllTUYwh1IBSL6w2j5lfZPpIkb4Jlk2KUoX6fN81pWkBC/fTBUSIB
|
||||
EHM9bL51+yKEYUbGIy/gANuRbHXsWg3sjUsFTNPN4hGTFk3w2xChCyl/f5us8Lo8
|
||||
Z633SNdpvwKBgQCGyDU9XzNzVZihXtx7wS0sE7OSjKtX5cf/UCbA1V0OVUWR3SYO
|
||||
J0HPCQFfF0BjFHSwwYPKuaR9C8zMdLNhK5/qdh/NU7czNi9fsZ7moh7SkRFbzJzN
|
||||
OxbKD9t/CzJEMQEXeF/nWTfsSpUgILqqZtAxuuFLbAcaAnJYlCKdAumQgQKBgQCK
|
||||
mqjJh68pn7gJwGUjoYNe1xtGbSsqHI9F9ovZ0MPO1v6e5M7sQJHH+Fnnxzv/y8e8
|
||||
d6tz8e73iX1IHymDKv35uuZHCGF1XOR+qrA/KQUc+vcKf21OXsP/JtkTRs1HLoRD
|
||||
S5aRf2DWcfvniyYARSNU2xTM8GWgi2ueWbMDHUp+ZwKBgA/swC+K+Jg5DEWm6Sau
|
||||
e6y+eC6S+SoXEKkI3wf7m9aKoZo0y+jh8Gas6gratlc181pSM8O3vZG0n19b493I
|
||||
apCFomMLE56zEzvyzfpsNhFhk5MBMCn0LPyzX6MiynRlGyWIj0c99fbHI3pOMufP
|
||||
WgmVLTZ8uDcSW1MbdUCwFSk5
|
||||
-----END PRIVATE KEY-----
|
||||
Bag Attributes
|
||||
friendlyName: test-alias
|
||||
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
|
||||
subject=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
|
||||
issuer=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
|
||||
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
|
||||
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
|
||||
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
|
||||
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
|
||||
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
|
||||
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
|
||||
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
|
||||
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
|
||||
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
|
||||
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
|
||||
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
|
||||
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
|
||||
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
|
||||
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
|
||||
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
|
||||
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
|
||||
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
|
||||
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
Reference in New Issue
Block a user