Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Boot - Interception of HTTP request headers at request upgrade/handshake #831

Closed
maxim-bandurko-lsvt opened this issue May 10, 2020 · 6 comments
Labels

Comments

@maxim-bandurko-lsvt
Copy link

I was trying to implement the native browser Cookie/Session authentication to RSocket plugged to server at Spring Boot, and can't find easier way of how intercept the request headers passed before handshake process not using much reflection overriding and excluding of private classes. May be there is some easier way exists already that allows to configure a bean that will intercept request headers from client and can push connection to be closed with error if that is needed?

Also, may be it is even too much in this one thread, but would be good to have an option to add to response headers custom values exactly on the moment of HTTP request upgrade to WebSockets?

This will allow to implement native browser Cookie/Session authentication to RSocket and stop dealing with tokens. Like, browser makes a request to server to have it upgraded to websockets, server checks for cookie inside request headers, and if server allows, server will make a response to make request upgrade. But in situation, when cookies are missing some properties, server will pass additional cookie property with value to the upgrade response, or for example, pass property to upgrade response and not allow upgrade (so browser will got a cookie property with closed request), etc.

So, the easier way that allows just to pass a function, that will allow to check request headers, and ideally, add something more to upgrade response headers, would help a lot.

I had similar process implemented in basic Nodejs websocket server, so trying to implement same inside Java server with minimal overrides to native Java classes.

Thank you.

@rstoyanchev
Copy link
Contributor

@maxim-bandurko-lsvt there have been a few changes for 1.0.1 that make this easier. Take a look at the current version of WebSocketHeadersSample. That should show you how to do this.

@maxim-bandurko-lsvt
Copy link
Author

@rstoyanchev Got it, thanks!

@mmaask
Copy link

mmaask commented May 28, 2021

@maxim-bandurko-lsvt did you manage to implement such interceptor for Spring Boot? Started digging through the RSocket server code and Spring Security side, but haven't found an appropriate place for extending the functionality.

@maxim-bandurko-lsvt
Copy link
Author

@mmaask It depends on what exact level is needed to implement headers inside actual spring boot. I have very draft sample only for ws level that I implemented using my custom override built, because this class is still private:
https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java

Not a big deal, as I just make my own class that works how I need and overriding beans from:
https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java

My goal was to not only to authenticate WS connection by headers/cookies, I was planing to populate the user id or whole user session object directly to @MessageMapping by passing the id into modified ConnectionSetupPayload that stores data inside socket for example. But still haven't implemented that yet.

Any way, at those class RSocketWebSocketNettyRouteProvider you can do all needed things for WebsocketInbound and WebsocketOutbound

@maxim-bandurko-lsvt
Copy link
Author

@mmaask Well, I finally came to the moment that need to implement this Cookie/Session auth, and first way I did how I explained year ago in my comment above, but it turned to be not so nice in terms of implementation.

Biggest "stone" was spring-projects/spring-boot#18549 with not public RSocketWebSocketNettyRouteProvider. So had to follow the same way out that @FWinkler79 originally described by placing custom bean configuration in the same package as RSocketWebSocketNettyRouteProvider. So finally had something like:

	@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.route(HttpPredicate.get(this.mappingPath), (req, resp) -> {
				if (req.requestHeaders() .containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
		
					if (req.requestHeaders().containsValue("Authorization", "test", true)){

						HttpServerOperations ops = (HttpServerOperations) req;
						return ops.withWebsocketSupport(
								req.uri(),
								WebsocketServerSpec.builder().build(),
								(in, out) -> connectionAcceptor.apply(new WebsocketDuplexConnection((Connection) in)).then(out.neverComplete())
						);

					}

					resp.status(HttpResponseStatus.UNAUTHORIZED);
					return res.send();
		
				}
				return resp.sendNotFound();
		}); 
	}

But after that I had to store some auth status to be accessed by WebsocketDuplexConnection, and again one more stone was that it is final class, and can't extend it. So had to use a wrapper for it that wasn't very convenient also.

