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.
607 lines
17 KiB
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)
|
|
}
|