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

net.luminis.quic.run.InteractiveShell Maven / Gradle / Ivy

/*
 * Copyright © 2019, 2020, 2021, 2022, 2023 Peter Doornbosch
 *
 * This file is part of Kwik, an implementation of the QUIC protocol in Java.
 *
 * Kwik is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Kwik is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see .
 */
package net.luminis.quic.run;

import net.luminis.quic.core.TransportParameters;
import net.luminis.quic.cid.ConnectionIdStatus;
import net.luminis.quic.core.QuicClientConnectionImpl;
import net.luminis.quic.receive.Receiver;
import net.luminis.tls.util.ByteUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;


public class InteractiveShell {

    private final Map> commands;
    private boolean running;
    private Map history;
    private final QuicClientConnectionImpl.ExtendedBuilder builder;
    private QuicClientConnectionImpl quicConnection;
    private ClientParameters params;
    private KwikCli.HttpVersion httpVersion;
    private HttpClient httpClient;
    private CompletableFuture> currentHttpGetResult;

    public InteractiveShell(QuicClientConnectionImpl.ExtendedBuilder builder, String alpn, KwikCli.HttpVersion httpVersion) {
        Objects.requireNonNull(builder);
        Objects.requireNonNull(alpn);
        this.builder = builder;
        builder.applicationProtocol(alpn);
        this.httpVersion = httpVersion;

        commands = new LinkedHashMap<>();
        history = new LinkedHashMap<>();
        setupCommands();

        params = new ClientParameters();
    }

    private void setupCommands() {
        commands.put("help", this::help);
        commands.put("set", this::setClientParameter);
        commands.put("scid_length", this::setScidLength);
        commands.put("connect", this::connect);
        commands.put("close", this::close);
        commands.put("get", this::httpGet);
        commands.put("stop", this::httpStop);
        commands.put("ping", this::sendPing);
        commands.put("params", this::printClientParams);
        commands.put("server_params", this::printServerParams);
        commands.put("cid_new", this::newConnectionIds);
        commands.put("cid_next", this::nextDestinationConnectionId);
        commands.put("cid_list", this::printConnectionIds);
        commands.put("cid_retire", this::retireConnectionId);
        commands.put("udp_rebind", this::changeUdpPort);
        commands.put("update_keys", this::updateKeys);
        commands.put("statistics", this::printStatistics);
        commands.put("!!", this::repeatLastCommand);
        commands.put("quit", this::quit);
    }

    private void repeatLastCommand(String arg) {
        if (history.size() > 0) {
            Map.Entry lastCommand = history.entrySet().stream().reduce((first, second) -> second).orElse(null);
            commands.get(lastCommand.getKey()).accept(lastCommand.getValue());
        }
    }

    public void start() {

        BufferedReader in = new BufferedReader(new InputStreamReader((System.in)));
        try {
            System.out.println("\nThis is the KWIK interactive shell. Type a command or 'help'.");
            prompt();

            running = true;
            while (running) {
                String cmdLine = in.readLine();
                if (! cmdLine.isBlank()) {
                    String cmd = cmdLine.split(" ")[0];
                    List matchingCommands = commands.keySet().stream().filter(command -> command.startsWith(cmd)).collect(Collectors.toList());
                    if (matchingCommands.size() == 1) {
                        String matchingCommand = matchingCommands.get(0);
                        Consumer commandFunction = commands.get(matchingCommand);
                        try {
                            String commandArgs = cmdLine.substring(cmd.length()).trim();
                            commandFunction.accept(commandArgs);
                            if (!matchingCommand.startsWith("!")) {
                                history.put(matchingCommand, commandArgs);
                            }
                        } catch (Exception error) {
                            error(error);
                        }
                    } else {
                        unknown(cmd);
                    }
                }
                if (running) {
                    prompt();
                }
            }
        } catch (IOException e) {
            System.out.println("Error: " + e);
        }
    }

    private void connect(String arg) {
        int connectionTimeout = 3000;
        if (arg != null && !arg.isBlank()) {
            try {
                connectionTimeout = Integer.parseInt(arg);
                if (connectionTimeout < 100) {
                    System.out.println("Connection timeout must be at least 100 ms");
                    return;
                }
            } catch (NumberFormatException notANumber) {
                System.out.println("Connection timeout argument must be an integer value");
                return;
            }
        }

        try {
            builder.connectTimeout(Duration.ofMillis(connectionTimeout));
            quicConnection = (QuicClientConnectionImpl) builder.build();
            quicConnection.connect();
            System.out.println("Ok, connected to " + quicConnection.getUri() + "\n");
        } catch (IOException e) {
            System.out.println("\nError: " + e);
        }
    }

    private void close(String arg) {
        if (quicConnection != null) {
            quicConnection.close();
        }
    }

