// 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" "crypto/tls" "fmt" "io" "log" "net" "os" "strings" "time" "github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License "github.com/mattn/go-xmpp" // BSD-3-Clause "github.com/pborman/getopt/v2" // BSD-3-Clause ) type configuration struct { username string jserver string port string password string resource string alias string } func readMessage(messageFilePath string) (string, error) { var ( output string err error ) // Check that message file is existing. _, err = os.Stat(messageFilePath) if os.IsNotExist(err) { return output, err } // Open message file. file, err := os.Open(messageFilePath) if err != nil { return output, err } 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 "", err } } file.Close() return output, err } func main() { type recipientsType struct { Jid string OxKeyRing *crypto.KeyRing } var ( err error message, user, server, password, resource, alias string oxPrivKey *crypto.Key recipients []recipientsType ) // 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.") flagResource := getopt.StringLong("resource", 'r', "", "Set resource. "+ "When sending to a chatroom this is used as 'alias'. DEPRECATED: Use --alias instead.") 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, 10, "Connection timeout in seconds.") flagTLSMinVersion := getopt.IntLong("tls-version", 0, 12, "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.") // 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) 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.") } // 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 resource = config.resource 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 } switch { // Use the resource as alias if no alias is specified otherwise but a resource is specified. // Backward compat, will be removed when resource is removed. case alias == "" && *flagAlias == "" && *flagResource != "": alias = *flagResource // Use the resource as alias if no alias is specified otherwise but a resource is specified. // Backward compat, will be removed when resource is removed. case alias == "" && *flagAlias == "" && resource != "": alias = resource // 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 } // Overwrite resource if specified via command line flag. if *flagResource != "" { fmt.Println("Deprecated flag: --resource.") resource = *flagResource } else if resource == "" { // Use "go-sendxmpp" plus a random string if no other resource is specified. resource = "go-sendxmpp." + getShortID() } // Timeout timeout := time.Duration(*flagTimeout * 1000000000) // Use ALPN var tlsConfig tls.Config tlsConfig.ServerName = user[strings.Index(user, "@")+1:] tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client") tlsConfig.InsecureSkipVerify = *flagSkipVerify switch *flagTLSMinVersion { case 10: tlsConfig.MinVersion = tls.VersionTLS10 case 11: tlsConfig.MinVersion = tls.VersionTLS11 case 12: tlsConfig.MinVersion = tls.VersionTLS12 case 13: tlsConfig.MinVersion = tls.VersionTLS13 default: fmt.Println("Unknown TLS version.") os.Exit(0) } // 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/mattn/go-xmpp#Options NoTLS: !*flagDirectTLS, StartTLS: !*flagDirectTLS, Debug: *flagDebug, TLSConfig: &tlsConfig, } // 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 { log.Fatal(err) } iqc := make(chan xmpp.IQ, 100) msgc := make(chan xmpp.Chat, 100) go rcvStanzas(client, 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 { log.Fatal(err) } recipients[i].Jid = validatedJid } switch { case *flagOxGenPrivKeyX25519: validatedOwnJid, err := MarshalJID(user) if err != nil { log.Fatal(err) } err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519") if err != nil { log.Fatal(err) } os.Exit(0) case *flagOxGenPrivKeyRSA: validatedOwnJid, err := MarshalJID(user) if err != nil { log.Fatal(err) } err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa") if err != nil { log.Fatal(err) } os.Exit(0) case *flagOxImportPrivKey != "": validatedOwnJid, err := MarshalJID(user) if err != nil { log.Fatal(err) } err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey, client, iqc) if err != nil { log.Fatal(err) } os.Exit(0) case *flagOxDeleteNodes: validatedOwnJid, err := MarshalJID(user) if err != nil { log.Fatal(err) } err = oxDeleteNodes(validatedOwnJid, client, iqc) if err != nil { log.Fatal(err) } os.Exit(0) case *flagOx: validatedOwnJid, err := MarshalJID(user) if err != nil { log.Fatal(err) } oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase) if err != nil { log.Fatal(err) } } if *flagHTTPUpload != "" { message = httpUpload(client, iqc, tlsConfig.ServerName, *flagHTTPUpload) } if *flagOOBFile != "" { // Remove invalid UTF8 code points. message = validUTF8(*flagOOBFile) // Check if the URI is valid. uri, err := validURI(message) if err != nil { log.Fatal(err) } message = uri.String() } var msgType string msgType = "chat" if *flagChatroom { msgType = "groupchat" // 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 { log.Fatal(err) } } } switch { case *flagRaw: if message == "" { break } // Send raw XML _, err = client.SendOrg(message + "\n") if err != nil { log.Fatal(err) } case *flagInteractive: // Send in endless loop (for usage with e.g. "tail -f"). var reader = bufio.NewReader(os.Stdin) for { message, err = reader.ReadString('\n') if err != nil { log.Fatal("failed to read from stdin") } // 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, iqc, oxPrivKey, recipient.Jid, recipient.OxKeyRing, message) if err != nil { fmt.Println("Ox: couldn't encrypt to", recipient.Jid) continue } _, err = client.SendOrg(oxMessage + "\n") if err != nil { log.Fatal(err) } default: _, err = client.Send(xmpp.Chat{Remote: recipient.Jid, Type: msgType, Text: message}) if err != nil { log.Fatal(err) } } } } case *flagListen: tz := time.Now().Location() 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 "chat": bareFrom = strings.Split(v.Remote, "/")[0] case "groupchat": 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 "chat": bareFrom = strings.Split(v.Remote, "/")[0] case "groupchat": 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}) 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 != "": _, err = client.SendOrg("" + message + "" + message + "") if err != nil { fmt.Println("Couldn't send message to", recipient.Jid) } case *flagOx: if recipient.OxKeyRing == nil { continue } oxMessage, err := oxEncrypt(client, iqc, oxPrivKey, recipient.Jid, recipient.OxKeyRing, message) if err != nil { fmt.Println("Ox: couldn't encrypt to", recipient.Jid) continue } _, err = client.SendOrg(oxMessage + "\n") if err != nil { log.Fatal(err) } default: _, err = client.Send(xmpp.Chat{Remote: recipient.Jid, Type: msgType, Text: message}) if err != nil { log.Fatal(err) } } } } // Wait for a short time as some messages are not delievered by the server // if the connection is closed immediately after sending a message. time.Sleep(100 * time.Millisecond) }