// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The interop command is the client and server used by QUIC interoperability tests.
//
// https://github.com/marten-seemann/quic-interop-runner
package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net"
	"net/url"
	"os"
	"path/filepath"
	"sync"

	"golang.org/x/net/quic"
	"golang.org/x/net/quic/qlog"
)

var (
	listen  = flag.String("listen", "", "listen address")
	cert    = flag.String("cert", "", "certificate")
	pkey    = flag.String("key", "", "private key")
	root    = flag.String("root", "", "serve files from this root")
	output  = flag.String("output", "", "directory to write files to")
	qlogdir = flag.String("qlog", "", "directory to write qlog output to")
)

func main() {
	ctx := context.Background()
	flag.Parse()
	urls := flag.Args()

	config := &quic.Config{
		TLSConfig: &tls.Config{
			InsecureSkipVerify: true,
			MinVersion:         tls.VersionTLS13,
			NextProtos:         []string{"hq-interop"},
		},
		MaxBidiRemoteStreams: -1,
		MaxUniRemoteStreams:  -1,
		QLogLogger: slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{
			Level: quic.QLogLevelFrame,
			Dir:   *qlogdir,
		})),
	}
	if *cert != "" {
		c, err := tls.LoadX509KeyPair(*cert, *pkey)
		if err != nil {
			log.Fatal(err)
		}
		config.TLSConfig.Certificates = []tls.Certificate{c}
	}
	if *root != "" {
		config.MaxBidiRemoteStreams = 100
	}
	if keylog := os.Getenv("SSLKEYLOGFILE"); keylog != "" {
		f, err := os.Create(keylog)
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		config.TLSConfig.KeyLogWriter = f
	}

	testcase := os.Getenv("TESTCASE")
	switch testcase {
	case "handshake", "keyupdate":
		basicTest(ctx, config, urls)
		return
	case "chacha20":
		// "[...] offer only ChaCha20 as a ciphersuite."
		//
		// crypto/tls does not support configuring TLS 1.3 ciphersuites,
		// so we can't support this test on the client.
		if *listen != "" && len(urls) == 0 {
			config.TLSConfig.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
				if len(hello.CipherSuites) == 1 && hello.CipherSuites[0] == tls.TLS_CHACHA20_POLY1305_SHA256 {
					return nil, nil
				}
				return nil, fmt.Errorf("this test requires the client to offer only ChaCha20")
			}
			basicTest(ctx, config, urls)
			return
		}
	case "ecn":
		// TODO: We give ECN feedback to the sender, but we don't add our own
		// ECN marks to outgoing packets.
	case "transfer":
		// "The client should use small initial flow control windows
		// for both stream- and connection-level flow control
		// such that the during the transfer of files on the order of 1 MB
		// the flow control window needs to be increased."
		config.MaxStreamReadBufferSize = 64 << 10
		config.MaxConnReadBufferSize = 64 << 10
		basicTest(ctx, config, urls)
		return
	case "http3":
		// TODO
	case "multiconnect":
		// TODO
	case "resumption":
		// TODO
	case "retry":
		if *listen != "" && len(urls) == 0 {
			config.RequireAddressValidation = true
		}
		basicTest(ctx, config, urls)
		return
	case "versionnegotiation":
		// "The client should start a connection using
		// an unsupported version number [...]"
		//
		// We don't support setting the client's version,
		// so only run this test as a server.
		if *listen != "" && len(urls) == 0 {
			basicTest(ctx, config, urls)
			return
		}
	case "v2":
		// We do not support QUIC v2.
	case "zerortt":
		// TODO
	}
	fmt.Printf("unsupported test case %q\n", testcase)
	os.Exit(127)
}

// basicTest runs the standard test setup.
//
// As a server, it serves the contents of the -root directory.
// As a client, it downloads all the provided URLs in parallel,
// making one connection to each destination server.
func basicTest(ctx context.Context, config *quic.Config, urls []string) {
	l, err := quic.Listen("udp", *listen, config)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("listening on %v", l.LocalAddr())

	byAuthority := map[string][]*url.URL{}
	for _, s := range urls {
		u, addr, err := parseURL(s)
		if err != nil {
			log.Fatal(err)
		}
		byAuthority[addr] = append(byAuthority[addr], u)
	}
	var g sync.WaitGroup
	defer g.Wait()
	for addr, u := range byAuthority {
		addr, u := addr, u
		g.Add(1)
		go func() {
			defer g.Done()
			fetchFrom(ctx, config, l, addr, u)
		}()
	}

	if config.MaxBidiRemoteStreams >= 0 {
		serve(ctx, l)
	}
}

func serve(ctx context.Context, l *quic.Endpoint) error {
	for {
		c, err := l.Accept(ctx)
		if err != nil {
			return err
		}
		go serveConn(ctx, c)
	}
}

func serveConn(ctx context.Context, c *quic.Conn) {
	for {
		s, err := c.AcceptStream(ctx)
		if err != nil {
			return
		}
		go func() {
			if err := serveReq(ctx, s); err != nil {
				log.Print("serveReq:", err)
			}
		}()
	}
}

func serveReq(ctx context.Context, s *quic.Stream) error {
	defer s.Close()
	req, err := io.ReadAll(s)
	if err != nil {
		return err
	}
	if !bytes.HasSuffix(req, []byte("\r\n")) {
		return errors.New("invalid request")
	}
	req = bytes.TrimSuffix(req, []byte("\r\n"))
	if !bytes.HasPrefix(req, []byte("GET /")) {
		return errors.New("invalid request")
	}
	req = bytes.TrimPrefix(req, []byte("GET /"))
	if !filepath.IsLocal(string(req)) {
		return errors.New("invalid request")
	}
	f, err := os.Open(filepath.Join(*root, string(req)))
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = io.Copy(s, f)
	return err
}

func parseURL(s string) (u *url.URL, authority string, err error) {
	u, err = url.Parse(s)
	if err != nil {
		return nil, "", err
	}
	host := u.Hostname()
	port := u.Port()
	if port == "" {
		port = "443"
	}
	authority = net.JoinHostPort(host, port)
	return u, authority, nil
}

func fetchFrom(ctx context.Context, config *quic.Config, l *quic.Endpoint, addr string, urls []*url.URL) {
	conn, err := l.Dial(ctx, "udp", addr, config)
	if err != nil {
		log.Printf("%v: %v", addr, err)
		return
	}
	log.Printf("connected to %v", addr)
	defer conn.Close()
	var g sync.WaitGroup
	for _, u := range urls {
		u := u
		g.Add(1)
		go func() {
			defer g.Done()
			if err := fetchOne(ctx, conn, u); err != nil {
				log.Printf("fetch %v: %v", u, err)
			} else {
				log.Printf("fetched %v", u)
			}
		}()
	}
	g.Wait()
}

func fetchOne(ctx context.Context, conn *quic.Conn, u *url.URL) error {
	if len(u.Path) == 0 || u.Path[0] != '/' || !filepath.IsLocal(u.Path[1:]) {
		return errors.New("invalid path")
	}
	file, err := os.Create(filepath.Join(*output, u.Path[1:]))
	if err != nil {
		return err
	}
	s, err := conn.NewStream(ctx)
	if err != nil {
		return err
	}
	defer s.Close()
	if _, err := s.Write([]byte("GET " + u.Path + "\r\n")); err != nil {
		return err
	}
	s.CloseWrite()
	if _, err := io.Copy(file, s); err != nil {
		return err
	}
	return nil
}
