All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.auties.whatsapp.implementation.SocketSession Maven / Gradle / Ivy

package it.auties.whatsapp.implementation;

import it.auties.whatsapp.net.SocketClient;
import it.auties.whatsapp.net.WebSocketClient;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;

public abstract sealed class SocketSession permits SocketSession.WebSocketSession, SocketSession.RawSocketSession {
    private static final String WEB_SOCKET_HOST = "web.whatsapp.com";
    private static final int WEB_SOCKET_PORT = 443;
    private static final String WEB_SOCKET_PATH = "/ws/chat";
    private static final String MOBILE_SOCKET_HOST = "g.whatsapp.net";
    private static final int MOBILE_SOCKET_PORT = 443;
    private static final int MESSAGE_LENGTH = 3;

    final URI proxy;
    SocketListener listener;

    private SocketSession(URI proxy) {
        this.proxy = proxy;
    }

    CompletableFuture connect(SocketListener listener) {
        this.listener = listener;
        return CompletableFuture.completedFuture(null);
    }

    abstract void disconnect();

    public abstract CompletableFuture sendBinary(byte[] bytes);

    static SocketSession of(URI proxy, boolean webSocket) {
        if (webSocket) {
            return new WebSocketSession(proxy);
        }

        return new RawSocketSession(proxy);
    }

    public static final class WebSocketSession extends SocketSession implements WebSocketClient.Listener {
        private WebSocketClient webSocket;
        private final MessageOutputStream messageOutputStream;

        WebSocketSession(URI proxy) {
            super(proxy);
            this.messageOutputStream = new MessageOutputStream();
        }

        @Override
        CompletableFuture connect(SocketListener listener) {
            if (webSocket != null) {
                return CompletableFuture.completedFuture(null);
            }

            super.connect(listener);
            try {
                var sslContext = SSLContext.getInstance("TLSv1.3");
                sslContext.init(null, null, null);
                var sslEngine = sslContext.createSSLEngine(WEB_SOCKET_HOST, WEB_SOCKET_PORT);
                sslEngine.setUseClientMode(true);
                this.webSocket = WebSocketClient.newSecureClient(sslEngine, proxy, this);
                var endpoint = proxy != null ? InetSocketAddress.createUnresolved(WEB_SOCKET_HOST, WEB_SOCKET_PORT) : new InetSocketAddress(WEB_SOCKET_HOST, WEB_SOCKET_PORT);
                return webSocket.connectAsync(endpoint, WEB_SOCKET_PATH)
                        .thenRunAsync(() -> listener.onOpen(this));
            }catch (Throwable throwable) {
                return CompletableFuture.failedFuture(throwable);
            }
        }

        @Override
        void disconnect() {
            if (webSocket == null) {
                return;
            }

            webSocket.close();
        }

        @Override
        public CompletableFuture sendBinary(byte[] bytes) {
            if (webSocket == null) {
                return CompletableFuture.completedFuture(null);
            }

            return webSocket.sendBinary(ByteBuffer.wrap(bytes));
        }

        @Override
        public void onClose(int statusCode, String reason) {
            listener.onClose();
            this.webSocket = null;
        }

        @Override
        public void onMessage(ByteBuffer data, boolean last) {
            try {
                while (data.hasRemaining()) {
                    if(messageOutputStream.isReady()) {
                        if(data.remaining() < MESSAGE_LENGTH) {
                            break;
                        }

                        var messageLength = (data.get() << 16) | Short.toUnsignedInt(data.getShort());
                        if (messageLength < 0) {
                            disconnect();
                            return;
                        }

                        messageOutputStream.init(messageLength);
                    }

                    var result = messageOutputStream.write(data);
                    if(result != null) {
                        listener.onMessage(result, messageOutputStream.length());
                    }
                }
            } catch (Throwable throwable) {
                listener.onError(throwable);
            }
        }

        private final static class MessageOutputStream {
            private byte[] buffer;
            private int position;
            private int limit;
            public void init(int limit) {
                if(buffer == null || buffer.length < limit) {
                    this.buffer = new byte[limit];
                }

                this.limit = limit;
            }

            public byte[] write(ByteBuffer input) {
                if(!input.hasRemaining()) {
                    return null;
                }

                var remaining = Math.min(limit - position, input.remaining());
                input.get(buffer, position, remaining);
                position += remaining;
                if (position != limit) {
                    return null;
                }

                position = 0;
                return buffer;
            }

            public boolean isReady() {
                return position == 0;
            }

            public int length() {
                return limit;
            }
        }
    }

    static final class RawSocketSession extends SocketSession {
        private volatile SocketClient socket;

        RawSocketSession(URI proxy) {
            super(proxy);
        }

        @Override
        CompletableFuture connect(SocketListener listener) {
            if (isOpen()) {
                return CompletableFuture.completedFuture(null);
            }

            super.connect(listener);
            try {
                this.socket = SocketClient.newPlainClient(proxy);
                var address = proxy != null ? InetSocketAddress.createUnresolved(MOBILE_SOCKET_HOST, MOBILE_SOCKET_PORT) : new InetSocketAddress(MOBILE_SOCKET_HOST, MOBILE_SOCKET_PORT);
                return socket.connectAsync(address).thenRunAsync(() -> {
                    listener.onOpen(RawSocketSession.this);
                    notifyNextMessage();
                });
            }catch (IOException exception) {
                return CompletableFuture.failedFuture(exception);
            }
        }

        private void notifyNextMessage() {
            if(socket == null) {
                return;
            }

            socket.readFullyAsync(
                    MESSAGE_LENGTH,
                    this::readNextMessageLength
            );
        }

        private void readNextMessageLength(ByteBuffer lengthBuffer, Throwable error) {
            if(error != null) {
                disconnect();
                return;
            }

            var messageLength = (lengthBuffer.get() << 16) | ((lengthBuffer.get() & 0xFF) << 8) | (lengthBuffer.get() & 0xFF);
            if(messageLength < 0) {
                disconnect();
                return;
            }

            socket.readFullyAsync(
                    messageLength,
                    this::notifyNextMessage
            );
        }

        private void notifyNextMessage(ByteBuffer messageBuffer, Throwable error) {
            if(error != null) {
                disconnect();
                return;
            }

            try {
                var message = messageBuffer.array();
                listener.onMessage(message, message.length);
            }catch (Throwable throwable) {
                listener.onError(throwable);
            }finally {
                notifyNextMessage();
            }
        }

        @Override
        void disconnect() {
            try {
                if (socket == null) {
                    return;
                }

                listener.onClose();
                socket.close();
                this.socket = null;
            } catch (Throwable ignored) {
                // No need to handle this
            }
        }

        private boolean isOpen() {
            return socket != null && socket.isConnected();
        }

        @Override
        public CompletableFuture sendBinary(byte[] bytes) {
            if (socket == null) {
                return CompletableFuture.completedFuture(null);
            }

            return socket.writeAsync(bytes);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy