001/**
002 *
003 * Copyright 2021-2023 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.websocket.java11;
018
019import java.net.URI;
020import java.net.http.HttpClient;
021import java.net.http.WebSocket;
022import java.nio.ByteBuffer;
023import java.util.concurrent.CompletableFuture;
024import java.util.concurrent.CompletionStage;
025import java.util.concurrent.ExecutionException;
026import java.util.logging.Level;
027
028import javax.net.ssl.SSLSession;
029
030import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
031import org.jivesoftware.smack.util.LazyStringBuilder;
032import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
033import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
034
035public final class Java11WebSocket extends AbstractWebSocket {
036
037    private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().build();
038
039    private WebSocket webSocket;
040
041    enum PingPong {
042        ping,
043        pong,
044    };
045
046    Java11WebSocket(WebSocketRemoteConnectionEndpoint endpoint,
047                    ModularXmppClientToServerConnectionInternal connectionInternal) {
048        super(endpoint, connectionInternal);
049
050        final WebSocket.Listener listener = new WebSocket.Listener() {
051            @Override
052            public void onOpen(WebSocket webSocket) {
053                LOGGER.finer(webSocket + " opened");
054                webSocket.request(1);
055            }
056
057            LazyStringBuilder received = new LazyStringBuilder();
058
059            @Override
060            public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
061                received.append(data);
062                webSocket.request(1);
063
064                if (last) {
065                    String wholeMessage = received.toString();
066                    received = new LazyStringBuilder();
067                    onIncomingWebSocketElement(wholeMessage);
068                }
069
070                return null;
071            }
072
073            @Override
074            public void onError(WebSocket webSocket, Throwable error) {
075                onWebSocketFailure(error);
076            }
077
078            @Override
079            public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
080                LOGGER.finer(webSocket + " closed with status code " + statusCode + ". Provided reason: " + reason);
081                // TODO: What should we do here? What if some server implementation closes the WebSocket out of the
082                // blue? Ideally, we should react on this. Some situation in the okhttp implementation.
083                return null;
084            }
085
086            @Override
087            public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) {
088                logPingPong(PingPong.ping, webSocket, message);
089
090                webSocket.request(1);
091                return null;
092            }
093
094            @Override
095            public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
096                logPingPong(PingPong.pong, webSocket, message);
097
098                webSocket.request(1);
099                return null;
100            }
101
102            private void logPingPong(PingPong pingPong, WebSocket webSocket, ByteBuffer message) {
103                final Level pingPongLogLevel = Level.FINER;
104                if (!LOGGER.isLoggable(pingPongLogLevel)) {
105                    return;
106                }
107
108                LOGGER.log(pingPongLogLevel, "Received " + pingPong + " over " + webSocket + ". Message: " + message);
109            }
110        };
111
112        final URI uri = endpoint.getUri();
113        CompletionStage<WebSocket> webSocketFuture = HTTP_CLIENT.newWebSocketBuilder()
114                        .subprotocols(SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_VALUE_XMPP)
115                        .buildAsync(uri, listener);
116
117        webSocketFuture.whenComplete((webSocket, throwable) -> {
118            if (throwable == null) {
119                this.webSocket = webSocket;
120                future.setResult(this);
121            } else {
122                onWebSocketFailure(throwable);
123            }
124        });
125    }
126
127    @Override
128    protected void send(String element) {
129        CompletableFuture<WebSocket> completableFuture = webSocket.sendText(element, true);
130        try {
131            completableFuture.get();
132        } catch (ExecutionException e) {
133            onWebSocketFailure(e);
134        } catch (InterruptedException e) {
135            // This thread should never be interrupted, as it is a Smack internal thread.
136            throw new AssertionError(e);
137        }
138    }
139
140    @Override
141    public void disconnect(int code, String message) {
142        try {
143            if (!webSocket.isOutputClosed()) {
144                CompletableFuture<WebSocket> completableFuture = webSocket.sendClose(code, message);
145                completableFuture.get();
146            }
147        } catch (ExecutionException e) {
148            LOGGER.log(Level.WARNING, "Failed to send final close when disconnecting " + this, e);
149        } catch (InterruptedException e) {
150            // This thread should never be interrupted, as it is a Smack internal thread.
151            throw new AssertionError(e);
152        } finally {
153            webSocket.abort();
154        }
155    }
156
157    @Override
158    public SSLSession getSSLSession() {
159        return null;
160    }
161
162    private void onWebSocketFailure(ExecutionException executionException) {
163        Throwable cause = executionException.getCause();
164        onWebSocketFailure(cause);
165    }
166}