// Copyright 2018 - 2020 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/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 ~/.sendxmpprc if no config path is specified. if configPath == "" { // Get systems user config path. osConfigDir := os.Getenv("$XDG_CONFIG_HOME") if osConfigDir != "" { configPath = osConfigDir + "/.sendxmpprc" } 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 + "/.sendxmpprc" } } // Check that config file is existing. info, err := os.Stat(configPath) if os.IsNotExist(err) { return output, err } // Only check file permissions if we are not running on windows. if runtime.GOOS != "windows" { // Check for file permissions. Must be 600 or 400. perm := info.Mode().Perm() permissions := strconv.FormatInt(int64(perm), 8) if permissions != "600" && permissions != "400" { return output, errors.New("Wrong permissions for " + configPath + ": " + permissions + " instead of 400 or 600.") } } // 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.Split(scanner.Text(), " ") switch row[0] { case "username:": output.username = row[1] case "jserver:": output.jserver = row[1] case "password:": output.password = row[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() 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 TLS.") flagStartTLS := getopt.BoolLong("start-tls", 'x', "Use StartTLS.") 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: ~/.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).") // 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. recipients := getopt.Args() if len(recipients) == 0 { 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, server or password is not specified. if *flagUser == "" || *flagServer == "" || *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 = strings.Split(user, "@")[1] tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client") tlsConfig.InsecureSkipVerify = *flagSkipVerify // Set XMPP connection options. options := xmpp.Options{ Host: server, User: user, Resource: *flagResource, Password: password, NoTLS: !*flagTLS, StartTLS: *flagStartTLS, 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 := options.NewClient() if err != nil { log.Fatal(err) } if *flagHttpUpload != "" { message = httpUpload(client, tlsConfig.ServerName, *flagHttpUpload) } // 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 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 { // If the chatroom flag is not set, send message to contact(s). // 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 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) // Close XMPP connection err = client.Close() if err != nil { log.Fatal(err) } }