go-sendxmpp/main.go
Martin Dosch 9f9c533941
Add support for SASL2 and BIND2
Squashed commit of the following:

commit 0805b1f06a
Author: Martin Dosch <martin@mdosch.de>
Date:   Tue Apr 9 10:57:29 2024 +0200

    Move to upstream go-xmpp.

commit 557d105238
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 12:45:10 2024 +0200

    Do not use sasl2 uuid attribute.

commit 986aea7957
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 11:29:33 2024 +0200

    Don't print error on io.EOF

commit ef927ce5cc
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 11:25:40 2024 +0200

    Detect stream error while receiving stanzas.

commit 73b00f0612
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 10:00:54 2024 +0200

    Use a client uuid per JID.

commit b2d090a623
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 09:29:50 2024 +0200

    Improve sasl2 user agent id stuff.

commit c7376832ce
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 09:11:03 2024 +0200

    Rework getting data path.

commit 080100486e
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 08:49:49 2024 +0200

    Update go-xmpp.

commit 9c56a79bae
Author: Martin Dosch <martin@mdosch.de>
Date:   Sun Apr 7 00:26:04 2024 +0200

    SASL2: Create per client installation ID.

commit cce36b070a
Author: Martin Dosch <martin@mdosch.de>
Date:   Sat Apr 6 22:20:35 2024 +0200

    Update go-xmpp.

commit 1c4acfd07c
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 12:04:09 2024 +0200

    Update go-xmpp.

commit 0703a7c2d6
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 11:38:28 2024 +0200

    Update go-xmpp.

commit 4f033fc5b9
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 09:39:30 2024 +0200

    Update vendored lib.

commit a44554218d
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 09:32:54 2024 +0200

    Use google/uuid for message IDs.

    It is used for go-xmpp in the sasl2 branch anyway.

commit 3e57ec3603
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 09:12:50 2024 +0200

    Update vendored modules.

commit ead44ef99d
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 09:12:25 2024 +0200

    Add . between go-sendxmpp and short id.

commit b3271a3530
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 08:38:03 2024 +0200

    Update Changelog.

commit 639b9b6657
Author: Martin Dosch <martin@mdosch.de>
Date:   Fri Apr 5 08:22:56 2024 +0200

    Add short ID back to resource string.

    That's still necessary to avoid two instances with the same resource
    when using SASL instead of SASL2.

commit 680593359b
Author: Martin Dosch <martin@mdosch.de>
Date:   Thu Apr 4 23:56:15 2024 +0200

    Experimental sasl2 support.
2024-04-09 10:59:00 +02:00

662 lines
19 KiB
Go

// 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"
"errors"
"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)
}
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)
}
}
err = file.Close()
if err != nil {
fmt.Println("error while closing file:", 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
)
// 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.")
// 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
// 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)
}
// Set XMPP connection options.
options := xmpp.Options{
Host: server,
User: user,
DialTimeout: timeout,
Resource: "go-sendxmpp." + getShortID(),
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: "go-sendxmpp",
}
// 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, defaultBufferSize)
msgc := make(chan xmpp.Chat, defaultBufferSize)
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 {
closeAndExit(client, err)
}
recipients[i].Jid = validatedJid
}
switch {
case *flagOxGenPrivKeyX25519:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
if err != nil {
closeAndExit(client, err)
}
os.Exit(0)
case *flagOxGenPrivKeyRSA:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
if err != nil {
closeAndExit(client, err)
}
os.Exit(0)
case *flagOxImportPrivKey != "":
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, err)
}
err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
client, iqc)
if err != nil {
closeAndExit(client, err)
}
os.Exit(0)
case *flagOxDeleteNodes:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, err)
}
err = oxDeleteNodes(validatedOwnJid, client, iqc)
if err != nil {
closeAndExit(client, err)
}
os.Exit(0)
case *flagOx:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, err)
}
oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
if err != nil {
closeAndExit(client, err)
}
}
if *flagHTTPUpload != "" {
message, err = httpUpload(client, iqc, tlsConfig.ServerName,
*flagHTTPUpload, timeout)
if err != nil {
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 {
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 {
closeAndExit(client, err)
}
}
}
switch {
case *flagRaw:
if message == "" {
break
}
// Send raw XML
_, err = client.SendOrg(message)
if err != nil {
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 {
closeAndExit(client, nil)
}
}()
for {
message, err = reader.ReadString('\n')
if err != nil {
closeAndExit(client, errors.New("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)
if err != nil {
fmt.Println("Ox: couldn't encrypt to",
recipient.Jid)
continue
}
_, err = client.SendOrg(oxMessage)
if err != nil {
closeAndExit(client, err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
})
if err != nil {
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 {
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,
})
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 to='" + recipient.Jid + "' type='" +
msgType + "'><body>" + message + "</body><x xmlns='jabber:x:oob'><url>" +
message + "</url></x></message>")
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)
if err != nil {
fmt.Println("Ox: couldn't encrypt to", recipient.Jid)
continue
}
_, err = client.SendOrg(oxMessage)
if err != nil {
closeAndExit(client, err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
})
if err != nil {
closeAndExit(client, err)
}
}
}
}
closeAndExit(client, nil)
}