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

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

There is a newer version: 0.9.1
Show newest version
/*
 * 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.QuicClientConnection;
import net.luminis.quic.QuicConnection;
import net.luminis.quic.QuicSessionTicket;
import net.luminis.quic.client.h09.Http09Client;
import net.luminis.quic.core.QuicClientConnectionImpl;
import net.luminis.quic.core.QuicSessionTicketImpl;
import net.luminis.quic.core.VersionNegotiationFailure;
import net.luminis.quic.log.FileLogger;
import net.luminis.quic.log.Logger;
import net.luminis.quic.log.SysOutLogger;
import net.luminis.tls.TlsConstants;
import org.apache.commons.cli.*;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Command line interface for Kwik client.
 */
public class KwikCli {

    private static String DEFAULT_LOG_ARGS = "wip";
    private static Options cmdLineOptions;

    private String newSessionTicketsFilename;
    private boolean useZeroRtt;
    private String serverCertificatesFile;
    private int keepAliveTime;

    public enum HttpVersion {
        HTTP09,
        HTTP3
    }

    private HttpVersion httpVersion;
    private String alpn;
    private Logger logger;


    public void run(String[] rawArgs) throws Exception {
        CommandLine cmd = getCommandLine(rawArgs);

        boolean interactiveMode = cmd.hasOption("i");

        QuicClientConnection.Builder connectionBuilder = createConnectionBuilder(interactiveMode);

        String httpRequestPath = processUrlArgs(cmd, connectionBuilder);

        processCipherArgs(cmd, connectionBuilder);

        if (cmd.hasOption("noCertificateCheck")) {
            connectionBuilder.noServerCertificateCheck();
        }

        if (cmd.hasOption("saveServerCertificates")) {
            serverCertificatesFile = cmd.getOptionValue("saveServerCertificates");
        }

        processLoggerArgs(cmd, connectionBuilder);

        processVersionArgs(cmd, connectionBuilder);

        httpVersion = determineHttpVersion();

        alpn = determineAlpn(cmd);

        processConnectTimeoutArgs(cmd, connectionBuilder);

        processKeepAliveArg(cmd);

        httpRequestPath = extractHttpRequestPath(cmd, connectionBuilder, httpRequestPath);

        useZeroRtt = cmd.hasOption("Z");
        if (useZeroRtt && httpRequestPath == null) {
            throw new IllegalArgumentException("Option --use0RTT requires option --http");
        }

        String outputFile = extractOutputFile(cmd);

        processSecretsArgs(cmd, connectionBuilder);

        processSessionTicketSaveArg(cmd, connectionBuilder);

        boolean useSessionTicket = processSessionTicketArg(cmd, connectionBuilder);

        if (useZeroRtt && !useSessionTicket) {
            throw new IllegalArgumentException("Option --use0RTT requires option --sessionTicket");
        }

        processClientCertificateArgs(cmd, connectionBuilder);

        processQuantumReadinessTestArg(cmd, connectionBuilder);

        processInitialRttArg(cmd, connectionBuilder);

        if (httpVersion == HttpVersion.HTTP3 && useZeroRtt) {
            throw new IllegalArgumentException("Option --use0RTT is not yet supported by this HTTP3 implementation.");
        }

        try {
            if (interactiveMode) {
                new InteractiveShell((QuicClientConnectionImpl.ExtendedBuilder) connectionBuilder, alpn, httpVersion).start();
            }
            else {
                executeRequest(httpRequestPath, outputFile, connectionBuilder);
                Thread.sleep(1000);
            }

            System.out.println("Terminating Kwik");

            if (!interactiveMode && httpRequestPath == null && keepAliveTime == 0) {
                System.out.println("This was quick, huh? Next time, consider using --http09 or --keepAlive argument.");
            }
        }
        catch (IOException e) {
            System.out.println("Got IO error: " + e);
        }
        catch (VersionNegotiationFailure e) {
            System.out.println("Client and server could not agree on a compatible QUIC version.");
        }
    }

    private CommandLine getCommandLine(String[] rawArgs) {
        CommandLine cmd;
        try {
            CommandLineParser parser = new DefaultParser();
            cmd = parser.parse(cmdLineOptions, rawArgs);

            if (cmd.hasOption("v")) {
                System.out.println("Kwik version: " + KwikVersion.getVersion());
                System.exit(0);
            }

            if (cmd.getArgList().isEmpty()) {
                throw new IllegalArgumentException("Missing arguments");
            }

            return cmd;
        }
        catch (ParseException argError) {
            throw new IllegalArgumentException("Invalid argument: " + argError.getMessage());
        }
    }

    private QuicClientConnection.Builder createConnectionBuilder(boolean interactiveMode) {
        if (interactiveMode) {
            return QuicClientConnectionImpl.newExtendedBuilder();
        }
        else {
            return QuicClientConnection.newBuilder();
        }
    }

    private String processUrlArgs(CommandLine cmd, QuicClientConnection.Builder builder) {
        String httpRequestPath = null;
        List args = cmd.getArgList();
        if (args.size() == 1) {
            String arg = args.get(0);
            try {
                if (arg.startsWith("http://") || arg.startsWith("https://")) {
                    try {
                        URL url = new URL(arg);
                        builder.uri(url.toURI());
                        if (!url.getPath().isEmpty()) {
                            httpRequestPath = url.getPath();
                        }
                    } catch (MalformedURLException e) {
                        throw new IllegalArgumentException("Cannot parse URL '" + arg + "'");
                    }
                }
                else if (arg.contains(":")) {
                    builder.uri(new URI("//" + arg));
                }
                else {
                    if (arg.matches("\\d+")) {
                        throw new IllegalArgumentException("Invalid hostname (did you forget to specify an option argument?).");
                    }
                    builder.uri(new URI("//" + arg + ":" + 443));
                }
            }
            catch (URISyntaxException invalidUri) {
                throw new IllegalArgumentException("Cannot parse URI '" + arg + "'");
            }
        }
        else if (args.size() == 2) {
            try {
                builder.uri(new URI("//" + args.get(0) + ":" + args.get(1)));
            }
            catch (URISyntaxException invalidUri) {
                throw new IllegalArgumentException("Cannot parse URI '" + args.stream().collect(Collectors.joining(":")) + "'");
            }
        }
        else if (args.size() > 2) {
            throw new IllegalArgumentException("Too many arguments");
        }
        return httpRequestPath;
    }

    private QuicClientConnection.Builder processCipherArgs(CommandLine cmd, QuicClientConnection.Builder builder) {
        List cipherOpts = List.of("aes128gcm", "aes256gcm", "chacha20");

        // Process cipher options in order, as order has meaning! (preference)
        List




© 2015 - 2024 Weber Informatics LLC | Privacy Policy