So, decided to switch to much easier implementation for right now by still keeping auth process per stream, but getting token from Cookie inside header (using Reflection as to get the needed private fields values):

    @MessageMapping("some")
    public Flux<DataBuffer> some(DataBuffer request, RSocketRequester requester) {

        RSocket rsocket = requester.rsocket();

        Field idcField = ReflectionUtils.findField(rsocket.getClass(), "connection");
        idcField.setAccessible(true);
        DuplexConnection idc = (DuplexConnection) ReflectionUtils.getField(idcField, rsocket);

        Field shdcField = ReflectionUtils.findField(idc.getClass(), "source");
        shdcField.setAccessible(true);
        DuplexConnection shdc = (DuplexConnection) ReflectionUtils.getField(shdcField, idc);

        Field wdcField = ReflectionUtils.findField(shdc.getClass(), "source");
        wdcField.setAccessible(true);
        WebsocketDuplexConnection wdc = (WebsocketDuplexConnection) ReflectionUtils.getField(wdcField, shdc);

        Field wiField = ReflectionUtils.findField(wdc.getClass(), "connection");
        wiField.setAccessible(true);
        WebsocketInbound wi = (WebsocketInbound) ReflectionUtils.getField(wiField, wdc);

        log.info("ConnectMapping - header Cookie: " + wi.headers().get(HttpHeaders.COOKIE));

        return Flux.empty();

    }