    private void httpGet(String arg) {
        if (quicConnection == null) {
            System.out.println("Error: no connected");
            return;
        }

        try {
            if (httpClient == null) {
                httpClient = KwikCli.createHttpClient(httpVersion, quicConnection, false);
            }
            InetSocketAddress serverAddress = quicConnection.getServerAddress();
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(new URI("https", null, serverAddress.getHostName(), serverAddress.getPort(), arg, null, null))
                    .build();

            final Instant start = Instant.now();
            CompletableFuture> sendResult = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofFile(createNewFile(arg).toPath()));
            sendResult.thenAccept(response -> {
                Instant done = Instant.now();
                Duration duration = Duration.between(start, done);
                String speed;
                try {
                    long size = Files.size(response.body());
                    speed = String.format("%.2f", ((float) size) / duration.toMillis() / 1000);
                }
                catch (IOException e) {
                    speed = "?";
                }
                System.out.println(String.format("Get requested finished in %.2f sec (%s MB/s) : %s", ((float) duration.toMillis())/1000, speed, response));
            });
            currentHttpGetResult = sendResult;
        }
        catch (IOException | URISyntaxException e) {
            System.out.println("Error: " + e);
        }
    }

    private void httpStop(String arg) {
        currentHttpGetResult.cancel(true);
    }

    private File createNewFile(String baseName) throws IOException {
        if (baseName.startsWith("/") && baseName.length() > 1) {
            baseName = baseName.substring(1);
        }
        baseName = baseName.replace('/', '_');
        File file = new File(baseName + ".dat");
        if (! file.exists()) {
            return file;
        }
        for (int i = 0; i < 1000; i++) {
            file = new File(baseName + i + ".dat");
            if (! file.exists()) {
                return file;
            }
        }
        throw new IOException("Cannot create output file '" + baseName + ".dat" + "'");
    }

    private void newConnectionIds(String args) {
        int newConnectionIdCount = 1;
        int retirePriorTo = 0;  // i.e. no retirement.

        if (!args.isEmpty()) {
            try {
                Object[] intArgs = Stream.of(args.split(" +")).map(arg -> Integer.parseInt(arg)).toArray();
                newConnectionIdCount = (int) intArgs[0];
                if (intArgs.length > 1) {
                    retirePriorTo = (int) intArgs[1];
                }
            } catch (NumberFormatException notANumber) {
                System.out.println("Expected arguments: [] []");
                return;
            }
        }

        byte[][] newConnectionIds = quicConnection.newConnectionIds(newConnectionIdCount, retirePriorTo);
        System.out.println("Generated new (source) connection id's: " +
                Arrays.stream(newConnectionIds)
                        .map(cid -> ByteUtils.bytesToHex(cid))
                        .collect(Collectors.joining(", ")));
    }

    private void printConnectionIds(String arg) {
        System.out.println("Source (client) connection id's:");
        quicConnection.getSourceConnectionIds().entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> System.out.println(toString(entry.getValue().getConnectionIdStatus()) + " " +
                        entry.getKey() + ": " + ByteUtils.bytesToHex(entry.getValue().getConnectionId())));
        System.out.println("Destination (server) connection id's:");
        quicConnection.getDestinationConnectionIds().entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> System.out.println(toString(entry.getValue().getConnectionIdStatus()) + " " +
                        entry.getKey() + ": " + ByteUtils.bytesToHex(entry.getValue().getConnectionId())));
    }

    private String toString(ConnectionIdStatus connectionIdStatus) {
        switch (connectionIdStatus) {
            case NEW: return " ";
            case IN_USE: return "*";
            case USED: return ".";
            case RETIRED: return "x";
            default:
                // Impossible
                throw new RuntimeException("");
        }
    }

    private void nextDestinationConnectionId(String arg) {
        byte[] newConnectionId = quicConnection.nextDestinationConnectionId();
        if (newConnectionId != null) {
            System.out.println("Switched to next destination connection id: " + ByteUtils.bytesToHex(newConnectionId));
        }
        else {
            System.out.println("Cannot switch to next destination connect id, because there is none available");
        }
    }

    private void retireConnectionId(String arg) {
        quicConnection.retireDestinationConnectionId(toInt(arg));
    }

    private void changeUdpPort(String args) {
        quicConnection.changeAddress();
    }

    private void help(String arg) {
        System.out.println("available commands: " + commands.keySet().stream().collect(Collectors.joining(", ")));
    }

    private void quit(String arg) {
        running = false;
    }

    private void unknown(String cmd) {
        System.out.println("unknown command: " + cmd);
    }

    private void sendPing(String arg) {
        quicConnection.ping();
    }

    private void printClientParams(String arg) {
        System.out.print("Client transport parameters: \n" + params + "\n");
    }

    private void setClientParameter(String argLine) {
        String[] args = argLine.split("\\s+");
        if (args.length == 2) {
            String name = args[0];
            String value = args[1];
            setClientParameter(name, value);
        } else {
            System.out.println("Incorrect parameters; should be  .");
            System.out.println("Supported parameters: ");
            printSupportedParameters();
        }
    }

    private void printSupportedParameters() {
        System.out.println("- idle (max idle timeout in seconds)");
        System.out.println("- cids (active connection id limit)");
        System.out.println("- maxstreamdata (receive buffer size)");
        System.out.println("- maxuni (max peer initiated unidirectional streams)");
        System.out.println("- maxbidi (max peer initiated bidirectional streams)");
        System.out.println("- payload (max udp payload)");
    }

    private void setClientParameter(String name, String value) {
        switch (name) {
            case "idle":
                builder.maxIdleTimeout(Duration.ofSeconds(toInt(value)));
                params.maxIdleTimeout = toInt(value);
                break;
            case "cids":
                builder.activeConnectionIdLimit(toInt(value));
                params.activeConnectionIdLimit = toInt(value);
                break;
            case "maxStreamData":
            case "maxstreamdata":
                builder.defaultStreamReceiveBufferSize(toLong(value));
                params.defaultStreamReceiveBufferSize = toLong(value);
                break;
            case "maxuni":
            case "maxUni":
                builder.maxOpenPeerInitiatedUnidirectionalStreams(toInt(value));
                params.maxOpenPeerInitiatedUnidirectionalStreams = toInt(value);
                break;
            case "maxbidi":
            case "maxBidi":
                builder.maxOpenPeerInitiatedBidirectionalStreams(toInt(value));
                params.maxOpenPeerInitiatedBidirectionalStreams = toInt(value);
                break;
            case "payload":
                builder.maxUdpPayloadSize(toInt(value));
                params.maxUdpPayloadSize = toInt(value);
                if (toInt(value) > Receiver.MAX_DATAGRAM_SIZE) {
                    System.out.println(String.format("Warning: client will read at most %d datagram bytes", Receiver.MAX_DATAGRAM_SIZE));
                }
                break;
            default:
                System.out.println("Parameter must be one of:");
                printSupportedParameters();
        }
    }

    private void printServerParams(String arg) {
        if (quicConnection != null) {
            TransportParameters parameters = quicConnection.getPeerTransportParameters();
            System.out.println("Server transport parameters: " + parameters);
        }
        else {
            System.out.println("Server transport parameters still unknown (no connection)");
        }
    }

    private void printStatistics(String arg) {
        if (quicConnection != null) {
            System.out.println(quicConnection.getStats());
        }
    }

    private void setScidLength(String arg) {
        builder.connectionIdLength(toInt(arg));
    }

    private void updateKeys(String arg) {
        quicConnection.updateKeys();
        quicConnection.ping();
    }

    private void error(Exception error) {
        System.out.println("error: " + error);
        error.printStackTrace();
    }

    private void prompt() {
        System.out.print("> ");
        System.out.flush();
    }

    private Integer toInt(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            System.out.println("Error: value not an integer; using 0");
            return 0;
        }
    }

    private Long toLong(String value) {
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            System.out.println("Error: value not an integer; using 0");
            return 0L;
        }
    }

    private class ClientParameters {

        public Integer maxIdleTimeout = QuicClientConnectionImpl.DEFAULT_MAX_IDLE_TIMEOUT / 1000;
        public Integer activeConnectionIdLimit = QuicClientConnectionImpl.DEFAULT_ACTIVE_CONNECTION_ID_LIMIT;
        public Long defaultStreamReceiveBufferSize = QuicClientConnectionImpl.DEFAULT_MAX_STREAM_DATA;
        public Integer maxOpenPeerInitiatedUnidirectionalStreams = QuicClientConnectionImpl.MAX_OPEN_PEER_INITIATED_UNI_STREAMS;
        public Integer maxOpenPeerInitiatedBidirectionalStreams = QuicClientConnectionImpl.MAX_OPEN_PEER_INITIATED_BIDI_STREAMS;
        public Integer maxUdpPayloadSize = QuicClientConnectionImpl.DEFAULT_MAX_UDP_PAYLOAD_SIZE;

        @Override
        public String toString() {
            return "idle=" + maxIdleTimeout + " (seconds)\ncids=" + activeConnectionIdLimit + "\nmaxStreamData=" + defaultStreamReceiveBufferSize +
                    "\nmaxUni=" + maxOpenPeerInitiatedUnidirectionalStreams + "\nmaxBidi=" + maxOpenPeerInitiatedBidirectionalStreams +
                    "\npayload=" + maxUdpPayloadSize;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy