You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-sendxmpp/main.go

607 lines
17 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"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"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, cancel context.CancelFunc, err error) {
cancel()
client.Close()
if err != nil {
log.Fatal(err)
}
}
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.")
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').")
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.")
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.")
flagInsecureConnect := getopt.BoolLong("insecure-connection-without-tls", 0, "Connect without any security. DO NOT USE!")
// 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 !*flagInsecureConnect:
fmt.Println("This version of go-sendxmpp is connecting without any encryption. It is only meant for debugging purposes if the server is running on the same machine. DO NOT USE except you know what your doing. Use --insecure-connection-without-tls if you want to use go-sendxmpp without TLS.")
os.Exit(0)
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.")
}
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
// Set XMPP connection options.
options := xmpp.Options{
Host: server,
User: user,
DialTimeout: timeout,
Resource: "go-sendxmpp." + getShortID(),
Password: password,
NoTLS: true,
StartTLS: false,
Debug: *flagDebug,
InsecureAllowUnencryptedAuth: true,
Mechanism: *flagSCRAMPinning,
}
// 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, false)
if err != nil {
log.Fatal(err)
}
iqc := make(chan xmpp.IQ, defaultBufferSize)
msgc := make(chan xmpp.Chat, defaultBufferSize)
ctx, cancel := context.WithCancel(context.Background())
go rcvStanzas(client, iqc, msgc, ctx, cancel)
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, cancel, err)
}
recipients[i].Jid = validatedJid
}
switch {
case *flagOxGenPrivKeyX25519:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, cancel, err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
if err != nil {
closeAndExit(client, cancel, err)
}
os.Exit(0)
case *flagOxGenPrivKeyRSA:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, cancel, err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
if err != nil {
closeAndExit(client, cancel, err)
}
os.Exit(0)
case *flagOxImportPrivKey != "":
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, cancel, err)
}
err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
client, iqc)
if err != nil {
closeAndExit(client, cancel, err)
}
os.Exit(0)
case *flagOxDeleteNodes:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, cancel, err)
}
err = oxDeleteNodes(validatedOwnJid, client, iqc)
if err != nil {
closeAndExit(client, cancel, err)
}
os.Exit(0)
case *flagOx:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
closeAndExit(client, cancel, err)
}
oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
if err != nil {
closeAndExit(client, cancel, err)
}
}
if *flagHTTPUpload != "" {
message, err = httpUpload(client, iqc, server,
*flagHTTPUpload, timeout)
if err != nil {
closeAndExit(client, cancel, 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, cancel, 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, cancel, err)
}
}
}
switch {
case *flagRaw:
if message == "" {
break
}
// Send raw XML
_, err = client.SendOrg(message)
if err != nil {
closeAndExit(client, cancel, 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()
client.Close()
os.Exit(0)
}
}()
for {
message, err = reader.ReadString('\n')
message = strings.TrimSuffix(message, "\n")
if err != nil {
closeAndExit(client, cancel, errors.New("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, 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, cancel, err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
})
if err != nil {
closeAndExit(client, cancel, 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 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, cancel, err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
})
if err != nil {
closeAndExit(client, cancel, err)
}
}
}
}
closeAndExit(client, cancel, nil)
}