Btw, it looks like having actual auth process would be easier to implement inside SocketAcceptorInterceptor and just store the auth status in some custom connection wrapper inside requester, so this way auth will be per connection and after that status that can be any Object can be re-used inside MessageMapping requesters (a little example discussed: #970 )

@maxim-bandurko-lsvt
Copy link
Author

@mmaask Actually, decided to finish with implementation and do auth per connection.

So:

  1. RSocketServerCustomizerCustom:
@Slf4j
@Configuration
public class RSocketServerCustomizerCustom implements RSocketServerCustomizer {

    @Override
    public void customize(RSocketServer rSocketServer) {

        rSocketServer.interceptors(
            (InterceptorRegistry configurer) -> {
                log.info("Adding RSocket interceptors...");
                configurer.forSocketAcceptor(new SocketAcceptorInterceptorCustom());
            }
        );

        
    }


}
  1. SocketAcceptorInterceptorCustom:
@Slf4j
public class SocketAcceptorInterceptorCustom implements SocketAcceptorInterceptor {
    

    @Override
    public SocketAcceptor apply(SocketAcceptor socketAcceptor) {


        return (ConnectionSetupPayload setup, RSocket sendingSocket) -> {
            try {
                
                Field idcField = ReflectionUtils.findField(sendingSocket.getClass(), "connection");
                idcField.setAccessible(true);
                DuplexConnection idc = (DuplexConnection) ReflectionUtils.getField(idcField, sendingSocket);

                Field shdcField = ReflectionUtils.findField(idc.getClass(), "source");
                shdcField.setAccessible(true);
                DuplexConnection shdc = (DuplexConnection) ReflectionUtils.getField(shdcField, idc);

                Field wdcField = ReflectionUtils.findField(shdc.getClass(), "source");
                wdcField.setAccessible(true);
                WebsocketDuplexConnection wdc = (WebsocketDuplexConnection) ReflectionUtils.getField(wdcField, shdc);

                WebsocketDuplexConnectionCustom wdcCustom = new WebsocketDuplexConnectionCustom(wdc);

                synchronized(shdc){
                    ReflectionUtils.setField(wdcField, shdc, wdcCustom);
                }

                log.info("SocketAcceptorInterceptorCustom - header Cookie: " + wdcCustom.headers().get(HttpHeaders.COOKIE));

                // Do some auth logic, can use setup payload also
                //ConnAuthDTO authInfo = new ObjectMapper().readValue(setup.getDataUtf8(), ConnAuthDTO.class);

                wdcCustom.setSession(true);
                

                return socketAcceptor.accept(setup, sendingSocket);
            } catch (Exception e) {
                String errMsg = "try to accept a connection error.msg";
                log.warn(errMsg, e);
                return Mono.error(e);
            }
        };
    }

}

It replaces existing WebsocketDuplexConnection wrapped with WebsocketDuplexConnectionCustom. And there you can do any Auth logic using headers or setup payload, and cache the session state value (can be any object) for reuse inside MessageMappings.

  1. WebsocketDuplexConnectionCustom:
public class WebsocketDuplexConnectionCustom implements DuplexConnection {

    private final WebsocketDuplexConnection websocketDuplexConnection;

    private final Field websocketDuplexConnectionField;
    private final Method doOnCloseMethod;


    private AtomicReference<Object> session = new AtomicReference<Object>();


    WebsocketDuplexConnectionCustom(WebsocketDuplexConnection websocketDuplexConnection){
        this.websocketDuplexConnection = websocketDuplexConnection;

        this.websocketDuplexConnectionField = ReflectionUtils.findField(websocketDuplexConnection.getClass(), "connection");
        websocketDuplexConnectionField.setAccessible(true);

        this.doOnCloseMethod = ReflectionUtils.findMethod(websocketDuplexConnection.getClass(), "doOnClose");
        doOnCloseMethod.setAccessible(true);

        session.set(null);
    }



    @Override
    public ByteBufAllocator alloc() {
        return websocketDuplexConnection.alloc();
    }

    @Override
    public SocketAddress remoteAddress() {
        return websocketDuplexConnection.remoteAddress();
    }

    //@Override
    protected void doOnClose() {
        ReflectionUtils.invokeMethod(doOnCloseMethod, websocketDuplexConnection);
    }

    @Override
    public Flux<ByteBuf> receive() {
        return websocketDuplexConnection.receive();
    }

    @Override
    public void sendErrorAndClose(RSocketErrorException e) {
        websocketDuplexConnection.sendErrorAndClose(e);
    }


    @Override
    public Mono<Void> onClose() {
        return websocketDuplexConnection.onClose();
    }



    @Override
    public void dispose() {
        websocketDuplexConnection.dispose();
    }



    @Override
    public void sendFrame(int streamId, ByteBuf frame) {
        websocketDuplexConnection.sendFrame(streamId, frame);
    }


    public Connection getConnection(){
        return (Connection) ReflectionUtils.getField(websocketDuplexConnectionField, websocketDuplexConnection);
    }


    public HttpHeaders headers(){
        WebsocketInbound wi = (WebsocketInbound) getConnection();
        return wi.headers();
    }
    


    public void setSession(Object session){
        this.session.set(session);
    }

    public Object getSession(){
        return session.get();
    }

}
  1. Any message mapping:
    @MessageMapping("some")
    public Flux<DataBuffer> some(DataBuffer request, RSocketRequester requester) {

        RSocket rsocket = requester.rsocket();

        Field idcField = ReflectionUtils.findField(rsocket.getClass(), "connection");
        idcField.setAccessible(true);
        DuplexConnection idc = (DuplexConnection) ReflectionUtils.getField(idcField, rsocket);

        Field shdcField = ReflectionUtils.findField(idc.getClass(), "source");
        shdcField.setAccessible(true);
        DuplexConnection shdc = (DuplexConnection) ReflectionUtils.getField(shdcField, idc);

        Field wdcField = ReflectionUtils.findField(shdc.getClass(), "source");
        wdcField.setAccessible(true);
        WebsocketDuplexConnectionCustom wdc = (WebsocketDuplexConnectionCustom) ReflectionUtils.getField(wdcField, shdc);

        log.info("MessageMapping - header Cookie: " + wdc.headers().get(HttpHeaders.COOKIE));
        log.info("MessageMapping - session: " + wdc.getSession());

        return Flux.empty();

    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants