// 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 ) // 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.Println("ox: error fetching key for", r+":", 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() 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.Println("Ox: couldn't encrypt to", recipient.Jid) 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() 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("%s%s%s", recipient.Jid, msgType, *flagSubject, message, message) } else { msg = fmt.Sprintf("%s%s", 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.Println("Ox: couldn't encrypt to", recipient.Jid) 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() closeAndExit(client, nil) }