go-sendxmpp/main.go

469 lines
13 KiB
Go
Raw Normal View History

2021-03-03 10:48:27 +00:00
// Copyright 2018 - 2021 Martin Dosch.
2020-04-05 20:02:09 +00:00
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.
2018-08-04 10:19:32 +00:00
2018-08-04 10:16:28 +00:00
package main
import (
"bufio"
"crypto/tls"
"fmt"
2018-08-04 10:16:28 +00:00
"io"
"log"
"net"
2018-08-04 10:16:28 +00:00
"os"
"regexp"
2018-08-10 10:14:21 +00:00
"strings"
"time"
2018-08-04 10:16:28 +00:00
"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
"github.com/mattn/go-xmpp" // BSD-3-Clause
"github.com/pborman/getopt/v2" // BSD-3-Clause
2018-08-04 10:16:28 +00:00
)
2018-08-10 10:14:21 +00:00
type configuration struct {
username string
jserver string
port string
password string
2021-06-26 07:59:08 +00:00
resource string
2018-08-10 10:14:21 +00:00
}
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
}
2018-08-04 10:16:28 +00:00
func main() {
type recipientsType struct {
Jid string
OxKeyRing *crypto.KeyRing
}
2018-08-04 10:16:28 +00:00
var (
2021-06-26 07:59:08 +00:00
err error
message, user, server, password, resource string
oxPrivKey *crypto.Key
recipients []recipientsType
2018-08-04 10:16:28 +00:00
)
2018-08-10 10:14:21 +00:00
// Define command line flags.
flagHelp := getopt.BoolLong("help", 0, "Show help.")
2020-04-09 12:58:03 +00:00
flagHttpUpload := getopt.StringLong("http-upload", 0, "", "Send a file via http-upload.")
2018-08-10 10:14:21 +00:00
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.")
2021-06-26 07:59:08 +00:00
flagResource := getopt.StringLong("resource", 'r', "", "Set resource. "+
"When sending to a chatroom this is used as 'alias'.")
2021-02-28 16:58:32 +00:00
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).")
2021-01-30 13:24:54 +00:00
flagRaw := getopt.BoolLong("raw", 0, "Send raw XML.")
2022-02-07 15:14:47 +00:00
flagListen := getopt.BoolLong("listen", 'l', "Listen for messages and print them to stdout.")
2022-02-12 08:18:02 +00:00
flagTimeout := getopt.IntLong("timeout", 0, 10, "Connection timeout in seconds.")
flagTLSMinVersion := getopt.IntLong("tls-version", 0, 12,
2022-02-23 09:06:06 +00:00
"Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).")
2022-02-12 10:16:45 +00:00
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).")
flagOxGenPrivKey := getopt.BoolLong("ox-genprivkey", 0,
"Generate a public OpenPGP key 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.")
2022-04-18 09:16:41 +00:00
flagOxImportPrivKey := getopt.StringLong("ox-import-privkey", 0, "",
"Import an existing private OpenPGP key.")
2018-08-10 10:14:21 +00:00
// Parse command line flags.
getopt.Parse()
2022-04-25 19:14:40 +00:00
switch {
case *flagHelp:
// If requested, show help and quit.
2018-08-10 10:14:21 +00:00
getopt.Usage()
os.Exit(0)
2022-04-25 19:14:40 +00:00
case *flagVersion:
// If requested, show version and quit.
2022-02-19 08:24:48 +00:00
fmt.Println("go-sendxmpp", VERSION)
2022-02-12 10:16:45 +00:00
fmt.Println("License: BSD-2-clause")
os.Exit(0)
2022-04-25 19:14:40 +00:00
// 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 *flagOx && *flagListen:
log.Fatal("No Ox support for receiving messages 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.")
}
2018-08-10 10:14:21 +00:00
// Read recipients from command line and quit if none are specified.
2022-02-06 22:25:00 +00:00
// 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()
2022-04-18 09:16:41 +00:00
if (len(recipientsList) == 0 && !*flagRaw && !*flagListen && !*flagOxGenPrivKey &&
*flagOxImportPrivKey == "") || (len(recipientsList) == 0 && *flagChatroom) {
2018-08-10 10:14:21 +00:00
log.Fatal("No recipient specified.")
}
2018-08-04 10:16:28 +00:00
// 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
2021-06-26 07:59:08 +00:00
resource = config.resource
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
}
2021-06-26 07:59:08 +00:00
// Overwrite resource if specified via command line flag
if *flagResource != "" {
resource = *flagResource
} else if resource == "" {
// Use "go-sendxmpp" plus a random string if no other resource is specified
resource = "go-sendxmpp." + getShortID()
2021-06-26 07:59:08 +00:00
}
2022-02-12 08:18:02 +00:00
// 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)
}
2018-08-10 10:14:21 +00:00
// Set XMPP connection options.
2018-08-04 10:16:28 +00:00
options := xmpp.Options{
2022-02-12 08:18:02 +00:00
Host: server,
User: user,
DialTimeout: timeout,
2021-06-26 07:59:08 +00:00
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,
2018-08-04 10:16:28 +00:00
}
// Read message from file.
if *flagMessageFile != "" {
message, err = readMessage(*flagMessageFile)
if err != nil {
log.Fatal(err)
}
}
2018-08-04 10:16:28 +00:00
// Connect to server.
client, err := connect(options, *flagDirectTLS)
2018-08-04 10:16:28 +00:00
if err != nil {
log.Fatal(err)
}
for _, r := range getopt.Args() {
var re recipientsType
re.Jid = r
2022-04-18 12:59:29 +00:00
if *flagOx {
re.OxKeyRing, err = oxGetPublicKeyRing(client, r)
2022-04-18 12:59:29 +00:00
if err != nil {
re.OxKeyRing = nil
2022-04-18 12:59:29 +00:00
fmt.Println("Couldn't receive key for:", r)
}
}
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
}
2022-04-25 19:14:40 +00:00
switch {
case *flagOxGenPrivKey:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
log.Fatal(err)
}
err = oxGenPrivKey(validatedOwnJid, client, *flagOxPassphrase)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
2022-04-25 19:14:40 +00:00
case *flagOxImportPrivKey != "":
2022-04-18 09:16:41 +00:00
validatedOwnJid, err := MarshalJID(user)
if err != nil {
log.Fatal(err)
}
err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
client)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
2022-04-25 19:14:40 +00:00
case *flagOx:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
log.Fatal(err)
}
oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
if err != nil {
log.Fatal(err)
}
}
2020-04-09 12:58:03 +00:00
if *flagHttpUpload != "" {
message = httpUpload(client, tlsConfig.ServerName,
2020-04-09 12:58:03 +00:00
*flagHttpUpload)
}
// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
2022-02-06 22:25:00 +00:00
// Also for listening mode.
if !*flagInteractive && !*flagListen {
if message == "" {
2018-08-04 10:16:28 +00:00
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
2018-08-04 10:16:28 +00:00
if message == "" {
message = scanner.Text()
} else {
message = message + "\n" + scanner.Text()
}
2018-08-04 10:16:28 +00:00
}
if err := scanner.Err(); err != nil {
if err != io.EOF {
// Close connection and quit.
_ = client.Close()
log.Fatal(err)
}
2018-08-04 10:16:28 +00:00
}
}
}
// Remove invalid code points.
message = strings.ToValidUTF8(message, "")
reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`)
message = reg.ReplaceAllString(message, "")
2022-04-25 19:14:40 +00:00
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, *flagResource,
*flagMUCPassword, xmpp.NoHistory, 0, &dummyTime)
} else {
_, err = client.JoinMUCNoHistory(recipient.Jid, *flagResource)
}
if err != nil {
log.Fatal(err)
}
2021-01-30 13:24:54 +00:00
}
2022-04-25 19:14:40 +00:00
}
switch {
case *flagRaw:
// Send raw XML
2021-01-30 13:24:54 +00:00
_, err = client.SendOrg(message)
if err != nil {
log.Fatal(err)
}
2022-04-25 19:14:40 +00:00
case *flagInteractive:
// Send in endless loop (for usage with e.g. "tail -f").
for {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
message = scanner.Text()
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 {
log.Fatal(err)
}
default:
_, err = client.Send(xmpp.Chat{Remote: recipient.Jid,
Type: msgType, Text: message})
if err != nil {
log.Fatal(err)
}
}
}
2021-01-30 13:24:54 +00:00
}
2022-04-25 19:14:40 +00:00
case *flagListen:
2022-02-06 22:25:00 +00:00
for {
2022-02-07 15:13:45 +00:00
received, err := client.Recv()
2022-02-06 22:25:00 +00:00
if err != nil {
log.Println(err)
}
switch v := received.(type) {
2022-02-06 22:25:00 +00:00
case xmpp.Chat:
if v.Text == "" {
continue
}
t := time.Now()
bareFrom := strings.Split(v.Remote, "/")[0]
// Print any messages if no recipients are specified
if len(recipients) == 0 {
fmt.Println(t.Format(time.RFC3339), bareFrom+":", v.Text)
} else {
for _, recipient := range recipients {
if bareFrom == strings.ToLower(recipient.Jid) {
fmt.Println(t.Format(time.RFC3339), bareFrom+":", v.Text)
}
}
2022-02-06 22:25:00 +00:00
}
default:
continue
}
}
2022-04-25 19:14:40 +00:00
default:
2018-08-10 10:14:21 +00:00
for _, recipient := range recipients {
2022-04-25 19:14:40 +00:00
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)
}
2022-04-25 19:14:40 +00:00
case *flagOx:
if recipient.OxKeyRing == nil {
continue
2020-04-09 14:49:27 +00:00
}
2022-04-25 19:14:40 +00:00
oxMessage, err := oxEncrypt(client, oxPrivKey,
recipient.Jid, recipient.OxKeyRing, message)
if err != nil {
2022-04-25 19:14:40 +00:00
fmt.Println("Ox: couldn't encrypt to", recipient.Jid)
continue
}
2022-04-25 19:14:40 +00:00
_, err = client.SendOrg(oxMessage)
if err != nil {
log.Fatal(err)
2020-04-09 14:49:27 +00:00
}
2022-04-25 19:14:40 +00:00
default:
_, err = client.Send(xmpp.Chat{Remote: recipient.Jid,
Type: msgType, Text: message})
if err != nil {
log.Fatal(err)
}
2018-08-10 10:14:21 +00:00
}
2018-08-04 10:16:28 +00:00
}
}
// 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)
2019-02-13 17:31:14 +00:00
// Close XMPP connection
err = client.Close()
if err != nil {
log.Fatal(err)
}
2018-08-04 10:16:28 +00:00
}