go-sendxmpp/go-sendxmpp.go
2021-06-04 17:37:48 +02:00

555 lines
14 KiB
Go

// Copyright 2018 - 2021 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"
"io"
"log"
"os"
"os/exec"
"os/user"
"runtime"
"strconv"
"strings"
"time"
"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
}
// Opens the config file and returns the specified values
// for username, server and port.
func parseConfig(configPath string) (configuration, error) {
var (
output configuration
err error
)
// Use $XDG_CONFIG_HOME/.config/go-sendxmpp/sendxmpprc or
// ~/.sendxmpprc if no config path is specified.
if configPath == "" {
// Get systems user config path.
osConfigDir := os.Getenv("$XDG_CONFIG_HOME")
if osConfigDir != "" {
configPath = osConfigDir + "/go-sendxmpp/sendxmpprc"
// Check that the config file is existing.
_, err := os.Stat(configPath)
if os.IsNotExist(err) {
// If the file is not existing at ~/.config/go-sendxmpp/sendxmpprc
// check at the perl sendxmpp legacy destination.
configPath = osConfigDir + "/.sendxmpprc"
_, err = os.Stat(configPath)
if os.IsNotExist(err) {
return output, errors.New("no configuration file found")
}
}
} else {
// Get the current user.
curUser, err := user.Current()
if err != nil {
return output, err
}
// Get home directory.
home := curUser.HomeDir
if home == "" {
return output, errors.New("no home directory found")
}
configPath = home + "/.config/go-sendxmpp/sendxmpprc"
// Check that config file is existing.
_, err = os.Stat(configPath)
if os.IsNotExist(err) {
// If the file is not existing at ~/.config/go-sendxmpp/sendxmpprc
// check at the perl sendxmpp legacy destination.
configPath = home + "/.sendxmpprc"
_, err = os.Stat(configPath)
if os.IsNotExist(err) {
return output, errors.New("no configuration file found")
}
}
}
}
// Only check file permissions if we are not running on windows.
if runtime.GOOS != "windows" {
info, _ := os.Stat(configPath)
// Check for file permissions. Must be 600, 640, 440 or 400.
perm := info.Mode().Perm()
permissions := strconv.FormatInt(int64(perm), 8)
if permissions != "600" && permissions != "640" && permissions != "440" && permissions != "400" {
return output, errors.New("Wrong permissions for " + configPath + ": " +
permissions + " instead of 400, 440, 600 or 640.")
}
}
// Open config file.
file, err := os.Open(configPath)
if err != nil {
return output, err
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
// Read config file per line.
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "#") {
continue
}
row := strings.SplitN(scanner.Text(), " ", 2)
switch row[0] {
case "username:":
output.username = row[1]
case "jserver:":
output.jserver = row[1]
case "password:":
output.password = row[1]
case "eval_password:":
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}
out, err := exec.Command(shell, "-c", row[1]).Output()
if err != nil {
log.Fatal(err)
}
output.password = string(out)
if output.password[len(output.password)-1] == '\n' {
output.password = output.password[:len(output.password)-1]
}
case "port:":
output.port = row[1]
default:
if len(row) >= 2 {
if strings.Contains(scanner.Text(), ";") {
output.username = strings.Split(row[0], ";")[0]
output.jserver = strings.Split(row[0], ";")[1]
output.password = row[1]
} else {
output.username = strings.Split(row[0], ":")[0]
output.jserver = strings.Split(row[0], "@")[1]
output.password = row[1]
}
}
}
}
file.Close()
// Check if the username is a valid JID
output.username, err = MarshalJID(output.username)
if err != nil {
// Check whether only the local part was used by appending an @ and the
// server part.
output.username = output.username + "@" + output.jserver
// Check if the username is a valid JID now
output.username, err = MarshalJID(output.username)
if err != nil {
return output, errors.New("invalid username/JID: " + output.username)
}
}
return output, err
}
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() {
var (
err error
message, user, server, password string
)
// Define command line flags.
flagHelp := getopt.BoolLong("help", 0, "Show help.")
flagHttpUpload := getopt.StringLong("http-upload", 0, "", "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.")
flagTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
flagStartTLS := getopt.BoolLong("start-tls", 'x', "Use StartTLS. (DEPRECATED)")
flagResource := getopt.StringLong("resource", 'r', "go-sendxmpp", "Set resource. "+
"When sending to a chatroom this is used as 'alias'. (Default: go-sendxmpp)")
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.")
// Parse command line flags.
getopt.Parse()
// If requested, show help and quit.
if *flagHelp {
getopt.Usage()
os.Exit(0)
}
// Read recipients from command line and quit if none are specified.
// For sending raw XML it's not required to specify a recipient except
// for MUCs (go-sendxmpp will join the MUC automatically).
recipients := getopt.Args()
if (len(recipients) == 0 && !*flagRaw) || (len(recipients) == 0 && *flagChatroom) {
log.Fatal("No recipient specified.")
}
// Quit if unreasonable TLS setting is set.
if *flagStartTLS && *flagTLS {
log.Fatal("Use either TLS or StartTLS.")
}
// Check that all recipient JIDs are valid.
for i, recipient := range recipients {
validatedJid, err := MarshalJID(recipient)
if err != nil {
log.Fatal(err)
}
recipients[i] = validatedJid
}
// 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
if config.port != "" {
server = server + ":" + 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 (*flagHttpUpload != "") && (*flagInteractive || (*flagMessageFile != "")) {
if *flagInteractive {
log.Fatal("Interactive mode and http upload can't" +
" be used at the same time.")
}
if *flagMessageFile != "" {
log.Fatal("You can't send a message while using" +
" http upload.")
}
}
// Use ALPN
var tlsConfig tls.Config
tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
tlsConfig.InsecureSkipVerify = *flagSkipVerify
// Set XMPP connection options.
options := xmpp.Options{
Host: server,
User: user,
// TODO: Check whether the timeout is reasonable or maybe make
// it configurable
DialTimeout: 1 * time.Second,
Resource: *flagResource,
Password: password,
NoTLS: !*flagTLS,
StartTLS: !*flagTLS,
Debug: *flagDebug,
TLSConfig: &tlsConfig,
}
// Read message from file.
if *flagMessageFile != "" {
message, err = readMessage(*flagMessageFile)
if err != nil {
log.Fatal(err)
}
}
// Connect to server.
client, err := connect(options, *flagTLS)
if err != nil {
log.Fatal(err)
}
if *flagHttpUpload != "" {
message = httpUpload(client, tlsConfig.ServerName,
*flagHttpUpload)
}
// Use a goroutine to check whether there is an error reply.
var messageErrors int
go func() {
for {
chat, err := client.Recv()
if err != nil {
log.Fatal(err)
}
switch v := chat.(type) {
case xmpp.Chat:
if strings.ToLower(v.Type) == "error" {
messageErrors++
log.Println("Error: Received error reply from",
v.Remote)
}
}
}
}()
// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
if !*flagInteractive {
if 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 {
// Close connection and quit.
_ = client.Close()
log.Fatal(err)
}
}
}
}
// Send raw XML to chatroom
if *flagChatroom && *flagRaw {
// Join the MUCs.
for _, recipient := range recipients {
_, err := client.JoinMUCNoHistory(recipient, *flagResource)
if err != nil {
// Try to nicely close connection,
// even if there was an error joining.
_ = client.Close()
log.Fatal(err)
}
}
// Send raw XMP
_, err = client.SendOrg(message)
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
log.Fatal(err)
}
// After sending the message, leave the MUCs
for _, recipient := range recipients {
_, err = client.LeaveMUC(recipient)
if err != nil {
log.Println(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)
return
}
// Send message to chatroom(s) if the flag is set.
if *flagChatroom {
for _, recipient := range recipients {
// Join the MUC.
_, err := client.JoinMUCNoHistory(recipient, *flagResource)
if err != nil {
// Try to nicely close connection,
// even if there was an error joining.
_ = client.Close()
log.Fatal(err)
}
}
// Send in endless loop (for usage with e.g. "tail -f").
if *flagInteractive {
for {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
message = scanner.Text()
for _, recipient := range recipients {
_, err = client.Send(xmpp.Chat{Remote: recipient,
Type: "groupchat", Text: message})
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
log.Fatal(err)
}
}
}
} else {
// Send the message.
for _, recipient := range recipients {
if *flagHttpUpload != "" {
_, err = client.Send(xmpp.Chat{Remote: recipient,
Type: "groupchat", Ooburl: message, Text: message})
} else {
_, err = client.Send(xmpp.Chat{Remote: recipient,
Type: "groupchat", Text: message})
}
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
log.Fatal(err)
}
}
}
for _, recipient := range recipients {
// After sending the message, leave the Muc
_, err = client.LeaveMUC(recipient)
if err != nil {
log.Println(err)
}
}
} else {
// Send raw XML
if *flagRaw {
_, err = client.SendOrg(message)
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
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)
return
}
// Send in endless loop (for usage with e.g. "tail -f").
if *flagInteractive {
for {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
message = scanner.Text()
for _, recipient := range recipients {
_, err = client.Send(xmpp.Chat{Remote: recipient,
Type: "chat", Text: message})
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
log.Fatal(err)
}
}
}
} else {
for _, recipient := range recipients {
if *flagHttpUpload != "" {
_, err = client.Send(xmpp.Chat{Remote: recipient, Type: "chat",
Ooburl: message, Text: message})
} else {
_, err = client.Send(xmpp.Chat{Remote: recipient, Type: "chat",
Text: message})
}
if err != nil {
// Try to nicely close connection,
// even if there was an error sending.
_ = client.Close()
log.Fatal(err)
}
}
}
}
// Wait for 1s whether there will be incoming errors.
time.Sleep(1 * time.Second)
// Log if there were message errors reported.
switch messageErrors {
case 0:
break
case 1:
log.Fatal("There was one error reply.")
default:
log.Fatal("There were ", messageErrors, " error replies.")
}
// Close XMPP connection
err = client.Close()
if err != nil {
log.Fatal(err)
}
}