I need to make my Spring Boot application start/stop listening on a new port dynamically.
I understand a new tomcat connector needs to be injected into Spring context for this.
I'm able to add a connector using a ServletWebServerFactory bean and tomcatConnectorCustomizer. But this bean is loaded only during Spring Bootup.
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
connector.setPort(serverPort);
connector.setScheme("https");
connector.setSecure(true);
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
protocol.setSSLEnabled(true);
protocol.setKeystoreType("PKCS12");
protocol.setKeystoreFile(keystorePath);
protocol.setKeystorePass(keystorePass);
protocol.setKeyAlias("spa");
protocol.setSSLVerifyClient(Boolean.toString(true));
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
return tomcat;
}
}
Is there any way to add a tomcat connector during run time? Say on a method call?
Update: Connector created but all requests to it return 404:
I've managed to add a Tomcat connector at runtime. But the request made to that port are not going to my RestController.
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
connector.setScheme("http");
connector.setSecure(false);
connector.setPort(8472);
protocol.setSSLEnabled(false);
};
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
tomcat.getWebServer().start();
How should I proceed further?
Hi here is my sample project: sample project
1- Main Application (DemoApplication.java):
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2 - Config file (AppConfig.java):
#Configuration
public class AppConfig {
#Autowired
private ServletWebServerApplicationContext server;
private static FilterConfig filterConfig = new FilterConfig();
#PostConstruct
void init() {
//setting default port config
filterConfig.addNewPortConfig(8080, "/admin");
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public FilterConfig createFilterConfig() {
return filterConfig;
}
public void addPort(String schema, String domain, int port, boolean secure) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
synchronized (this) {
ts.getTomcat().setConnector(createConnector(schema, domain, port, secure));
}
}
public void addContextAllowed(FilterConfig filterConfig, int port, String context) {
filterConfig.addNewPortConfig(port, context);
}
public void removePort(int port) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
Service service = ts.getTomcat().getService();
synchronized (this) {
Connector[] findConnectors = service.findConnectors();
for (Connector connector : findConnectors) {
if (connector.getPort() == port) {
try {
connector.stop();
connector.destroy();
filterConfig.removePortConfig(port);
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
}
}
private Connector createConnector(String schema, String domain, int port, boolean secure) {
Connector conn = new Connector("org.apache.coyote.http11.Http11NioProtocol");
conn.setScheme(schema);
conn.setPort(port);
conn.setSecure(true);
conn.setDomain(domain);
if (secure) {
// config secure port...
}
return conn;
}
}
3 - Filter (NewPortFilter.java):
public class NewPortFilter {
#Bean(name = "restrictFilter")
public FilterRegistrationBean<Filter> retstrictFilter(FilterConfig filterConfig) {
Filter filter = new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// get allowed url contexts
Set<String> config = filterConfig.getConfig().get(request.getLocalPort());
if (config == null || config.isEmpty()) {
response.sendError(403);
}
boolean accepted = false;
for (String value : config) {
if (request.getPathInfo().startsWith(value)) {
accepted = true;
break;
}
}
if (accepted) {
filterChain.doFilter(request, response);
} else {
response.sendError(403);
}
}
};
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("restrictFilter");
return filterRegistrationBean;
}
}
4 - Filter Config (FilterConfig.java):
public class FilterConfig {
private Map<Integer, Set<String>> acceptedContextsByPort = new ConcurrentHashMap<>();
public void addNewPortConfig(int port, String allowedContextUrl) {
if(port > 0 && allowedContextUrl != null) {
Set<String> set = acceptedContextsByPort.get(port);
if (set == null) {
set = new HashSet<>();
}
set = new HashSet<>(set);
set.add(allowedContextUrl);
acceptedContextsByPort.put(port, set);
}
}
public void removePortConfig(int port) {
if(port > 0) {
acceptedContextsByPort.remove(port);
}
}
public Map<Integer, Set<String>> getConfig(){
return acceptedContextsByPort;
}
}
5 - Controller (TestController.java):
#RestController
public class TestController {
#Autowired
AppConfig config;
#Autowired
FilterConfig filterConfig;
#GetMapping("/admin/hello")
String test() {
return "hello test";
}
#GetMapping("/alternative/hello")
String test2() {
return "hello test 2";
}
#GetMapping("/admin/addNewPort")
ResponseEntity<String> createNewPort(#RequestParam Integer port, #RequestParam String context) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.addPort("http", "localhost", port, false);
if (context != null && context.length() > 0) {
config.addContextAllowed(filterConfig, port, context);
}
return new ResponseEntity<>("Added port:" + port, HttpStatus.OK);
}
#GetMapping("/admin/removePort")
ResponseEntity<String> removePort(#RequestParam Integer port) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.removePort(port);
return new ResponseEntity<>("Removed port:" + port, HttpStatus.OK);
}
}
How to test it?
In a browser:
1 - try:
http://localhost:8080/admin/hello
Expected Response: hello test
2 - try:
http://localhost:8080/admin/addNewPort?port=9090&context=alternative
Expected Response: Added port:9090
3 - try:
http://localhost:9090/alternative/hello
Expected Response: hello test 2
4 - try expected errors:
http://localhost:9090/alternative/addNewPort?port=8181&context=alternative
Expected Response (context [alternative] allowed but endpoint not registered in controller for this context): Whitelabel Error Page...
http://localhost:9090/any/hello
Expected Response (context [any] not allowed): Whitelabel Error Page...
http://localhost:8888/any/hello
Expected Response (invalid port number): ERR_CONNECTION_REFUSED
http://localhost:8080/hello
Expected Response (no context allowed [/hello]): Whitelabel Error Page...
5 - try remove a port:
http://localhost:8080/admin/removePort?port=9090
6 - check removed port:
http://localhost:9090/alternative/hello
Expected Response (port closed): ERR_CONNECTION_REFUSED
I hope it helps.
You should create ServletWebServerFactory bean as a prototype bean using #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).
Now in bean where you need new tomcat connector to be injected into Spring context(MySingletonBean in example) autowire the application context and get ServletWebServerFactory bean(MyPrototypeBean in example) from getBean method. In this way you will always get new tomcat connector bean.
Following is a simple sample code:-
public class MySingletonBean {
#Autowired
private ApplicationContext applicationContext;
public void showMessage(){
MyPrototypeBean bean = applicationContext.getBean(MyPrototypeBean.class);
}
}
Based on the accepted answer of #ariel-carrera, I did it a little differently (IMO cleaner):
Define a normal Spring controller for handling the request on any dynamic port
import package.IgnoredBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.reflect.MethodUtils.getMethodsWithAnnotation;
//must be declared in a separate java file so that it's not picked up by component scanning as inner class
#IgnoredBean
#RestController
#Slf4j
#RequiredArgsConstructor
class DynamicController {
static final Method HANDLER_METHOD = getMethodsWithAnnotation(DynamicController.class, RequestMapping.class)[0];
private final String myContext;
#RequestMapping
public Object handle(
#RequestBody Map<String, Object> body,
#RequestParam MultiValueMap<String, Object> requestParams,
#PathVariable Map<String, Object> pathVariables
) {
Map<String, Object> allAttributes = new HashMap<>(body.size() + requestParams.size() + pathVariables.size());
allAttributes.putAll(body);
allAttributes.putAll(pathVariables);
requestParams.forEach((name, values) -> allAttributes.put(name, values.size() > 1 ? values : values.get(0)));
log.info("Handling request for '{}': {}", myContext, allAttributes);
return allAttributes;
}
// this handler only affects this particular controller. Otherwise it will use any of your regular #ControllerAdvice beans or fall back to spring's default
#ExceptionHandler
public ResponseEntity<?> onError(Exception e) {
log.debug("something happened in '{}'", myContext, e);
return ResponseEntity.status(500).body(Map.of("message", e.getMessage()));
}
}
Must be declared in its own file
It is not a Spring Bean, we will instantiate it manually for each port and give it some context objects that are relevant to the port owner.
Note the #IgnoredBean, a custom annotation:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target(TYPE)
#Retention(RUNTIME)
public #interface IgnoredBean {
}
#SpringBootApplication
#ComponentScan(excludeFilters = #ComponentScan.Filter(IgnoredBean.class))
...
public class MyApplication{...}
The rest of it
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toUnmodifiableSet;
#Service
#RequiredArgsConstructor
#Slf4j
class DynamicControllerService {
private final RequestMappingHandlerMapping requestHandlerMapper;
private final Map<Integer, RequestMappingInfo> mappingByPort = new ConcurrentHashMap<>();
private Tomcat tomcat;
#Autowired
void setTomcat(ServletWebServerApplicationContext context) {
tomcat = ((TomcatWebServer) context.getWebServer()).getTomcat();
}
public int addMapping(#Nullable Integer givenPort, RequestMethod method, String path, Object myContext) {
val connector = new Connector(new Http11NioProtocol());
connector.setThrowOnFailure(true);
//0 means it will pick any available port
connector.setPort(Optional.ofNullable(givenPort).orElse(0));
try {
tomcat.setConnector(connector);
} catch (IllegalArgumentException e) {
// if it fails to start the connector, the object will still be left inside here
tomcat.getService().removeConnector(connector);
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
int port = connector.getLocalPort();
val mapping = RequestMappingInfo
.paths(path)
.methods(method)
.customCondition(new PortRequestCondition(port))
.build();
requestHandlerMapper.registerMapping(
mapping,
new DynamicController("my context for port " + port),
DynamicController.HANDLER_METHOD
);
mappingByPort.put(port, mapping);
log.info("added mapping {} {} for port {}", method, path, port);
return port;
}
public void removeMapping(Integer port) {
Stream.of(tomcat.getService().findConnectors())
.filter(connector -> connector.getPort() == port)
.findFirst()
.ifPresent(connector -> {
try {
tomcat.getService().removeConnector(connector);
connector.destroy();
} catch (IllegalArgumentException | LifecycleException e) {
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
val mapping = mappingByPort.get(port);
requestHandlerMapper.unregisterMapping(mapping);
log.info("removed mapping {} {} for port {}",
mapping.getMethodsCondition().getMethods(),
Optional.ofNullable(mapping.getPathPatternsCondition())
.map(PathPatternsRequestCondition::getPatternValues)
.orElse(Set.of()),
port
);
});
}
#RequiredArgsConstructor
private static class PortRequestCondition implements RequestCondition<PortRequestCondition> {
private final Set<Integer> ports;
public PortRequestCondition(Integer... ports) {
this.ports = Set.of(ports);
}
#Override
public PortRequestCondition combine(PortRequestCondition other) {
return new PortRequestCondition(Stream.concat(ports.stream(), other.ports.stream()).collect(toUnmodifiableSet()));
}
#Override
public PortRequestCondition getMatchingCondition(HttpServletRequest request) {
return ports.contains(request.getLocalPort()) ? this : null;
}
#Override
public int compareTo(PortRequestCondition other, HttpServletRequest request) {
return 0;
}
}
}
I want to build a REST api in Spring Webflux using functional endpoints. For CORS I use a WebFilter corsFilter method which sets the required headers. I do see that the method is called (I see the log messages from it) and I see that the headers on the response are indeed the ones I set in my Webflux api. However, as soon as I started to use the corsFilter the requests return 404 status (earlier they would return JSON). I suspect that corsFilter doesn't pass over the request to the router functions. Why would that be?
Specifically I'm wondering if this line is enough to connect the cors config with the routes:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
This is my main class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import reactor.ipc.netty.http.server.HttpServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RequestPredicates.method;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
#SpringBootApplication
public class Server {
private static final Logger log = LogManager.getLogger(Server.class);
public static final String HOST = "localhost";
public static final int PORT = 8080;
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CorsConfiguration.class);
Server server = new Server();
server.startReactorServer(ctx);
System.out.println("Press ENTER to exit.");
System.in.read();
}
public RouterFunction<ServerResponse> routingFunction() {
PersonRepository repository = new DummyPersonRepository();
PersonHandler handler = new PersonHandler(repository);
return nest(path("/person"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), handler::getPerson)
.andRoute(method(HttpMethod.GET), handler::listPeople)
).andRoute(POST("/").and(contentType(APPLICATION_JSON)), handler::createPerson));
}
public void startReactorServer(AnnotationConfigApplicationContext ctx) {
RouterFunction<ServerResponse> route = this.routingFunction().filter((request, next) -> {
log.warn(request.path());
if (request.path().contains("person")) {
log.warn("calling next()");
return next.handle(request);
} else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(adapter).block();
}
}
and this is my CORS config class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Configuration
#EnableWebFlux
public class CorsConfiguration {
private static final Logger log = LogManager.getLogger(CorsConfiguration.class);
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN, mode";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "*";
private static final String MAX_AGE = "3600";
#Bean
public WebFilter corsFilter() {
log.warn("from CorsConfiguration!!!");
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
log.warn("after ServerHttpRequest");
if (CorsUtils.isCorsRequest(request)) {
log.warn("inside isCorsRequest");
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
To use the functional approach when defining your endpoints Spring Boot's official documentation has a very simple example.
FooBarApplication.class this is our main class.
#SpringBootApplication
public class FooBarApplication {
public static void main(String[] args) {
SpringApplication.run(FooBarApplication.class, args);
}
}
RoutingConfiguration.class (or whatever you wanna call it)
#Configuration
public class RoutingConfiguration {
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser)
.andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers)
.andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser);
}
}
#Component
public class UserHandler {
public Mono<ServerResponse> getUser(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
// ...
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
// ...
}
}
any class annotated with #Configuration will be run at startup and run all #Bean annotated methods. So this will run the monoRouterFunction and set up all our routes for us.
Example taken from the official spring boot documentation Spring boot webflux scroll down a little bit.
EDIT:
and as a side note the #EnableWebFlux annotation means that you will disable the auto-configuration of webflux and set upp configuration manually. I do not recommend this if you are just starting out (i know the name is very misleading) you can read about the webflux auto-configuration here Spring WebFlux Auto-configuration
EDIT2:
WebFlux has a built in CorsFilter that you can use all you need is to configure it.
#Bean
CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("http://allowed-origin.com"));
corsConfig.setMaxAge(8000L);
corsConfig.addAllowedMethod("PUT");
corsConfig.addAllowedHeader("Baeldung-Allowed");
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
Example taken from Enabling CORS with a WebFilter
SOLVED: see comment
I am currently trying to open a WebSockets connection between a spring server and an angular client with a x-auth-token. Therefore I have the following pieces of code:
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
LOGGER.info("registering websockets");
registry.addEndpoint("/api/v1/websocket").setAllowedOrigins("*").withSockJS();
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exceptio) {
LOGGER.info("ERRORORRRRRRRR");
exception.printStackTrace();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
LOGGER.info("in override");
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authToken = accessor.getFirstNativeHeader("x-auth-token");
LOGGER.info("Header auth token: " + authToken);
// Principal user = ... ; // access authentication header(s)
//accessor.setUser(user);
}
return message;
}
});
}
}
As you can see from my WebSocketConfig I am currently not really processing the x-auth-token, but rather trying to log it once. In the end I was trying to follow the Spring documentation: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication-token-based
The only problem I have with this is that in configureClientInboundChannel() the preSend() method seems not get called - and I cannot figure out why.
On the client side, the following error is displayed:
As you can see by comparing the error log and the endpoint defined in registerStompEndpoints() shows the actual endpoint is the correct one. Still I do not understand, why LOGGER.info("in override"); in configureClientInboundChannel() is not called.
I would really like to go with the auth-token being transferred via the stomp headers instead of the hacky token-as-URL-paramter-solution. Does someone here have an idea? Did I miss something in the already linked spring documentation?
Thanks in advance for your help!
I have a WebSocket Application using Spring MVC that I have defined as per this tutorial. The file that configures the WebSocket is the following:
package com.myapp.spring.web.controller;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.web.socket.server.standard.SpringConfigurator;
#ServerEndpoint(value="/serverendpoint", configurator = SpringConfigurator.class)
public class serverendpoint {
#OnOpen
public void handleOpen () {
System.out.println("JAVA: Client is now connected...");
}
#OnMessage
public String handleMessage (Session session, String message) throws IOException {
if (message.equals("ping")) {
// return "pong"
session.getBasicRemote().sendText("pong");
}
else if (message.equals("close")) {
handleClose();
return null;
}
System.out.println("JAVA: Received from client: "+ message);
MyClass mc = new MyClass(message);
String res = mc.action();
session.getBasicRemote().sendText(res);
return res;
}
#OnClose
public void handleClose() {
System.out.println("JAVA: Client is now disconnected...");
}
#OnError
public void handleError (Throwable t) {
t.printStackTrace();
}
}
My Question is if I have a Javascript client trying to connect to this WebSocket, what Uri should I use if the WebSocket is mapped at "/serverendpoint" as seen from the above #serverendpoint annotation?
var wsUri = "??????"
var webSocket = new WebSocket(wsUri);
What should wsUri be?
Here is my Spring MVC project hierarchy:
It is the same address for your server running the http port
Here is the codec and data format configuration:
package com.chorke.boot.jproxy.config;
import org.apache.camel.component.hl7.HL7DataFormat;
import org.apache.camel.component.hl7.HL7MLLPCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
#Configuration
#ImportResource({"classpath*:/META-INF/camel/applicationContext-camel.xml"})
public class ApacheCamelConfig {
#SuppressWarnings("unused")
private static final Logger log = LoggerFactory.getLogger(ApacheCamelConfig.class);
#Bean
public HL7MLLPCodec hl7codec() {
HL7MLLPCodec hl7codec = new HL7MLLPCodec();
hl7codec.setCharset("UTF-8");
return hl7codec;
}
#Bean
public HL7DataFormat hl7Format() {
HL7DataFormat hl7Format = new HL7DataFormat();
return hl7Format;
}
}
Here is the port forwarding route:
package com.chorke.boot.jproxy.route;
import org.apache.camel.builder.RouteBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProxyRoute extends RouteBuilder {
private static final Logger log =LoggerFactory.getLogger(ProxyRoute.class);
#Override
public void configure() throws Exception {
from("mina2://tcp://0.0.0.0:22210?codec=#hl7codec&sync=true").process(new Processor() {
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
log.info("Port-Forwarded body:\n {}", body);
}
}).to("mina2://tcp://192.168.0.10:22210?codec=#hl7codec&sync=true").end();
from("mina2://tcp://0.0.0.0:22211?codec=#hl7codec&sync=true").process(new Processor() {
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
log.info("Port-Forwarded body:\n {}", body);
}
}).to("mina2://tcp://192.168.0.11:22211?codec=#hl7codec&sync=true").end();
}
}
The route summery is:
============================================
request forwarded
tcp ip:port tcp ip:port
============================================
0.0.0.0:22210 192.168.0.10:22210
0.0.0.0:22211 192.168.0.11:22211
============================================
And it's working fine, it's protocol specific for MLLP. But our goal is to route any request regardless their protocol. Lets say it could be handle any kind of request, not limited to HTTP, REST, SOAP, MLLP, SMTP, FTP, SMB or etc. Would you please help us to configure port-forwarding route regardless their protocol.
There is no out of the box support for protocol detection and route port forwarding in Apache Camel. You are better finding some existing product that can do that.