// Copyright Martin Dosch.
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.

package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	osUser "os/user"
	"runtime"
	"strings"
	"time"

	"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
	"github.com/pborman/getopt/v2"             // BSD-3-Clause
	"github.com/xmppo/go-xmpp"                 // BSD-3-Clause
)

type configuration struct {
	username string
	jserver  string
	port     string
	password string
	alias    string
}

func closeAndExit(client *xmpp.Client, err error) {
	client.Close()
	if err != nil {
		log.Fatal(err)
	}
	os.Exit(0)
}

func readMessage(messageFilePath string) (string, error) {
	var (
		output string
		err    error
	)

	// Check that message file is existing.
	_, err = os.Stat(messageFilePath)
	if err != nil {
		return output, fmt.Errorf("readMessage: %w", err)
	}

	// Open message file.
	file, err := os.Open(messageFilePath)
	if err != nil {
		return output, fmt.Errorf("readMessage: %w", err)
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)
	for scanner.Scan() {
		if output == "" {
			output = scanner.Text()
		} else {
			output = output + "\n" + scanner.Text()
		}
	}

	if err = scanner.Err(); err != nil {
		if err != io.EOF {
			return "", fmt.Errorf("readMessage: %w", err)
		}
	}

	return output, nil
}

func main() {
	type recipientsType struct {
		Jid       string
		OxKeyRing *crypto.KeyRing
	}

	var (
		err                                    error
		message, user, server, password, alias string
		oxPrivKey                              *crypto.Key
		recipients                             []recipientsType
		fast                                   xmpp.Fast
		// There are some errors that we ignore as we do not want to
		// stop the execution. Failure is used to track those to exit
		// with a non-success return value.
		failure error
	)

	// Define command line flags.
	flagHelp := getopt.BoolLong("help", 0, "Show help.")
	flagHTTPUpload := getopt.StringLong("http-upload", 'h', "", "Send a file via http-upload.")
	flagDebug := getopt.BoolLong("debug", 'd', "Show debugging info.")
	flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.")
	flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.")
	flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.")
	flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.")
	flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
	flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+
		"for chatrooms.")
	flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+
		"~/.config/go-sendxmpp/sendxmpprc)")
	flagMessageFile := getopt.StringLong("message", 'm', "", "Set file including the message.")
	flagInteractive := getopt.BoolLong("interactive", 'i', "Interactive mode (for use with e.g. 'tail -f').")
	flagSkipVerify := getopt.BoolLong("no-tls-verify", 'n',
		"Skip verification of TLS certificates (not recommended).")
	flagRaw := getopt.BoolLong("raw", 0, "Send raw XML.")
	flagListen := getopt.BoolLong("listen", 'l', "Listen for messages and print them to stdout.")
	flagTimeout := getopt.IntLong("timeout", 0, defaultTimeout, "Connection timeout in seconds.")
	flagTLSMinVersion := getopt.IntLong("tls-version", 0, defaultTLSMinVersion,
		"Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).")
	flagVersion := getopt.BoolLong("version", 0, "Show version information.")
	flagMUCPassword := getopt.StringLong("muc-password", 0, "", "Password for password protected MUCs.")
	flagOx := getopt.BoolLong("ox", 0, "Use \"OpenPGP for XMPP\" encryption (experimental).")
	flagOxGenPrivKeyRSA := getopt.BoolLong("ox-genprivkey-rsa", 0,
		"Generate a private OpenPGP key (RSA 4096 bit) for the given JID and publish the "+
			"corresponding public key.")
	flagOxGenPrivKeyX25519 := getopt.BoolLong("ox-genprivkey-x25519", 0,
		"Generate a private OpenPGP key (x25519) for the given JID and publish the "+
			"corresponding public key.")
	flagOxPassphrase := getopt.StringLong("ox-passphrase", 0, "",
		"Passphrase for locking and unlocking the private OpenPGP key.")
	flagOxImportPrivKey := getopt.StringLong("ox-import-privkey", 0, "",
		"Import an existing private OpenPGP key.")
	flagOxDeleteNodes := getopt.BoolLong("ox-delete-nodes", 0, "Delete existing OpenPGP nodes on the server.")
	flagOOBFile := getopt.StringLong("oob-file", 0, "", "URL to send a file as out of band data.")
	flagHeadline := getopt.BoolLong("headline", 0, "Send message as type headline.")
	flagSCRAMPinning := getopt.StringLong("scram-mech-pinning", 0, "", "Enforce the use of a certain SCRAM authentication mechanism.")
	flagSSDPOff := getopt.BoolLong("ssdp-off", 0, "Disable XEP-0474: SASL SCRAM Downgrade Protection.")
	flagSubject := getopt.StringLong("subject", 's', "", "Set message subject.")
	flagFastOff := getopt.BoolLong("fast-off", 0, "Disable XEP-0484: Fast Authentication Streamlining Tokens.")

	// Parse command line flags.
	getopt.Parse()

	switch {
	case *flagHelp:
		// If requested, show help and quit.
		getopt.PrintUsage(os.Stdout)
		os.Exit(0)
	case *flagVersion:
		// If requested, show version and quit.
		fmt.Println("Go-sendxmpp", version)
		system := runtime.GOOS + "/" + runtime.GOARCH
		fmt.Println("System:", system, runtime.Version())
		fmt.Println("License: BSD-2-clause")
		os.Exit(0)
		// Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like
		// groupchat, http-upload or listening.
	case *flagOx && *flagHTTPUpload != "":
		log.Fatal("No Ox support for http-upload available.")
	case *flagOx && *flagChatroom:
		log.Fatal("No Ox support for chat rooms available.")
	case *flagHTTPUpload != "" && *flagInteractive:
		log.Fatal("Interactive mode and http upload can't" +
			" be used at the same time.")
	case *flagHTTPUpload != "" && *flagMessageFile != "":
		log.Fatal("You can't send a message while using" +
			" http upload.")
	case *flagOx && *flagOOBFile != "":
		log.Fatal("No encryption possible for OOB data.")
	case *flagOx && *flagHeadline:
		log.Fatal("No Ox support for headline messages.")
	case *flagHeadline && *flagChatroom:
		log.Fatal("Can't use message type headline for groupchat messages.")
	}

	// Print a warning if go-sendxmpp is run by the user root on non-windows systems.
	if runtime.GOOS != "windows" {
		// Get the current user.
		currUser, err := osUser.Current()
		if err != nil {
			log.Fatal("Failed to get current user: ", err)
		}
		if currUser.Username == "root" {
			fmt.Println("WARNING: It seems you are running go-sendxmpp as root user.\n" +
				"This is is not recommended as go-sendxmpp does not require root " +
				"privileges. Please consider using a less privileged user. For an " +
				"example how to do this with sudo please consult the manpage chapter " +
				"TIPS.")
		}
	}

	switch *flagSCRAMPinning {
	case "", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS",
		"SCRAM-SHA-512", "SCRAM-SHA-512-PLUS":
	default:
		log.Fatal("Unknown SCRAM mechanism: ", *flagSCRAMPinning)
	}

	// Read recipients from command line and quit if none are specified.
	// For listening or sending raw XML it's not required to specify a recipient except
	// when sending raw messages to MUCs (go-sendxmpp will join the MUC automatically).
	recipientsList := getopt.Args()
	if (len(recipientsList) == 0 && !*flagRaw && !*flagListen && !*flagOxGenPrivKeyX25519 &&
		!*flagOxGenPrivKeyRSA && *flagOxImportPrivKey == "") && !*flagOxDeleteNodes ||
		(len(recipientsList) == 0 && *flagChatroom) {
		log.Fatal("No recipient specified.")
	}

	// Read configuration file if user or password is not specified.
	if *flagUser == "" || *flagPassword == "" {
		// Read configuration from file.
		config, err := parseConfig(*flagFile)
		if err != nil {
			log.Fatal("Error parsing ", *flagFile, ": ", err)
		}
		// Set connection options according to config.
		user = config.username
		server = config.jserver
		password = config.password
		alias = config.alias
		if config.port != "" {
			server = net.JoinHostPort(server, fmt.Sprint(config.port))
		}
	}

	// Overwrite user if specified via command line flag.
	if *flagUser != "" {
		user = *flagUser
	}

	// Overwrite server if specified via command line flag.
	if *flagServer != "" {
		server = *flagServer
	}

	// Overwrite password if specified via command line flag.
	if *flagPassword != "" {
		password = *flagPassword
	}
	// If no server part is specified in the username but a server is specified
	// just assume the server is identical to the server part and hope for the
	// best. This is for compatibility with the old perl sendxmpp config files.
	var serverpart string
	if !strings.Contains(user, "@") && server != "" {
		// Remove port if server contains it.
		if strings.Contains(server, ":") {
			serverpart, _, err = net.SplitHostPort(server)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			serverpart = server
		}
		user = user + "@" + serverpart
	}

	switch {
	// Use "go-sendxmpp" if no nick is specified via config or command line flag.
	case alias == "" && *flagAlias == "":
		alias = "go-sendxmpp"
		// Overwrite configured alias if a nick is specified via command line flag.
	case *flagAlias != "":
		alias = *flagAlias
	}

	// Timeout
	timeout := time.Duration(*flagTimeout) * time.Second

	clientID, err := getClientID(user)
	if err != nil {
		fmt.Println(err)
	}

	if !*flagFastOff {
		fast, _ = getFastData(user, password)
		// Reset FAST token and mechanism if expired.
		if time.Now().After(fast.Expiry) {
			fast.Token = ""
			fast.Mechanism = ""
		}
	}

	// Use ALPN
	var tlsConfig tls.Config
	tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
	tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
	tlsConfig.InsecureSkipVerify = *flagSkipVerify
	tlsConfig.Renegotiation = tls.RenegotiateNever
	switch *flagTLSMinVersion {
	case defaultTLS10:
		tlsConfig.MinVersion = tls.VersionTLS10
	case defaultTLS11:
		tlsConfig.MinVersion = tls.VersionTLS11
	case defaultTLS12:
		tlsConfig.MinVersion = tls.VersionTLS12
	case defaultTLS13:
		tlsConfig.MinVersion = tls.VersionTLS13
	default:
		fmt.Println("Unknown TLS version.")
		os.Exit(0)
	}

	resource := "go-sendxmpp." + getShortID()

	// Set XMPP connection options.
	options := xmpp.Options{
		Host:        server,
		User:        user,
		DialTimeout: timeout,
		Resource:    resource,
		Password:    password,
		// NoTLS doesn't mean that no TLS is used at all but that instead
		// of using an encrypted connection to the server (direct TLS)
		// an unencrypted connection is established. As StartTLS is
		// set when NoTLS is set go-sendxmpp won't use unencrypted
		// client-to-server connections.
		// See https://pkg.go.dev/github.com/xmppo/go-xmpp#Options
		NoTLS:         !*flagDirectTLS,
		StartTLS:      !*flagDirectTLS,
		Debug:         *flagDebug,
		TLSConfig:     &tlsConfig,
		Mechanism:     *flagSCRAMPinning,
		SSDP:          !*flagSSDPOff,
		UserAgentSW:   resource,
		UserAgentID:   clientID,
		Fast:          !*flagFastOff,
		FastToken:     fast.Token,
		FastMechanism: fast.Mechanism,
	}

	// Read message from file.
	if *flagMessageFile != "" {
		message, err = readMessage(*flagMessageFile)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
	// Also for listening mode and Ox key handling.
	if !*flagInteractive && !*flagListen && *flagHTTPUpload == "" &&
		!*flagOxDeleteNodes && *flagOxImportPrivKey == "" &&
		!*flagOxGenPrivKeyX25519 && !*flagOxGenPrivKeyRSA && *flagOOBFile == "" &&
		message == "" {
		scanner := bufio.NewScanner(os.Stdin)
		for scanner.Scan() {
			if message == "" {
				message = scanner.Text()
			} else {
				message = message + "\n" + scanner.Text()
			}
		}

		if err := scanner.Err(); err != nil {
			if err != io.EOF {
				log.Fatal(err)
			}
		}
	}

	// Remove invalid code points.
	message = validUTF8(message)
	// Exit if message is empty.
	if message == "" && !*flagInteractive && !*flagListen && !*flagOxGenPrivKeyRSA &&
		!*flagOxGenPrivKeyX25519 && *flagOxImportPrivKey == "" &&
		!*flagOxDeleteNodes && *flagHTTPUpload == "" && *flagOOBFile == "" {
		os.Exit(0)
	}

	// Connect to server.
	client, err := connect(options, *flagDirectTLS)
	if err != nil {
		if fast.Token != "" {
			// Reset FAST token and mechanism if FAST login failed.
			fast.Token = ""
			fast.Mechanism = ""
			fast.Expiry = time.Now()
			err := writeFastData(user, password, fast)
			if err != nil {
				fmt.Println(err)
			}
			options.FastToken = ""
			// Try to connect to server without FAST.
			client, err = connect(options, *flagDirectTLS)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			log.Fatal(err)
		}
	}

	// Update fast token if a new one is received or expiry time is reduced.
	if (client.Fast.Token != "" && client.Fast.Token != fast.Token) ||
		(client.Fast.Expiry.Before(fast.Expiry) && !client.Fast.Expiry.IsZero()) {
		fast.Token = client.Fast.Token
		fast.Mechanism = client.Fast.Mechanism
		fast.Expiry = client.Fast.Expiry
		err := writeFastData(user, password, fast)
		if err != nil {
			fmt.Println(err)
		}
	}
	iqc := make(chan xmpp.IQ, defaultBufferSize)
	msgc := make(chan xmpp.Chat, defaultBufferSize)
	ctx, cancel := context.WithCancel(context.Background())
	go rcvStanzas(client, ctx, iqc, msgc)
	for _, r := range getopt.Args() {
		var re recipientsType
		re.Jid = r
		if *flagOx {
			re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r)
			if err != nil {
				re.OxKeyRing = nil
				fmt.Printf("Ox: error fetching key for %s: %v\n", r, err)
				failure = err
			}
		}
		recipients = append(recipients, re)
	}

	// Check that all recipient JIDs are valid.
	for i, recipient := range recipients {
		validatedJid, err := MarshalJID(recipient.Jid)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		recipients[i].Jid = validatedJid
	}

	switch {
	case *flagOxGenPrivKeyX25519:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxGenPrivKeyRSA:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxImportPrivKey != "":
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
			client, iqc)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxDeleteNodes:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		err = oxDeleteNodes(validatedOwnJid, client, iqc)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOx:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	}

	if *flagHTTPUpload != "" {
		message, err = httpUpload(client, iqc, tlsConfig.ServerName,
			*flagHTTPUpload, timeout)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	}

	if *flagOOBFile != "" {
		// Remove invalid UTF8 code points.
		message = validUTF8(*flagOOBFile)
		// Check if the URI is valid.
		uri, err := validURI(message)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		message = uri.String()
	}

	var msgType string
	if *flagHeadline {
		msgType = strHeadline
	} else {
		msgType = strChat
	}
	if *flagChatroom {
		msgType = strGroupchat
		// Join the MUCs.
		for _, recipient := range recipients {
			if *flagMUCPassword != "" {
				dummyTime := time.Now()
				_, err = client.JoinProtectedMUC(recipient.Jid, alias,
					*flagMUCPassword, xmpp.NoHistory, 0, &dummyTime)
			} else {
				_, err = client.JoinMUCNoHistory(recipient.Jid, alias)
			}
			if err != nil {
				cancel()
				closeAndExit(client, err)
			}
		}
	}
	switch {
	case *flagRaw:
		if message == "" {
			break
		}
		// Send raw XML
		_, err = client.SendOrg(message)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	case *flagInteractive:
		// Send in endless loop (for usage with e.g. "tail -f").
		reader := bufio.NewReader(os.Stdin)
		// Quit if ^C is pressed.
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt)
		go func() {
			for range c {
				cancel()
				if failure != nil {
					closeAndExit(client, failure)
				}
				closeAndExit(client, nil)
			}
		}()
		for {
			message, err = reader.ReadString('\n')
			if err != nil {
				select {
				case <-ctx.Done():
					return
				default:
					if err != nil {
						cancel()
						closeAndExit(client, fmt.Errorf("failed to read from stdin"))
					}
				}
			}
			message = strings.TrimSuffix(message, "\n")

			// Remove invalid code points.
			message = validUTF8(message)
			if message == "" {
				continue
			}
			for _, recipient := range recipients {
				switch {
				case *flagOx:
					if recipient.OxKeyRing == nil {
						continue
					}
					oxMessage, err := oxEncrypt(client, oxPrivKey,
						recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
					if err != nil {
						fmt.Printf("Ox: couldn't encrypt to %s: %v\n",
							recipient.Jid, err)
						failure = err
						continue
					}
					_, err = client.SendOrg(oxMessage)
					if err != nil {
						cancel()
						closeAndExit(client, err)
					}
				default:
					_, err = client.Send(xmpp.Chat{
						Remote: recipient.Jid,
						Type:   msgType, Text: message,
						Subject: *flagSubject,
					})
					if err != nil {
						cancel()
						closeAndExit(client, err)
					}
				}
			}
		}
	case *flagListen:
		tz := time.Now().Location()
		// Quit if ^C is pressed.
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt)
		go func() {
			for range c {
				cancel()
				if failure != nil {
					closeAndExit(client, failure)
				}
				closeAndExit(client, nil)
			}
		}()
		for {
			v := <-msgc
			switch {
			case isOxMsg(v) && *flagOx:
				msg, t, err := oxDecrypt(v, client, iqc, user, oxPrivKey)
				if err != nil {
					log.Println(err)
					continue
				}
				if msg == "" {
					continue
				}
				var bareFrom string
				switch v.Type {
				case strChat:
					bareFrom = strings.Split(v.Remote, "/")[0]
				case strGroupchat:
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), "[OX]",
						bareFrom+":", msg)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								"[OX]", bareFrom+":", msg)
						}
					}
				}
			default:
				var t time.Time
				if v.Text == "" {
					continue
				}
				if v.Stamp.IsZero() {
					t = time.Now()
				} else {
					t = v.Stamp
				}
				var bareFrom string
				switch v.Type {
				case strChat:
					bareFrom = strings.Split(v.Remote, "/")[0]
				case strGroupchat:
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), bareFrom+":", v.Text)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								bareFrom+":", v.Text)
						}
					}
				}
			}
		}
	default:
		for _, recipient := range recipients {
			if message == "" {
				break
			}
			switch {
			case *flagHTTPUpload != "":
				_, err = client.Send(xmpp.Chat{
					Remote: recipient.Jid,
					Type:   msgType, Ooburl: message, Text: message,
					Subject: *flagSubject,
				})
				if err != nil {
					fmt.Println("Couldn't send message to",
						recipient.Jid)
				}
			// (Hopefully) temporary workaround due to go-xmpp choking on URL encoding.
			// Once this is fixed in the lib the http-upload case above can be reused.
			case *flagOOBFile != "":
				var msg string
				if *flagSubject != "" {
					msg = fmt.Sprintf("<message to='%s' type='%s'><subject>%s</subject><body>%s</body><x xmlns='jabber:x:oob'><url>%s</url></x></message>",
						recipient.Jid, msgType, *flagSubject, message, message)
				} else {
					msg = fmt.Sprintf("<message to='%s' type='%s'><body>%s</body><x xmlns='jabber:x:oob'><url>%s</url></x></message>",
						recipient.Jid, msgType, message, message)
				}
				_, err = client.SendOrg(msg)
				if err != nil {
					fmt.Println("Couldn't send message to",
						recipient.Jid)
				}
			case *flagOx:
				if recipient.OxKeyRing == nil {
					continue
				}
				oxMessage, err := oxEncrypt(client, oxPrivKey,
					recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
				if err != nil {
					fmt.Printf("Ox: couldn't encrypt to %s: %v\n",
						recipient.Jid, err)
					failure = err
					continue
				}
				_, err = client.SendOrg(oxMessage)
				if err != nil {
					cancel()
					closeAndExit(client, err)
				}
			default:
				_, err = client.Send(xmpp.Chat{
					Remote: recipient.Jid,
					Type:   msgType, Text: message,
					Subject: *flagSubject,
				})
				if err != nil {
					cancel()
					closeAndExit(client, err)
				}
			}
		}
	}
	cancel()
	if failure != nil {
		closeAndExit(client, failure)
	}
	closeAndExit(client, nil)
}
