Just wonder if anyone has experimented with WebSocket proxying (for transparent proxy) using embedded Jetty?
After about a day and a half playing with Jetty 9.1.2.v20140210, all I can tell is that it can't proxy WebSockets in its current form, and adding such support is non-trivial task (afaict at least).
Basically, Jetty ProxyServlet strips out the "Upgrade" and "Connection" header fields regardless of whether it's from a WebSocket handshake request. Adding these fields back is easy as shown below. But, when the proxied server returned a response with HTTP code 101 (switching protocols), no protocol upgrade is done on the proxy server. So, when the first WebSocket packet arrives, the HttpParser chokes and see that as a bad HTTP request.
If anyone already has a solution for it or is familiar with Jetty to suggest what to try, that would be very much appreciated.
Below is the code in my experiment stripping out the unimportant bits:
public class ProxyServer
{
public static void main(String[] args) throws Exception
{
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(8888);
server.addConnector(connector);
// Setup proxy handler to handle CONNECT methods
ConnectHandler proxy = new ConnectHandler();
server.setHandler(proxy);
// Setup proxy servlet
ServletContextHandler context = new ServletContextHandler(proxy, "/", ServletContextHandler.SESSIONS);
ServletHolder proxyServlet = new ServletHolder(MyProxyServlet.class);
context.addServlet(proxyServlet, "/*");
server.start();
}
}
#SuppressWarnings("serial")
public class MyProxyServlet extends ProxyServlet
{
#Override
protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request)
{
// Pass through the upgrade and connection header fields for websocket handshake request.
String upgradeValue = request.getHeader("Upgrade");
if (upgradeValue != null && upgradeValue.compareToIgnoreCase("websocket") == 0)
{
setHeader(proxyRequest, "Upgrade", upgradeValue);
setHeader(proxyRequest, "Connection", request.getHeader("Connection"));
}
}
#Override
protected void onResponseHeaders(HttpServletRequest request, HttpServletResponse response, Response proxyResponse)
{
super.onResponseHeaders(request, response, proxyResponse);
// Restore the upgrade and connection header fields for websocket handshake request.
HttpFields fields = proxyResponse.getHeaders();
for (HttpField field : fields)
{
if (field.getName().compareToIgnoreCase("Upgrade") == 0)
{
String upgradeValue = field.getValue();
if (upgradeValue != null && upgradeValue.compareToIgnoreCase("websocket") == 0)
{
response.setHeader(field.getName(), upgradeValue);
for (HttpField searchField : fields)
{
if (searchField.getName().compareToIgnoreCase("Connection") == 0) {
response.setHeader(searchField.getName(), searchField.getValue());
}
}
}
}
}
}
}
Let's imagine the proxy scheme that you are trying to build, we have client A, server B and proxy P. Now let's walk through connect workflow:
A established TCP connection with proxy P (A-P)
A sends CONNECT addr(B) request with WebSocket handshake
Here you have the first problem, by HTTP RFC headers used in WS handshake are not end-to-end headers, because for HTTP they make sense only on a transport layer (between two hops).
P establishes TCP connection to B (P-B)
P sends WS handshake HTTP request to B
B responds with HTTP->WS upgrade (by sending 101)
And here is another problem, after sending HTTP 101 server B and client A now will communicate only over TCP, but jetty servlet doesn't support plain TCP packets propagation. In other words jetty proxy servlet waits till client A will start transmitting HTTP request, which will never happen after A will receive HTTP 101.
You would need to implement this by yourself using WS server and WS client.
Related
I am trying to have my STOMP client connect to a Websocket server over a Spring Gateway server.
My STOMP client can connect directly to the Websocket Server just fine, but i am having trouble using the gateway.
But it does not work.
I am using spring-cloud-gateway-mvc for the proxy.
Here my general purpose controller-method that does the proxying:
private final GatewayAssistant gatewayAssistant;
#RequestMapping(value = "/{service}/**")
public ResponseEntity<?> proxy(ProxyExchange<Object> proxy, #PathVariable String service, HttpServletRequest request, HttpServletResponse response) {
//Assistant method to get the correct path to the Websocket server
String path = gatewayAssistant.getUrl(proxy, service);
proxy.uri(path);
if(request.getHeader("Upgrade") != null){
path = path.replace("http", "ws"); //a temporary hack
}
//Assistant method to call the correct method, e.G if request.getMethod() is GET, it will call proxy.get()
final ResponseEntity<?> call = gatewayAssistant.call(proxy, RequestMethod.valueOf(request.getMethod()));
return call;
}
Here the Assistant class:
#Component
#RequiredArgsConstructor
public class GatewayAssistant {
private final ApplicationProperties applicationProperties;
public String getUrl(ProxyExchange<Object> proxy, String service) {
final String urlBase = applicationProperties.getGatewayPaths().get(service);
if(Objects.isNull(urlBase)) {
throw new ApplicationManagementException("No service '".concat(service).concat("' available"), HttpStatus.BAD_REQUEST);
}
final String path = proxy.path("/api/".concat(service));
return urlBase.concat(path);
}
public ResponseEntity<?> call(ProxyExchange<Object> proxy, RequestMethod method) {
return switch(method) {
case OPTIONS -> proxy.options();
case GET -> proxy.get();
case PUT -> proxy.put();
case HEAD -> proxy.head();
case POST -> proxy.post();
case PATCH -> proxy.patch();
case DELETE -> proxy.delete();
case TRACE -> throw new ApplicationManagementException("Cannot handle TRACE calls", HttpStatus.BAD_REQUEST);
};
}
}
I use simple Javascript to connect to the websocket:
let sock = new SockJS("http://my-proxy:8100/api/communication/stomp");
When i connect, then the first call gets proxied just fine, but the second call that has the "ws:" schema does not get proxied. In the WebSocket serer i get the message "Handshake failed due to invalid Upgrade header: null".
UPDATE
I get the error because use the "ws" schema. But now that i fixed that, i get no exceptions in both server, in debug logging i see no errors, yes, the front-end browser client gets "failed: Error during WebSocket handshake: Unexpected response code: 500"
Hello I am new on spring integration
I checked examples for Spring Integration dynamic routing. Finally ı found it in here
Dynamic TCP Client
In here there were lines
#Component
#MessagingGateway(defaultRequestChannel = "toTcp.input")
public interface TcpClientGateway {
byte[] send(String data, #Header("host") String host, #Header("port") int port);
}
private MessageChannel createNewSubflow(Message<?> message) {
String host = (String) message.getHeaders().get("host");
Integer port = (Integer) message.getHeaders().get("port");
Assert.state(host != null && port != null, "host and/or port header missing");
String hostPort = host + port;
TcpNetClientConnectionFactory cf = new TcpNetClientConnectionFactory(host, port);
TcpSendingMessageHandler handler = new TcpSendingMessageHandler();
handler.setConnectionFactory(cf);
IntegrationFlow flow = f -> f.handle(handler);
IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
this.flowContext.registration(flow)
.addBean(cf)
.id(hostPort + ".flow")
.register();
MessageChannel inputChannel = flowRegistration.getInputChannel();
this.subFlows.put(hostPort, inputChannel);
return inputChannel;
}
but i changed it with
private MessageChannel createNewSubflow(Message<?> message) {
String host = (String) message.getHeaders().get("host");
Integer port = (Integer) message.getHeaders().get("port");
Assert.state(host != null && port != null, "host and/or port header missing");
String hostPort = host + port;
TcpNetClientConnectionFactory cf = new TcpNetClientConnectionFactory(host, port);
cf.setLeaveOpen(true);
//cf.setSingleUse(true);
ByteArrayCrLfSerializer byteArrayCrLfSerializer =new ByteArrayCrLfSerializer();
byteArrayCrLfSerializer.setMaxMessageSize(1048576);
cf.setSerializer(byteArrayCrLfSerializer);
cf.setDeserializer(byteArrayCrLfSerializer);
TcpOutboundGateway tcpOutboundGateway = new TcpOutboundGateway();
tcpOutboundGateway.setConnectionFactory(cf);
IntegrationFlow flow = f -> f.handle(tcpOutboundGateway);
IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
this.flowContext.registration(flow)
.addBean(cf)
.id(hostPort + ".flow")
.register();
MessageChannel inputChannel = flowRegistration.getInputChannel();
this.subFlows.put(hostPort, inputChannel);
return inputChannel;
}
to work with request/response architecture. It really works fine because it provides dynamic routing with out creating tcp clients by hand.
At this point i need some help to improve my scenario. My scenario is like that;
Client sends a message to Server and receive that message's response from server but then server needs to send arbitrary messages to that client (it is like GPS location update information). When server starts to send these messages to client generates error messages like below
ERROR 54816 --- [pool-2-thread-1] o.s.i.ip.tcp.TcpOutboundGateway : Cannot correlate response - no pending reply for ::58628:62fd67b6-af2d-42f1-9c4d-d232fbe9c8ca
I checked spring integration document and noticed that Gateways is working only with request/response so i learned that i should use adapters but i do not know how should i use adapters with dynamic tcp client.
here ı found similar topics and some responses but could not reach my goal or found example to combine solutions.
Spring Integration TCP
Spring integration TCP server push to client
You just need to register two flows; one for input; one for output - the problem is correlating the response for the reply, and routing the arbitrary messages to some place other than the gateway.
I updated the sample for this use case on this branch.
You can see the changes in the last commit on that branch; most of the changes were to simulate your server side.
On the client side, we simply register two flows and use a #ServiceActivator method to get the inbound messages; you can identify which server they come from via the connection id.
I am working on a Spring application that serves up REST endpoints. One of the endpoints essentially acts as a proxy between the HTML client and a third party cloud storage provider. This endpoint retrieves files from the storage provider and proxies them back to the client. Something like the following (note there is a synchronous and asynchronous version of the same endpoint):
#Controller
public class CloudStorageController {
...
#RequestMapping(value = "/fetch-image/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<byte[]> fetchImageSynchronous(#PathVariable final Long id) {
final byte[] imageFileContents = this.fetchImage(id);
return ResponseEntity.ok().body(imageFileContents);
}
#RequestMapping(value = "/fetch-image-async/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public Callable<ResponseEntity<byte[]>> fetchImageAsynchronous(#PathVariable final Long id) {
return () -> {
final byte[] imageFileContents = this.fetchImage(id);
return ResponseEntity.ok().body(imageFileContents);
};
}
private byte[] fetchImage(final long id) {
// fetch the file from cloud storage and return as byte array
...
}
...
}
Due to the nature of the client app (HTML5 + ajax) and how this endpoint is used, user authentication is supplied to this endpoint differently that the other endpoints. To handle this, a HandlerInterceptor was developed to deal with authentication for this endpoint:
#Component("cloudStorageAuthenticationInterceptor")
public class CloudStorageAuthenticationInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
// examine the request for the authentication information and verify it
final Authentication authenticated = ...
if (authenticated == null) {
try {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
else {
try {
request.login(authenticated.getName(), (String) authenticated.getCredentials());
} catch (final ServletException e) {
throw new BadCredentialsException("Bad credentials");
}
}
return true;
}
}
The interceptor is registered like this:
#Configuration
#EnableWebMvc
public class ApiConfig extends WebMvcConfigurerAdapter {
#Autowired
#Qualifier("cloudStorageAuthenticationInterceptor")
private HandlerInterceptor cloudStorageAuthenticationInterceptor;
#Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(this.cloudStorageAuthenticationInterceptor)
.addPathPatterns(
"/fetch-image/**",
"/fetch-image-async/**"
);
}
#Override
public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(this.asyncThreadPoolCoreSize);
executor.setMaxPoolSize(this.asyncThreadPoolMaxSize);
executor.setQueueCapacity(this.asyncThreadPoolQueueCapacity);
executor.setThreadNamePrefix(this.asyncThreadPoolPrefix);
executor.initialize();
configurer.setTaskExecutor(executor);
super.configureAsyncSupport(configurer);
}
}
Ideally, the image fetching would be done asynchronously (using the /fetch-image-asyc/{id} endpoint) because it has to call a third party web service which could have some latency.
The synchronous endpoint (/fetch-image/{id}) works correctly for all browsers. However, if using the asynchronous endpoint (/fetch-image-async/{id}), Chrome and Firefox work as expect.
However, if the client is Microsoft IE or Microsoft Edge, we seem some strange behavior. The endpoint is called correctly and the response sent successfully (at least from the server's viewpoint). However, it seems that the browser is waiting for something additional. In the IE/Edge DevTools window, the network request for the image shows as pending for 30 seconds, then seems to timeout, updates to successful and the image is successfully display. It also seems the connection to the server is still open, as the server side resources like database connections are not released. In the other browsers, the async response is received and processed in a second or less.
If I remove the HandlerInterceptor and just hard-wire some credentials for debugging, the behavior goes away. So this seems to have something to with the interaction between the HandlerInterceptor and the asynchronous controller method, and is only exhibited for some clients.
Anyone have a suggestion on why the semantics of IE/Edge are causing this behavior?
Based on your description, there are some different behaviors when using IE or Edge
it seems that the browser is waiting for something additional
the connection seems still open
it works fine if remove HandlerInterceptor and use hard code in auth logic
For the first behavior, I would suggest you use fiddler to trace all http requests. It is better if you could compare two different actions via fiddler (1) run on chrome, 2) run on edge ). Check all http headers in requests and responses carefully to see whether there is some different part. For the other behaviors, I would suggest you write logs to find which part spend the most time. It will provide you useful information to troubleshot.
After much tracing on the server and reading through the JavaDocs comments for AsyncHandlerInterceptor, I was able to resolve the issue. For requests to asynchronous controller methods, the preHandle method of any interceptor is called twice. It is called before the request is handed off to the servlet handling the request and again after the servlet has handled the request. In my case, the interceptor was attempting to authenticate the request for both scenarios (pre and post request handling). The application's authentication provider checks credentials in a database. For some reason if the client is IE or Edge, the authentication provider was unable to get a database connection when called from preHandle in the interceptor after the servlet handled the request. The following exception would be thrown:
ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataAccessResourceFailureException: Could not open connection; nested exception is org.hibernate.exception.JDBCConnectionException: Could not open connection] with root cause
java.sql.SQLTransientConnectionException: HikariPool-0 - Connection is not available, request timed out after 30001ms.
So the servlet would successfully handle the request and send a response, but the filter would get hung up for 30 seconds waiting for the database connection to timeout on the post processing called to preHandle.
So for me, the simple solution was to add a check in preHandle if it is being called after the servlet has already handled the request. I updated the preHandle method as follows:
#Override
public boolean preHandle(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Object pHandler) {
if (pRequest.getDispatcherType().equals(DispatcherType.REQUEST)) {
... perform authentication ...
}
return true;
}
That solved the issue for me. It doesn't explain everything (i.e., why only IE/Edge would cause the issue), but it seems that preHandle should only do work before the servlet handles the request anyways.
I have a Spring Boot app that normally runs on tomcat (as a normal WAR), and for some URL patterns I have Spring-Security setup to force it to be HTTPS. All the HTTPS/certificates stuff is done outside of my application, so all this step (at least appears) to be doing is redirecting to the HTTPS address (and port).
My security configuration:
http.requiresChannel().requestMatchers( "/**" ) .requiresSecure()
It all works as expected on tomcat, and I get redirected correctly.
Now I want to write some Spring #IntegrationTests, but am not able to get it to make requests to the HTTPS URL - I suspect I am missing something fairly obvious, but not having any joy with my attempts.
My basic test case:
#RunWith( SpringJUnit4ClassRunner )
#SpringApplicationConfiguration( classes = Application )
#WebAppConfiguration
#IntegrationTest( [ "server.port:0" ] )
class MultipleUserAuthenticationTest {
#Value('${local.server.port}') private int port
private URL base
private RestTemplate template
#Before public void setUp() throws Exception {
this.base = new URL("https://localhost:" + port + "/api")
template = new RestTemplate()
}
#Test public void apiRequest() throws Exception {
HttpHeaders headers = //set custom headers
HttpEntity<String> entity = new HttpEntity<String>( headers )
ResponseEntity<String> exchange = template.exchange( base.toString(), HttpMethod.GET, entity, String )
}
I have tried the approach outlined here https://stackoverflow.com/a/34411279/258813 and https://stackoverflow.com/a/24491820/258813
But no joy - It still seems to be failing because the port it is attempting to access is the HTTP port, so I basically get:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:52002/api":Unrecognized SSL message, plaintext connection?; nested exception is javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
Am I missing something obvious? Is there another way to get the HTTPS port (or tell spring to use one)?
I am using org.eclipse.jetty.websocket.client.WebSocketClient to establish a websocket connection.
After the initial handshake(Protocol switch/Upgrade) the websocket session is established.
Here is the code snipped i am using:
WebSocketClient client = new WebSocketClient();
client.start();
URI echoUri = new URI("destinationURI");
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("myCustomHeader", "CustomHeader");
client.connect(socket, echoUri, request);
Collection<WebSocketSession> sessions = client.getConnectionManager().getSessions();
for (WebSocketSession webSocketSession : sessions) {
webSocketSession.getRemote().sendString("<Custome message>");//I am able to recieve the messages //to the configured message handler
}
My message handler looks like:
#Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message//This is what i sent above) throws Exception {
session.getHandshakeHeaders();//This has "myCustomHeader", "CustomHeader"
BinaryMessage binaryMessage = new BinaryMessage(new String(
"Hello . This is message sent from server").getBytes());
session.sendMessage(binaryMessage);
}
Is it possible to send a custom header, after the web socket session is established?
Here is what i tried:
WebSocketClient client = new WebSocketClient();
client.start();
URI echoUri = new URI("destinationURI");
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("myCustomHeader", "CustomHeader");
client.connect(socket, echoUri, request);
Collection<WebSocketSession> sessions = client.getConnectionManager().getSessions();
for (WebSocketSession webSocketSession : sessions) {
webSocketSession.getUpgradeRequest().setHeader("mySecondCustomHeader","MySecondCustomHeader");
webSocketSession.getRemote().sendString("<Custome message>");//I am able to recieve the messages //to the configured message handler
}
I am only getting myCustomHeader and not mySecondCustomHeader in session.getHandshakeHeaders()
#Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message//This is what i sent above) throws Exception {
session.getHandshakeHeaders();//This has "myCustomHeader", "CustomHeader"
BinaryMessage binaryMessage = new BinaryMessage(new String(
"Hello . This is message sent from server").getBytes());
session.sendMessage(binaryMessage);
}
s it possible to send a custom header, after the web socket session is
established?
No, it is not possible. Once the HTTP negotiation has concluded, the connection only uses binary frames to communicate and cannot do more HTTP interactions.