Compare commits

..

No commits in common. 'master' and 'v0.6.1' have entirely different histories.

@ -38,31 +38,28 @@ format:
#- '[ -e .golangci.yml ] || cp /golangci/.golangci.yml .' #- '[ -e .golangci.yml ] || cp /golangci/.golangci.yml .'
#- golangci-lint run #- golangci-lint run
#allow_failure: true #allow_failure: true
artifacts:
expire_in: 1 year
compile: compile:
stage: build stage: build
only: only:
- tags - tags
before_script:
- echo "deb https://deb.debian.org/debian/ bookworm-backports main" >> /etc/apt/sources.list
- apt-get -qq update && apt-get -qq install -y upx-ucl
script: script:
- echo "${CI_JOB_ID}" > CI_JOB_ID.txt - echo "${CI_JOB_ID}" > CI_JOB_ID.txt
- env GOOS=linux GOARCH=amd64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-amd64/go-sendxmpp - env GOOS=linux GOARCH=amd64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-amd64/go-sendxmpp
- upx $CI_PROJECT_DIR/linux-amd64/go-sendxmpp || true
- env GOOS=linux GOARCH=arm64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-arm64/go-sendxmpp - env GOOS=linux GOARCH=arm64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-arm64/go-sendxmpp
- upx $CI_PROJECT_DIR/linux-arm64/go-sendxmpp || true - env GOOS=linux GOARCH=386 go build -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-386/go-sendxmpp
- env GOOS=linux GOARCH=arm go build -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/linux-arm/go-sendxmpp
- env GOOS=windows GOARCH=386 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/win386/go-sendxmpp.exe
- env GOOS=windows GOARCH=amd64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/win64/go-sendxmpp.exe - env GOOS=windows GOARCH=amd64 go build -buildmode=pie -ldflags "-s -w -extldflags '-static'" -o $CI_PROJECT_DIR/win64/go-sendxmpp.exe
- upx $CI_PROJECT_DIR/win64/go-sendxmpp.exe || true
artifacts: artifacts:
paths: paths:
- linux-amd64/go-sendxmpp - linux-amd64/go-sendxmpp
- linux-arm64/go-sendxmpp - linux-arm64/go-sendxmpp
- linux-386/go-sendxmpp
- linux-arm/go-sendxmpp
- win386/go-sendxmpp.exe
- win64/go-sendxmpp.exe - win64/go-sendxmpp.exe
- CI_JOB_ID.txt - CI_JOB_ID.txt
expire_in: 2 years
release: release:
stage: release stage: release
@ -74,6 +71,7 @@ release:
release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG --description="`head -n $(expr "$(grep -nm2 "^## " CHANGELOG.md|awk '(NR>1) {print $1}'|cut -f1 -d:) - 2"|bc) CHANGELOG.md`" \ release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG --description="`head -n $(expr "$(grep -nm2 "^## " CHANGELOG.md|awk '(NR>1) {print $1}'|cut -f1 -d:) - 2"|bc) CHANGELOG.md`" \
--assets-link "{\"name\":\"Linux amd64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-amd64/go-sendxmpp\"}" \ --assets-link "{\"name\":\"Linux amd64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-amd64/go-sendxmpp\"}" \
--assets-link "{\"name\":\"Linux arm64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-arm64/go-sendxmpp\"}" \ --assets-link "{\"name\":\"Linux arm64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-arm64/go-sendxmpp\"}" \
--assets-link "{\"name\":\"Linux 386\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-386/go-sendxmpp\"}" \
--assets-link "{\"name\":\"Linux arm\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/linux-arm/go-sendxmpp\"}" \
--assets-link "{\"name\":\"Windows 386\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/win386/go-sendxmpp.exe\"}" \
--assets-link "{\"name\":\"Windows amd64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/win64/go-sendxmpp.exe\"}" --assets-link "{\"name\":\"Windows amd64\",\"url\":\"https://salsa.debian.org/mdosch/go-sendxmpp/-/jobs/`cat CI_JOB_ID.txt`/artifacts/file/win64/go-sendxmpp.exe\"}"
artifacts:
expire_in: 2 years

@ -1,104 +1,5 @@
# Changelog # Changelog
## UNRELEASED
## [v0.11.2] 2024-09-17
### Changed
- Add Gopenpgp and Xmppsrv version to `--version` output (requires xmppsrv >= 0.3.2).
- Improve selection between StartTLS and DirectTLS.
## [v0.11.1] 2024-07-11
### Changed
- Fix Ox encryption in interactive mode (do not add the same recipient key to the keyring over and over again).
- Exit with error code if Ox encryption for one recipient fails.
- Improved handling of perl sendxmpp config files.
## [v0.11.0] 2024-05-29
### Changed
- Move private Ox key into JID folder in ~/.local/share/go-sendxmpp.
- Use `fmt.Errorf()` instead of `errors.New()` to create new error messages.
### Added
- Add new parameter `--subject`.
- Added flag `--fast-off` to disable XEP-0484: Fast Authentication Streamlining Tokens (requires go-xmpp >= 0.2.1).
## [v0.10.0] 2024-04-13
### Changed
- Fixed a race condition in receiving stanzas (requires go-xmpp >= v0.1.5).
### Added
- Add support for SASL2 and BIND2 (via go-xmpp >= v0.2.0).
- Add support for FAST authentication (via go-xmpp >= v0.2.0).
- Add a warning when run by the user *root*.
## [v0.9.0] 2024-03-28
### Changed
- Properly close stream if `Ctrl+C` is pressed in interactive mode.
- Properly close stream if `Ctrl+C` is pressed in listening mode.
- Print OS, architecture and go version for flag `--version`.
- Improve closing of connection (via go-xmpp v0.1.4).
- Don't send stanzas that exceed the size limit provided by XEP-0478 (requires go-xmpp >= v0.1.4).
- Fixed hanging forever in stream close if the server doesn't reply with a closing stream element (via go-xmpp >= v0.1.4).
### Added
- New command line flag `ssdp-off` to disable XEP-0474: SASL SCRAM Downgrade Protection (requires go-xmpp >= v0.1.4).
## [v0.8.4] 2024-03-09
### Changed
- Properly handle lost connection.
- Better compatibility with perl sendxmpp config files.
- Improve file name for private Ox keys.
- Improve fallback behavior if no SRV records are provided.
- Remove 100ms sleep before closing the connection. This should be no more needed since go-xmpp commit 9684a8ff690f0d75e284f8845696c5057926d276.
- Return an error if there is no answer to an IQ within 60s.
- Check for errors after sending the auth message during SCRAM authentication (via go-xmpp v0.1.2).
## [v0.8.3] 2024-02-17
### Changed
- Use a human readable file name for private Ox keys.
- Fix specifying a message via command line flag `-m`.
## [v0.8.2] 2024-01-19
### Changed
- Fix an issue in look up of SRV records (via xmppsrv v0.2.6). Thx mtp.
## [v0.8.1] 2024-01-16
### Added
- Add support for `tls-server-end-point` channel binding (via go-xmpp commit 3f0cbac30767faa562ad198ee69f36055f5924bc).
- Add experimental support for SOCKS5 proxies using the `HTTP_PROXY` environment variable (requires go-xmpp commit 685570cbd85c31ea3b426bea34dd4af404aac8cf).
### Changed
- http-upload: Improve error handling.
## [v0.8.0] 2024-01-09
### Added
- Add new parameter `--scram-mech-pinning`.
### Changed
- Refuse to upload a file if upload slot doesn't provide https.
- Use XEP-0474 instead of SCRAM mechanism pinning to prevent downgrade attacks (requires go-xmpp commit 39f5b80375b6f6f266df37b4a4adcbeb606ffec2).
## [v0.7.0] 2023-11-11
### Added
- Reply to XEP-0092 software version requests.
- Add support for PLUS variants of SCRAM authentication mechanisms (requires go-xmpp commit 4c385a334c606e8bc387f0a3d4d84975802b3984).
- Add pinning of last used authentication mechanism if a SCRAM mechanism was used.
### Changed
- Print every stanza in a new line (requires go-xmpp commit 31c7eb6919b67b18e901dc45a8e5681040ea7f31).
## [v0.6.2] 2023-09-29
### Changed
- Properly close connection to server if ^C is pressed in interactive mode.
- Replace invalid characters by UTF8 replacement char.
- Add warning that there is no Ox support for messages of type headline.
- Suppress warnings about reading from closed connection if go-sendxmpp closes the connection before exiting.
- Remove unnecessary newlines after stanzas.
- Fix segfault when authentication fails due to invalid username or password.
## Removed
- Removed deprecated flag and config option `resource`.
## [v0.6.1] 2023-07-25 ## [v0.6.1] 2023-07-25
### Changed ### Changed
- Properly close connection to server. - Properly close connection to server.

@ -15,7 +15,7 @@ sendxmpp incarnations. :)
## requirements ## requirements
* [go](https://golang.org/) >= 1.21 * [go](https://golang.org/) >= 1.17
## installation ## installation
@ -75,11 +75,10 @@ If no configuration file is present or if the values should be overridden it is
the account details via command line options: the account details via command line options:
```plain ```plain
Usage: go-sendxmpp [-cdilnt] [-a value] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [-m value] [--muc-password value] [--oob-file value] [--ox] [--ox-delete-nodes] [--ox-genprivkey-rsa] [--ox-genprivkey-x25519] [--ox-import-privkey value] [--ox-passphrase value] [-p value] [--raw] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--timeout value] [--tls-version value] [-u value] [--version] [recipients…] Usage: go-sendxmpp [-cdilnt] [-a value] [-f value] [--headline] [--help] [-h value] [-j value] [-m value] [--muc-password value] [--oob-file value] [--ox] [--ox-delete-nodes] [--ox-genprivkey-rsa] [--ox-genprivkey-x25519] [--ox-import-privkey value] [--ox-passphrase value] [-p value] [--raw] [-r value] [--timeout value] [--tls-version value] [-u value] [--version] [parameters ...]
-a, --alias=value Set alias/nicknamefor chatrooms. -a, --alias=value Set alias/nicknamefor chatrooms.
-c, --chatroom Send message to a chatroom. -c, --chatroom Send message to a chatroom.
-d, --debug Show debugging info. -d, --debug Show debugging info.
--fast-off Disable XEP-0484: Fast Authentication Streamlining Tokens.
-f, --file=value Set configuration file. (Default: -f, --file=value Set configuration file. (Default:
~/.config/go-sendxmpp/sendxmpprc) ~/.config/go-sendxmpp/sendxmpprc)
--headline Send message as type headline. --headline Send message as type headline.
@ -115,11 +114,9 @@ Usage: go-sendxmpp [-cdilnt] [-a value] [--fast-off] [-f value] [--headline] [--
-p, --password=value -p, --password=value
Password for XMPP account. Password for XMPP account.
--raw Send raw XML. --raw Send raw XML.
--scram-mech-pinning=value -r, --resource=value
Enforce the use of a certain SCRAM authentication mechanism. Set resource. When sending to a chatroom this is used as
--ssdp-off Disable XEP-0474: SASL SCRAM Downgrade Protection. 'alias'. DEPRECATED: Use --alias instead.
-s, --subject=value
Set message subject.
--timeout=value --timeout=value
Connection timeout in seconds. [10] Connection timeout in seconds. [10]
-t, --tls Use direct TLS. -t, --tls Use direct TLS.
@ -142,7 +139,7 @@ cat message.txt | ./go-sendxmpp -f ./sendxmpp recipient1@example.com recipient2@
Send a message to two recipients directly defining account credentials. Send a message to two recipients directly defining account credentials.
```bash ```bash
cat message.txt | ./go-sendxmpp -u bob@example.com -p swordfish recipient1@example.com recipient2@example.com cat message.txt | ./go-sendxmpp -u bob@example.com -j example.com -p swordfish recipient1@example.com recipient2@example.com
``` ```
Send a message to two groupchats (`-c`) using a configuration file. Send a message to two groupchats (`-c`) using a configuration file.
@ -156,11 +153,6 @@ Send file changes to two groupchats (`-c`) using a configuration file.
```bash ```bash
tail -f example.log | ./go-sendxmpp -cif ./sendxmpp chat1@conference.example.com chat2@conference.example.com tail -f example.log | ./go-sendxmpp -cif ./sendxmpp chat1@conference.example.com chat2@conference.example.com
``` ```
Send a notification if a long running process finishes.
```bash
waitpid $(pidof -s rsync) && echo "Rsync finished."|go-sendxmpp recipient@example.com
```
### shell completion ### shell completion

@ -5,70 +5,55 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"os"
"strings" "strings"
"github.com/xmppo/go-xmpp" // BSD-3-Clause "github.com/mattn/go-xmpp" // BSD-3-Clause
"salsa.debian.org/mdosch/xmppsrv" // BSD-2-Clause "salsa.debian.org/mdosch/xmppsrv" // BSD-2-Clause
) )
func connect(options xmpp.Options, directTLS bool) (*xmpp.Client, error) { func connect(options xmpp.Options, directTLS bool) (*xmpp.Client, error) {
proxy := os.Getenv("HTTP_PROXY")
server := options.User[strings.Index(options.User, "@")+1:]
// Look up SRV records if server is not specified manually. // Look up SRV records if server is not specified manually.
if options.Host == "" { if options.Host == "" {
// Don't do SRV look ups if proxy is set. server := options.User[strings.Index(options.User, "@")+1:]
if proxy == "" { // Look up xmpp-client SRV records.
// Look up xmpp-client SRV records. srvMixed, err := xmppsrv.LookupClient(server)
srvMixed, err := xmppsrv.LookupClient(server) if len(srvMixed) > 0 && err == nil {
if len(srvMixed) > 0 && err == nil { for _, adr := range srvMixed {
for _, adr := range srvMixed { if !directTLS && adr.Type != "xmpps-client" {
switch { // Use StartTLS
case directTLS && adr.Type == "xmpp-client": options.NoTLS = true
continue options.StartTLS = true
case adr.Type == "xmpp-client": } else if adr.Type == "xmpps-client" {
// Use StartTLS // Use direct TLS
options.NoTLS = true options.NoTLS = false
options.StartTLS = true options.StartTLS = false
case adr.Type == "xmpps-client": }
// Use direct TLS options.Host = net.JoinHostPort(adr.Target, fmt.Sprint(adr.Port))
options.NoTLS = false // Connect to server
options.StartTLS = false client, err := options.NewClient()
default: if errors.Unwrap(err) == nil {
continue return client, nil
}
options.Host = net.JoinHostPort(adr.Target, fmt.Sprint(adr.Port))
// Connect to server
client, err := options.NewClient()
if err == nil {
return client, nil
}
} }
} }
} }
} // Try port 5223 if directTLS is set and no xmpp-client SRV records are provided.
_, port, _ := net.SplitHostPort(options.Host)
if port == "" {
if options.Host == "" {
options.Host = server
}
// Try port 5223 if directTLS is set and no port is provided.
if directTLS { if directTLS {
options.NoTLS = false options.NoTLS = false
options.StartTLS = false options.StartTLS = false
options.Host = net.JoinHostPort(options.Host, "5223") options.Host = net.JoinHostPort(server, "5223")
} else { } else {
// Try port 5222 if no port is provided and directTLS is not set. // Try port 5222 if no xmpp-client SRV records are provided and directTLS is not set.
options.NoTLS = true options.NoTLS = true
options.StartTLS = true options.StartTLS = true
options.Host = net.JoinHostPort(options.Host, "5222") options.Host = net.JoinHostPort(server, "5222")
} }
} }
// Connect to server // Connect to server
client, err := options.NewClient() client, err := options.NewClient()
if err == nil { if errors.Unwrap(err) == nil {
return client, nil return client, nil
} }
return client, fmt.Errorf("failed to connect to server: %w", err) return client, fmt.Errorf("failed to connect to server: %w", err)

@ -5,24 +5,25 @@
package main package main
const ( const (
version = "0.11.3-dev" version = "0.6.1"
// defaults // defaults
defaultBufferSize = 100 defaultBufferSize = 100
defaultConfigColumnSep = 2 defaultConfigRowSep = 2
defaultDirRights = 0o700 defaultDirRights = 0o700
defaultFileRights = 0o600 defaultFileRights = 0o600
defaultFileRightsWin = 0o200 defaultFileRightsWin = 0o200
defaultIDBytes = 12 defaultIDBytes = 12
defaultLenServerConf = 2 defaultLenServerConf = 2
defaultRpadMultiple = 100 defaultRpadMultiple = 100
defaultRSABits = 4096 defaultRSABits = 4096
defaultShortIDBytes = 4 defaultShortIDBytes = 4
defaultTimeout = 10 defaultSleepTime = 100
defaultTLSMinVersion = 12 defaultTimeout = 10
defaultTLS10 = 10 defaultTLSMinVersion = 12
defaultTLS11 = 11 defaultTLS10 = 10
defaultTLS12 = 12 defaultTLS11 = 11
defaultTLS13 = 13 defaultTLS12 = 12
defaultTLS13 = 13
// namespace // namespace
nsDiscoInfo = "http://jabber.org/protocol/disco#info" nsDiscoInfo = "http://jabber.org/protocol/disco#info"
nsDiscoItems = "http://jabber.org/protocol/disco#items" nsDiscoItems = "http://jabber.org/protocol/disco#items"
@ -35,13 +36,11 @@ const (
nsOxPubKeys = "urn:xmpp:openpgp:0:public-keys" nsOxPubKeys = "urn:xmpp:openpgp:0:public-keys"
nsPubsub = "http://jabber.org/protocol/pubsub" nsPubsub = "http://jabber.org/protocol/pubsub"
nsPubsubOwner = "http://jabber.org/protocol/pubsub#owner" nsPubsubOwner = "http://jabber.org/protocol/pubsub#owner"
nsVersion = "jabber:iq:version"
nsXMPPStanzas = "urn:ietf:params:xml:ns:xmpp-stanzas" nsXMPPStanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"
// strings // strings
oxAltBody = "This message is encrypted (XEP-0373: OpenPGP for XMPP)." oxAltBody = "This message is encrypted (XEP-0373: OpenPGP for XMPP)."
pubsubPubOptions = "http://jabber.org/protocol/pubsub#publish-options" pubsubPubOptions = "http://jabber.org/protocol/pubsub#publish-options"
strChat = "chat" strChat = "chat"
strEmpty = ""
strError = "error" strError = "error"
strGroupchat = "groupchat" strGroupchat = "groupchat"
strHeadline = "headline" strHeadline = "headline"

@ -1,24 +1,23 @@
module salsa.debian.org/mdosch/go-sendxmpp module salsa.debian.org/mdosch/go-sendxmpp
go 1.21.5 go 1.17
require ( require (
github.com/ProtonMail/gopenpgp/v2 v2.7.5 github.com/ProtonMail/gopenpgp/v2 v2.7.2
github.com/beevik/etree v1.4.1 github.com/beevik/etree v1.2.0
github.com/gabriel-vasile/mimetype v1.4.5 github.com/gabriel-vasile/mimetype v1.4.2
github.com/google/uuid v1.6.0 github.com/mattn/go-xmpp v0.0.1
github.com/pborman/getopt/v2 v2.1.0 github.com/pborman/getopt/v2 v2.1.0
github.com/xmppo/go-xmpp v0.2.2-0.20240910180004-06b143aee341 salsa.debian.org/mdosch/xmppsrv v0.2.5
golang.org/x/crypto v0.27.0
salsa.debian.org/mdosch/xmppsrv v0.3.2
) )
require ( require (
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/cloudflare/circl v1.4.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
golang.org/x/net v0.29.0 // indirect golang.org/x/crypto v0.11.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/net v0.12.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
) )

@ -1,23 +1,21 @@
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA= github.com/ProtonMail/gopenpgp/v2 v2.7.2 h1:mIwxSUPezxNYq0RA5106VPWyKC+Ly3FvBUnBJh/7GWw=
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= github.com/ProtonMail/gopenpgp/v2 v2.7.2/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI= github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw=
github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/mattn/go-xmpp v0.0.1 h1:njHom/3EP3ynacLHX9lBpKMMknYL76ic/19fPsR6MB8=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-xmpp v0.0.1/go.mod h1:Cs5mF0OsrRRmhkyOod//ldNPOwJsrBvJ+1WRspv0xoc=
github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA=
github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -27,15 +25,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xmppo/go-xmpp v0.2.2-0.20240910180004-06b143aee341 h1:Nn7xp8WOd+hPDasApuRBRrmt0UbhEj2FM2cSovdTMdc=
github.com/xmppo/go-xmpp v0.2.2-0.20240910180004-06b143aee341/go.mod h1:0ZxTwt7zQQbRkVg9PpBISmGAjmxf+oik0JyAahsewM8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -44,8 +40,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -58,21 +55,25 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -81,5 +82,5 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
salsa.debian.org/mdosch/xmppsrv v0.3.2 h1:c83iqkp/GnsPYqQ12dTw8MQBzI+Dtw9mQOFSuL3GjaQ= salsa.debian.org/mdosch/xmppsrv v0.2.5 h1:ACPk8EhmCAUMl59TnGe5kvvwSnW065CJrhN7uvt25xY=
salsa.debian.org/mdosch/xmppsrv v0.3.2/go.mod h1:udWXnWFa9zkcyN9YSB/u44BCnnRDpeQ0eDy3MVLjHZQ= salsa.debian.org/mdosch/xmppsrv v0.2.5/go.mod h1:udWXnWFa9zkcyN9YSB/u44BCnnRDpeQ0eDy3MVLjHZQ=

@ -6,29 +6,22 @@ package main
import ( import (
"bytes" "bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand" "crypto/rand"
"encoding/gob" "errors"
"fmt" "fmt"
"log" "log"
"math/big" "math/big"
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"runtime"
"strings" "strings"
"github.com/google/uuid" // BSD-3-Clause
"github.com/xmppo/go-xmpp" // BSD-3-Clause
"golang.org/x/crypto/scrypt" // BSD-3-Clause
) )
func validUTF8(s string) string { func validUTF8(s string) string {
// Remove invalid code points. // Remove invalid code points.
s = strings.ToValidUTF8(s, "<EFBFBD>") s = strings.ToValidUTF8(s, "")
reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`) reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`)
s = reg.ReplaceAllString(s, "<EFBFBD>") s = reg.ReplaceAllString(s, "")
return s return s
} }
@ -36,190 +29,24 @@ func validUTF8(s string) string {
func validURI(s string) (*url.URL, error) { func validURI(s string) (*url.URL, error) {
// Check if URI is valid // Check if URI is valid
uri, err := url.ParseRequestURI(s) uri, err := url.ParseRequestURI(s)
if err != nil { return uri, fmt.Errorf("validURI: %w", err)
return uri, fmt.Errorf("validURI: %w", err)
}
return uri, nil
} }
func readFile(path string) (*bytes.Buffer, error) { func readFile(path string) (*bytes.Buffer, error) {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("readFile: %w", err) return nil, fmt.Errorf("readFile: %w", err)
} }
defer file.Close()
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(file) _, err = buffer.ReadFrom(file)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("readFile: %w", err) return nil, fmt.Errorf("readFile: %w", err)
} }
return buffer, nil err = file.Close()
} if errors.Unwrap(err) != nil {
fmt.Println("error while closing file:", err)
func getFastData(jid string, password string) (xmpp.Fast, error) {
folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1)
var fast xmpp.Fast
fastPath, err := getDataPath(folder)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache folder: %w", err)
}
fastFileLoc := fastPath + "fast.bin"
buf, err := readFile(fastFileLoc)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err)
}
decBuf := bytes.NewBuffer(buf.Bytes())
decoder := gob.NewDecoder(decBuf)
err = decoder.Decode(&fast)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err)
}
salt := make([]byte, 32)
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to create aes key: %w", err)
}
c, err := aes.NewCipher([]byte(key))
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err)
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err)
}
nonceSize := gcm.NonceSize()
cryptBuf := []byte(fast.Token)
nonce, cryptBuf := cryptBuf[:nonceSize], cryptBuf[nonceSize:]
tokenBuf, err := gcm.Open(nil, []byte(nonce), cryptBuf, nil)
if err != nil {
return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err)
}
fast.Token = string(tokenBuf)
return fast, nil
}
func writeFastData(jid string, password string, fast xmpp.Fast) error {
var encBuf bytes.Buffer
folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1)
fastPath, err := getDataPath(folder)
if err != nil {
return fmt.Errorf("writeFastData: failed to write fast cache file: %w", err)
}
fastFileLoc := fastPath + "fast.bin"
salt := make([]byte, 32)
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err)
}
c, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err)
}
buf := gcm.Seal(nonce, nonce, []byte(fast.Token), nil)
fast.Token = string(buf)
encode := gob.NewEncoder(&encBuf)
err = encode.Encode(fast)
if err != nil {
return fmt.Errorf("writeFastData: failed to create fast token file: %w", err)
}
file, err := os.Create(fastFileLoc)
if err != nil {
return fmt.Errorf("writeFastData: failed to create fast token file: %w", err)
}
defer file.Close()
if runtime.GOOS != "windows" {
_ = file.Chmod(os.FileMode(defaultFileRights))
} else {
_ = file.Chmod(os.FileMode(defaultFileRightsWin))
}
_, err = file.Write(encBuf.Bytes())
if err != nil {
return fmt.Errorf("writeFastData: failed to write fast token file: %w", err)
} }
return nil return buffer, nil
}
func getClientID(jid string) (string, error) {
var clientID string
folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1)
clientIDLoc, err := getClientIDLoc(folder)
if err != nil {
return strError, err
}
buf, err := readFile(clientIDLoc)
if err != nil {
clientID = uuid.NewString()
file, err := os.Create(clientIDLoc)
if err != nil {
return strEmpty, fmt.Errorf("getClientID: failed to create clientid file: %w", err)
}
defer file.Close()
if runtime.GOOS != "windows" {
_ = file.Chmod(os.FileMode(defaultFileRights))
} else {
_ = file.Chmod(os.FileMode(defaultFileRightsWin))
}
_, err = file.Write([]byte(clientID))
if err != nil {
return strEmpty, fmt.Errorf("getClientID: failed to write client id file: %w", err)
}
} else {
clientID = buf.String()
}
return clientID, nil
}
func getDataPath(folder string) (string, error) {
var err error
var homeDir, dataDir string
switch {
case os.Getenv("$XDG_DATA_HOME") != "":
dataDir = os.Getenv("$XDG_DATA_HOME")
case os.Getenv("$XDG_HOME") != "":
homeDir = os.Getenv("$XDG_HOME")
dataDir = homeDir + "/.local/share"
case os.Getenv("$HOME") != "":
homeDir = os.Getenv("$HOME")
dataDir = homeDir + "/.local/share"
default:
homeDir, err = os.UserHomeDir()
if err != nil {
return strError, fmt.Errorf("getDataPath: failed to determine user dir: %w", err)
}
if homeDir == "" {
return strError, fmt.Errorf("getDataPath: received empty string for home directory")
}
dataDir = homeDir + "/.local/share"
}
if folder != "" && !strings.HasSuffix(folder, "/") {
folder = fmt.Sprintf("%s/", folder)
}
dataDir = fmt.Sprintf("%s/go-sendxmpp/%s", dataDir, folder)
if _, err = os.Stat(dataDir); os.IsNotExist(err) {
err = os.MkdirAll(dataDir, defaultDirRights)
if err != nil {
return strError, fmt.Errorf("getDataPath: could not create folder: %w", err)
}
}
return dataDir, nil
}
func getClientIDLoc(folder string) (string, error) {
dataDir, err := getDataPath(folder)
if err != nil {
return strError, fmt.Errorf("getClientIDLoc: %w", err)
}
dataFile := dataDir + "clientid"
return dataFile, nil
} }
func getRpad(messageLength int) string { func getRpad(messageLength int) string {
@ -229,7 +56,7 @@ func getRpad(messageLength int) string {
rpad := make([]rune, length) rpad := make([]rune, length)
for i := range rpad { for i := range rpad {
randInt, err := rand.Int(rand.Reader, max) randInt, err := rand.Int(rand.Reader, max)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
rpad[i] = rpadRunes[randInt.Int64()] rpad[i] = rpadRunes[randInt.Int64()]
@ -238,9 +65,19 @@ func getRpad(messageLength int) string {
} }
func getID() string { func getID() string {
return uuid.NewString() id := make([]byte, defaultIDBytes)
_, err := rand.Read(id)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
return fmt.Sprintf("%x-%x-%x", id[0:4], id[4:8], id[8:])
} }
func getShortID() string { func getShortID() string {
return uuid.NewString()[:6] id := make([]byte, defaultShortIDBytes)
_, err := rand.Read(id)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
return fmt.Sprintf("%x", id[0:4])
} }

@ -7,39 +7,35 @@ package main
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"time"
"github.com/beevik/etree" // BSD-2-clause "github.com/beevik/etree" // BSD-2-clause
"github.com/gabriel-vasile/mimetype" // MIT License "github.com/gabriel-vasile/mimetype" // MIT License
"github.com/xmppo/go-xmpp" // BSD-3-Clause "github.com/mattn/go-xmpp" // BSD-3-Clause
) )
func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath string, func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath string) string {
timeout time.Duration,
) (string, error) {
var uploadComponent string var uploadComponent string
var maxFileSize int64 var maxFileSize int64
var iqDiscoItemsXMLQuery, iqDiscoInfoXMLQuery *etree.Element
// Get file size // Get file size
fileInfo, err := os.Stat(filePath) fileInfo, err := os.Stat(filePath)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
fileSize := fileInfo.Size() fileSize := fileInfo.Size()
// Read file // Read file
buffer, err := readFile(filePath) buffer, err := readFile(filePath)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
// Get mime type // Get mime type
@ -50,24 +46,24 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
// Get file name // Get file name
fileName := filepath.Base(filePath) fileName := filepath.Base(filePath)
// Just use alphanumerical and some special characters for now // Just use alphanumerical and some special characters for now
// to work around https://github.com/xmppo/go-xmpp/issues/132 // to work around https://github.com/mattn/go-xmpp/issues/132
reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`) reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`)
fileNameEscaped := reg.ReplaceAllString(fileName, "_") fileNameEscaped := reg.ReplaceAllString(fileName, "_")
// Query server for disco#items // Query server for disco#items
iqContent, err := sendIQ(client, iqc, jserver, "get", iqContent, err := sendIQ(client, iqc, jserver, "get",
"<query xmlns='http://jabber.org/protocol/disco#items'/>") "<query xmlns='http://jabber.org/protocol/disco#items'/>")
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
iqDiscoItemsXML := etree.NewDocument() iqDiscoItemsXML := etree.NewDocument()
err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query) err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
iqDiscoItemsXMLQuery = iqDiscoItemsXML.SelectElement("query") iqDiscoItemsXMLQuery := iqDiscoItemsXML.SelectElement("query")
if iqDiscoItemsXMLQuery == nil { if iqDiscoItemsXMLQuery == nil {
return "", fmt.Errorf("http-upload: no query element in disco items reply") log.Fatal("no query element in disco items reply")
} }
iqDiscoItemsXMLItems := iqDiscoItemsXMLQuery.SelectElements("item") iqDiscoItemsXMLItems := iqDiscoItemsXMLQuery.SelectElements("item")
@ -79,22 +75,22 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query") iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query")
iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo) iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo)
iqdi, err := iqDiscoInfoReqXML.WriteToString() iqdi, err := iqDiscoInfoReqXML.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi) iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
if iqDiscoInfo.Type != strResult { if iqDiscoInfo.Type != strResult {
continue continue
} }
iqDiscoInfoXML := etree.NewDocument() iqDiscoInfoXML := etree.NewDocument()
err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query) err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
iqDiscoInfoXMLQuery = iqDiscoInfoXML.SelectElement("query") iqDiscoInfoXMLQuery := iqDiscoInfoXML.SelectElement("query")
if iqDiscoInfoXMLQuery == nil { if iqDiscoInfoXMLQuery == nil {
continue continue
} }
@ -113,13 +109,12 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
if iqDiscoInfoXMLType.Value == "file" && if iqDiscoInfoXMLType.Value == "file" &&
iqDiscoInfoXMLCategory.Value == "store" { iqDiscoInfoXMLCategory.Value == "store" {
uploadComponent = jid.Value uploadComponent = jid.Value
break
} }
} }
if uploadComponent == "" { if uploadComponent == "" {
return "", fmt.Errorf("http-upload: no http upload component found") log.Fatal("No http upload component found.")
} }
iqDiscoInfoXMLX := iqDiscoInfoXMLQuery.SelectElements("x") iqDiscoInfoXMLX := iqDiscoItemsXMLQuery.SelectElements("x")
for _, r := range iqDiscoInfoXMLX { for _, r := range iqDiscoInfoXMLX {
field := r.SelectElements("field") field := r.SelectElements("field")
for i, t := range field { for i, t := range field {
@ -127,23 +122,18 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
if varAttr == nil { if varAttr == nil {
continue continue
} }
prevFieldVal := field[i-1].SelectElement("value")
if prevFieldVal == nil {
continue
}
curFieldVal := t.SelectElement("value") curFieldVal := t.SelectElement("value")
if curFieldVal == nil { if curFieldVal == nil {
continue continue
} }
if varAttr.Value == "max-file-size" { if varAttr.Value == "max-file-size" && prevFieldVal.Text() == nsHTTPUpload {
var prevFieldVal *etree.Element maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 10, 64)
if i > 0 { if errors.Unwrap(err) != nil {
prevFieldVal = field[i-1].SelectElement("value") log.Fatal("error while checking server maximum http upload file size.")
if prevFieldVal == nil {
continue
}
}
if prevFieldVal.Text() == nsHTTPUpload {
maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 10, 64)
if err != nil {
return "", fmt.Errorf("http-upload: error while checking server maximum http upload file size")
}
} }
} }
} }
@ -153,8 +143,9 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
// the best. // the best.
if maxFileSize != 0 { if maxFileSize != 0 {
if fileSize > maxFileSize { if fileSize > maxFileSize {
return "", fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB)", log.Fatal("File size " + strconv.FormatInt(fileSize/1024/1024, 10) +
strconv.FormatInt(fileSize/1024/1024, 10), strconv.FormatInt(maxFileSize/1024/1024, 10)) " MB is larger than the maximum file size allowed (" +
strconv.FormatInt(maxFileSize/1024/1024, 10) + " MB).")
} }
} }
@ -166,56 +157,42 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
requestReq.CreateAttr("size", fmt.Sprint(fileSize)) requestReq.CreateAttr("size", fmt.Sprint(fileSize))
requestReq.CreateAttr("content-type", mimeType) requestReq.CreateAttr("content-type", mimeType)
r, err := request.WriteToString() r, err := request.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
// Request http upload slot // Request http upload slot
uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r) uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
if uploadSlot.Type != strResult { if uploadSlot.Type != strResult {
return "", fmt.Errorf("http-upload: error while requesting upload slot") log.Fatal("error while requesting upload slot.")
} }
iqHTTPUploadSlotXML := etree.NewDocument() iqHTTPUploadSlotXML := etree.NewDocument()
err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query) err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot") iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot")
if iqHTTPUploadSlotXMLSlot == nil { if iqHTTPUploadSlotXMLSlot == nil {
return "", fmt.Errorf("http-upload: no slot element") log.Fatal("http-upload: no slot element")
} }
iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put") iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put")
if iqHTTPUploadSlotXMLPut == nil { if iqHTTPUploadSlotXMLPut == nil {
return "", fmt.Errorf("http-upload: no put element") log.Fatal("http-upload: no put element")
} }
iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url") iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url")
if iqHTTPUploadSlotXMLPutURL == nil { if iqHTTPUploadSlotXMLPutURL == nil {
return "", fmt.Errorf("http-upload: no url attribute") log.Fatal("http-upload: no url attribute")
}
if !strings.HasPrefix(iqHTTPUploadSlotXMLPutURL.Value, "https://") {
return "", fmt.Errorf("http-upload: upload slot does not provide https")
} }
// Upload file // Upload file
httpTransport := &http.Transport{ httpClient := &http.Client{}
IdleConnTimeout: timeout,
TLSHandshakeTimeout: timeout,
}
proxyEnv := os.Getenv("HTTP_PROXY")
if proxyEnv != "" {
proxyURL, err := url.Parse(proxyEnv)
if err != nil {
return "", err
}
httpTransport.Proxy = http.ProxyURL(proxyURL)
}
httpClient := &http.Client{Transport: httpTransport}
req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value, req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value,
buffer) buffer)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
req.Header.Set("Content-Type", mimeTypeEscaped.String()) req.Header.Set("Content-Type", mimeTypeEscaped.String())
iqHTTPUploadSlotXMLPutHeaders := iqHTTPUploadSlotXMLPut.SelectElements("header") iqHTTPUploadSlotXMLPutHeaders := iqHTTPUploadSlotXMLPut.SelectElements("header")
@ -230,26 +207,26 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
} }
} }
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if errors.Unwrap(err) != nil {
return "", err log.Fatal(err)
} }
// Test for http status code "200 OK" or "201 Created" // Test for http status code "200 OK" or "201 Created"
if resp.StatusCode != 200 && resp.StatusCode != 201 { if resp.StatusCode != 200 && resp.StatusCode != 201 {
return "", fmt.Errorf("http-upload: upload failed") log.Fatal("Http upload failed.")
} }
// Return http link // Return http link
iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get") iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get")
if iqHTTPUploadSlotXMLGet == nil { if iqHTTPUploadSlotXMLGet == nil {
return "", fmt.Errorf("http-upload: no get element") log.Fatal("http-upload: no get element")
} }
iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url") iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url")
if iqHTTPUploadSlotXMLGetURL == nil { if iqHTTPUploadSlotXMLGetURL == nil {
return "", fmt.Errorf("http-upload: no url attribute") log.Fatal("http-upload: no url attribute")
} }
err = resp.Body.Close() err = resp.Body.Close()
if err != nil { if errors.Unwrap(err) != nil {
fmt.Println("http-upload: error while closing http request body:", err) fmt.Println("error while closing http request body:", err)
} }
return iqHTTPUploadSlotXMLGetURL.Value, nil return iqHTTPUploadSlotXMLGetURL.Value
} }

@ -8,7 +8,7 @@
package main package main
import ( import (
"fmt" "errors"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
) )
@ -35,7 +35,7 @@ func MarshalJID(input string) (string, error) {
} else { } else {
// If the resource part exists, make sure it isn't empty. // If the resource part exists, make sure it isn't empty.
if sep == len(s)-1 { if sep == len(s)-1 {
return input, fmt.Errorf("invalid JID %s: the resourcepart must be larger than 0 bytes", input) return input, errors.New("Invalid JID" + input + ": The resourcepart must be larger than 0 bytes")
} }
resourcepart = s[sep+1:] resourcepart = s[sep+1:]
s = s[:sep] s = s[:sep]
@ -52,7 +52,7 @@ func MarshalJID(input string) (string, error) {
domainpart = s domainpart = s
case sep == 0: case sep == 0:
// The JID starts with an @ sign (invalid empty localpart) // The JID starts with an @ sign (invalid empty localpart)
err = fmt.Errorf("invalid JID: %s", input) err = errors.New("Invalid JID:" + input)
return input, err return input, err
default: default:
domainpart = s[sep+1:] domainpart = s[sep+1:]
@ -73,11 +73,11 @@ func MarshalJID(input string) (string, error) {
var jid string var jid string
if !utf8.ValidString(localpart) || !utf8.ValidString(domainpart) || !utf8.ValidString(resourcepart) { if !utf8.ValidString(localpart) || !utf8.ValidString(domainpart) || !utf8.ValidString(resourcepart) {
return input, fmt.Errorf("invalid JID: %s", input) return input, errors.New("Invalid JID: " + input)
} }
if domainpart == "" { if domainpart == "" {
return input, fmt.Errorf("invalid JID: %s", input) return input, errors.New("Invalid JID: " + input)
} }
if localpart == "" { if localpart == "" {

@ -6,24 +6,19 @@ package main
import ( import (
"bufio" "bufio"
"context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"net" "net"
"os" "os"
"os/signal"
osUser "os/user"
"runtime"
"strings" "strings"
"time" "time"
gopenpgpConst "github.com/ProtonMail/gopenpgp/v2/constants" // MIT License "github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License "github.com/mattn/go-xmpp" // BSD-3-Clause
"github.com/pborman/getopt/v2" // BSD-3-Clause "github.com/pborman/getopt/v2" // BSD-3-Clause
"github.com/xmppo/go-xmpp" // BSD-3-Clause
"salsa.debian.org/mdosch/xmppsrv" // BSD-2-Clause
) )
type configuration struct { type configuration struct {
@ -31,17 +26,10 @@ type configuration struct {
jserver string jserver string
port string port string
password string password string
resource string
alias string alias string
} }
func closeAndExit(client *xmpp.Client, err error) {
client.Close()
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}
func readMessage(messageFilePath string) (string, error) { func readMessage(messageFilePath string) (string, error) {
var ( var (
output string output string
@ -50,16 +38,15 @@ func readMessage(messageFilePath string) (string, error) {
// Check that message file is existing. // Check that message file is existing.
_, err = os.Stat(messageFilePath) _, err = os.Stat(messageFilePath)
if err != nil { if os.IsNotExist(err) {
return output, fmt.Errorf("readMessage: %w", err) return output, fmt.Errorf("readMessage: %w", err)
} }
// Open message file. // Open message file.
file, err := os.Open(messageFilePath) file, err := os.Open(messageFilePath)
if err != nil { if errors.Unwrap(err) != nil {
return output, fmt.Errorf("readMessage: %w", err) return output, fmt.Errorf("readMessage: %w", err)
} }
defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
for scanner.Scan() { for scanner.Scan() {
@ -70,13 +57,18 @@ func readMessage(messageFilePath string) (string, error) {
} }
} }
if err = scanner.Err(); err != nil { if err := scanner.Err(); errors.Unwrap(err) != nil {
if err != io.EOF { if errors.Unwrap(err) != io.EOF {
return "", fmt.Errorf("readMessage: %w", err) return "", fmt.Errorf("readMessage: %w", err)
} }
} }
return output, nil err = file.Close()
if errors.Unwrap(err) != nil {
fmt.Println("error while closing file:", err)
}
return output, fmt.Errorf("readMessage: %w", err)
} }
func main() { func main() {
@ -86,15 +78,10 @@ func main() {
} }
var ( var (
err error err error
message, user, server, password, alias string message, user, server, password, resource, alias string
oxPrivKey *crypto.Key oxPrivKey *crypto.Key
recipients []recipientsType recipients []recipientsType
fast xmpp.Fast
// There are some errors that we ignore as we do not want to
// stop the execution. Failure is used to track those to exit
// with a non-success return value.
failure error
) )
// Define command line flags. // Define command line flags.
@ -106,6 +93,8 @@ func main() {
flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.") flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.")
flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.") flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.")
flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.") flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
flagResource := getopt.StringLong("resource", 'r', "", "Set resource. "+
"When sending to a chatroom this is used as 'alias'. DEPRECATED: Use --alias instead.")
flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+ flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+
"for chatrooms.") "for chatrooms.")
flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+ flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+
@ -135,10 +124,6 @@ func main() {
flagOxDeleteNodes := getopt.BoolLong("ox-delete-nodes", 0, "Delete existing OpenPGP nodes on the server.") 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.") 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.") 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.")
flagSSDPOff := getopt.BoolLong("ssdp-off", 0, "Disable XEP-0474: SASL SCRAM Downgrade Protection.")
flagSubject := getopt.StringLong("subject", 's', "", "Set message subject.")
flagFastOff := getopt.BoolLong("fast-off", 0, "Disable XEP-0484: Fast Authentication Streamlining Tokens.")
// Parse command line flags. // Parse command line flags.
getopt.Parse() getopt.Parse()
@ -150,11 +135,7 @@ func main() {
os.Exit(0) os.Exit(0)
case *flagVersion: case *flagVersion:
// If requested, show version and quit. // If requested, show version and quit.
fmt.Println("Go-sendxmpp", version) fmt.Println("go-sendxmpp", version)
fmt.Println("Xmppsrv library version:", xmppsrv.Version)
fmt.Println("Gopenpgp library version:", gopenpgpConst.Version)
system := runtime.GOOS + "/" + runtime.GOARCH
fmt.Println("System:", system, runtime.Version())
fmt.Println("License: BSD-2-clause") fmt.Println("License: BSD-2-clause")
os.Exit(0) os.Exit(0)
// Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like // Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like
@ -171,35 +152,10 @@ func main() {
" http upload.") " http upload.")
case *flagOx && *flagOOBFile != "": case *flagOx && *flagOOBFile != "":
log.Fatal("No encryption possible for OOB data.") log.Fatal("No encryption possible for OOB data.")
case *flagOx && *flagHeadline:
log.Fatal("No Ox support for headline messages.")
case *flagHeadline && *flagChatroom: case *flagHeadline && *flagChatroom:
log.Fatal("Can't use message type headline for groupchat messages.") log.Fatal("Can't use message type headline for groupchat messages.")
} }
// Print a warning if go-sendxmpp is run by the user root on non-windows systems.
if runtime.GOOS != "windows" {
// Get the current user.
currUser, err := osUser.Current()
if err != nil {
log.Fatal("Failed to get current user: ", err)
}
if currUser.Username == "root" {
fmt.Println("WARNING: It seems you are running go-sendxmpp as root user.\n" +
"This is is not recommended as go-sendxmpp does not require root " +
"privileges. Please consider using a less privileged user. For an " +
"example how to do this with sudo please consult the manpage chapter " +
"TIPS.")
}
}
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. // 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 // 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). // when sending raw messages to MUCs (go-sendxmpp will join the MUC automatically).
@ -214,13 +170,14 @@ func main() {
if *flagUser == "" || *flagPassword == "" { if *flagUser == "" || *flagPassword == "" {
// Read configuration from file. // Read configuration from file.
config, err := parseConfig(*flagFile) config, err := parseConfig(*flagFile)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal("Error parsing ", *flagFile, ": ", err) log.Fatal("Error parsing ", *flagFile, ": ", err)
} }
// Set connection options according to config. // Set connection options according to config.
user = config.username user = config.username
server = config.jserver server = config.jserver
password = config.password password = config.password
resource = config.resource
alias = config.alias alias = config.alias
if config.port != "" { if config.port != "" {
server = net.JoinHostPort(server, fmt.Sprint(config.port)) server = net.JoinHostPort(server, fmt.Sprint(config.port))
@ -241,24 +198,16 @@ func main() {
if *flagPassword != "" { if *flagPassword != "" {
password = *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 { switch {
// Use the resource as alias if no alias is specified otherwise but a resource is specified.
// Backward compat, will be removed when resource is removed.
case alias == "" && *flagAlias == "" && *flagResource != "":
alias = *flagResource
// Use the resource as alias if no alias is specified otherwise but a resource is specified.
// Backward compat, will be removed when resource is removed.
case alias == "" && *flagAlias == "" && resource != "":
alias = resource
// Use "go-sendxmpp" if no nick is specified via config or command line flag. // Use "go-sendxmpp" if no nick is specified via config or command line flag.
case alias == "" && *flagAlias == "": case alias == "" && *flagAlias == "":
alias = "go-sendxmpp" alias = "go-sendxmpp"
@ -267,29 +216,23 @@ func main() {
alias = *flagAlias alias = *flagAlias
} }
// Timeout // Overwrite resource if specified via command line flag.
timeout := time.Duration(*flagTimeout) * time.Second if *flagResource != "" {
fmt.Println("Deprecated flag: --resource.")
clientID, err := getClientID(user) resource = *flagResource
if err != nil { } else if resource == "" {
fmt.Println(err) // Use "go-sendxmpp" plus a random string if no other resource is specified.
resource = "go-sendxmpp." + getShortID()
} }
if !*flagFastOff { // Timeout
fast, _ = getFastData(user, password) timeout := time.Duration(*flagTimeout) * time.Second
// Reset FAST token and mechanism if expired.
if time.Now().After(fast.Expiry) {
fast.Token = ""
fast.Mechanism = ""
}
}
// Use ALPN // Use ALPN
var tlsConfig tls.Config var tlsConfig tls.Config
tlsConfig.ServerName = user[strings.Index(user, "@")+1:] tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client") tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
tlsConfig.InsecureSkipVerify = *flagSkipVerify tlsConfig.InsecureSkipVerify = *flagSkipVerify
tlsConfig.Renegotiation = tls.RenegotiateNever
switch *flagTLSMinVersion { switch *flagTLSMinVersion {
case defaultTLS10: case defaultTLS10:
tlsConfig.MinVersion = tls.VersionTLS10 tlsConfig.MinVersion = tls.VersionTLS10
@ -304,8 +247,6 @@ func main() {
os.Exit(0) os.Exit(0)
} }
resource := "go-sendxmpp." + getShortID()
// Set XMPP connection options. // Set XMPP connection options.
options := xmpp.Options{ options := xmpp.Options{
Host: server, Host: server,
@ -318,24 +259,17 @@ func main() {
// an unencrypted connection is established. As StartTLS is // an unencrypted connection is established. As StartTLS is
// set when NoTLS is set go-sendxmpp won't use unencrypted // set when NoTLS is set go-sendxmpp won't use unencrypted
// client-to-server connections. // client-to-server connections.
// See https://pkg.go.dev/github.com/xmppo/go-xmpp#Options // See https://pkg.go.dev/github.com/mattn/go-xmpp#Options
NoTLS: !*flagDirectTLS, NoTLS: !*flagDirectTLS,
StartTLS: !*flagDirectTLS, StartTLS: !*flagDirectTLS,
Debug: *flagDebug, Debug: *flagDebug,
TLSConfig: &tlsConfig, TLSConfig: &tlsConfig,
Mechanism: *flagSCRAMPinning,
SSDP: !*flagSSDPOff,
UserAgentSW: resource,
UserAgentID: clientID,
Fast: !*flagFastOff,
FastToken: fast.Token,
FastMechanism: fast.Mechanism,
} }
// Read message from file. // Read message from file.
if *flagMessageFile != "" { if *flagMessageFile != "" {
message, err = readMessage(*flagMessageFile) message, err = readMessage(*flagMessageFile)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -356,7 +290,7 @@ func main() {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
if err != io.EOF { if errors.Unwrap(err) != io.EOF {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -373,51 +307,22 @@ func main() {
// Connect to server. // Connect to server.
client, err := connect(options, *flagDirectTLS) client, err := connect(options, *flagDirectTLS)
if err != nil { if errors.Unwrap(err) != nil {
if fast.Token != "" { log.Fatal(err)
// Reset FAST token and mechanism if FAST login failed.
fast.Token = ""
fast.Mechanism = ""
fast.Expiry = time.Now()
err := writeFastData(user, password, fast)
if err != nil {
fmt.Println(err)
}
options.FastToken = ""
// Try to connect to server without FAST.
client, err = connect(options, *flagDirectTLS)
if err != nil {
log.Fatal(err)
}
} else {
log.Fatal(err)
}
} }
// Update fast token if a new one is received or expiry time is reduced.
if (client.Fast.Token != "" && client.Fast.Token != fast.Token) ||
(client.Fast.Expiry.Before(fast.Expiry) && !client.Fast.Expiry.IsZero()) {
fast.Token = client.Fast.Token
fast.Mechanism = client.Fast.Mechanism
fast.Expiry = client.Fast.Expiry
err := writeFastData(user, password, fast)
if err != nil {
fmt.Println(err)
}
}
iqc := make(chan xmpp.IQ, defaultBufferSize) iqc := make(chan xmpp.IQ, defaultBufferSize)
msgc := make(chan xmpp.Chat, defaultBufferSize) msgc := make(chan xmpp.Chat, defaultBufferSize)
ctx, cancel := context.WithCancel(context.Background()) go rcvStanzas(client, iqc, msgc)
go rcvStanzas(client, ctx, iqc, msgc)
for _, r := range getopt.Args() { for _, r := range getopt.Args() {
var re recipientsType var re recipientsType
re.Jid = r re.Jid = r
if *flagOx { if *flagOx {
re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r) re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r)
if err != nil { if errors.Unwrap(err) != nil {
re.OxKeyRing = nil re.OxKeyRing = nil
fmt.Printf("Ox: error fetching key for %s: %v\n", r, err) fmt.Println("ox: error fetching key for", r+":", err)
failure = err
} }
} }
recipients = append(recipients, re) recipients = append(recipients, re)
@ -426,9 +331,8 @@ func main() {
// Check that all recipient JIDs are valid. // Check that all recipient JIDs are valid.
for i, recipient := range recipients { for i, recipient := range recipients {
validatedJid, err := MarshalJID(recipient.Jid) validatedJid, err := MarshalJID(recipient.Jid)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
recipients[i].Jid = validatedJid recipients[i].Jid = validatedJid
} }
@ -436,73 +340,59 @@ func main() {
switch { switch {
case *flagOxGenPrivKeyX25519: case *flagOxGenPrivKeyX25519:
validatedOwnJid, err := MarshalJID(user) validatedOwnJid, err := MarshalJID(user)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519") err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
os.Exit(0) os.Exit(0)
case *flagOxGenPrivKeyRSA: case *flagOxGenPrivKeyRSA:
validatedOwnJid, err := MarshalJID(user) validatedOwnJid, err := MarshalJID(user)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa") err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
os.Exit(0) os.Exit(0)
case *flagOxImportPrivKey != "": case *flagOxImportPrivKey != "":
validatedOwnJid, err := MarshalJID(user) validatedOwnJid, err := MarshalJID(user)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey, err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
client, iqc) client, iqc)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
os.Exit(0) os.Exit(0)
case *flagOxDeleteNodes: case *flagOxDeleteNodes:
validatedOwnJid, err := MarshalJID(user) validatedOwnJid, err := MarshalJID(user)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
err = oxDeleteNodes(validatedOwnJid, client, iqc) err = oxDeleteNodes(validatedOwnJid, client, iqc)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
os.Exit(0) os.Exit(0)
case *flagOx: case *flagOx:
validatedOwnJid, err := MarshalJID(user) validatedOwnJid, err := MarshalJID(user)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase) oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
} }
if *flagHTTPUpload != "" { if *flagHTTPUpload != "" {
message, err = httpUpload(client, iqc, tlsConfig.ServerName, message = httpUpload(client, iqc, tlsConfig.ServerName,
*flagHTTPUpload, timeout) *flagHTTPUpload)
if err != nil {
cancel()
closeAndExit(client, err)
}
} }
if *flagOOBFile != "" { if *flagOOBFile != "" {
@ -510,9 +400,8 @@ func main() {
message = validUTF8(*flagOOBFile) message = validUTF8(*flagOOBFile)
// Check if the URI is valid. // Check if the URI is valid.
uri, err := validURI(message) uri, err := validURI(message)
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
message = uri.String() message = uri.String()
} }
@ -534,9 +423,8 @@ func main() {
} else { } else {
_, err = client.JoinMUCNoHistory(recipient.Jid, alias) _, err = client.JoinMUCNoHistory(recipient.Jid, alias)
} }
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
} }
} }
@ -546,40 +434,19 @@ func main() {
break break
} }
// Send raw XML // Send raw XML
_, err = client.SendOrg(message) _, err = client.SendOrg(message + "\n")
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
case *flagInteractive: case *flagInteractive:
// Send in endless loop (for usage with e.g. "tail -f"). // Send in endless loop (for usage with e.g. "tail -f").
reader := bufio.NewReader(os.Stdin) 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()
if failure != nil {
closeAndExit(client, failure)
}
closeAndExit(client, nil)
}
}()
for { for {
message, err = reader.ReadString('\n') message, err = reader.ReadString('\n')
if err != nil {
select {
case <-ctx.Done():
return
default:
if err != nil {
cancel()
closeAndExit(client, fmt.Errorf("failed to read from stdin"))
}
}
}
message = strings.TrimSuffix(message, "\n") message = strings.TrimSuffix(message, "\n")
if errors.Unwrap(err) != nil {
log.Fatal("failed to read from stdin")
}
// Remove invalid code points. // Remove invalid code points.
message = validUTF8(message) message = validUTF8(message)
@ -593,51 +460,35 @@ func main() {
continue continue
} }
oxMessage, err := oxEncrypt(client, oxPrivKey, oxMessage, err := oxEncrypt(client, oxPrivKey,
recipient.Jid, *recipient.OxKeyRing, message, *flagSubject) recipient.Jid, recipient.OxKeyRing, message)
if err != nil { if errors.Unwrap(err) != nil {
fmt.Printf("Ox: couldn't encrypt to %s: %v\n", fmt.Println("Ox: couldn't encrypt to",
recipient.Jid, err) recipient.Jid)
failure = err
continue continue
} }
_, err = client.SendOrg(oxMessage) _, err = client.SendOrg(oxMessage + "\n")
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
default: default:
_, err = client.Send(xmpp.Chat{ _, err = client.Send(xmpp.Chat{
Remote: recipient.Jid, Remote: recipient.Jid,
Type: msgType, Text: message, Type: msgType, Text: message,
Subject: *flagSubject,
}) })
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
} }
} }
} }
case *flagListen: case *flagListen:
tz := time.Now().Location() tz := time.Now().Location()
// Quit if ^C is pressed.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cancel()
if failure != nil {
closeAndExit(client, failure)
}
closeAndExit(client, nil)
}
}()
for { for {
v := <-msgc v := <-msgc
switch { switch {
case isOxMsg(v) && *flagOx: case isOxMsg(v) && *flagOx:
msg, t, err := oxDecrypt(v, client, iqc, user, oxPrivKey) msg, t, err := oxDecrypt(v, client, iqc, user, oxPrivKey)
if err != nil { if errors.Unwrap(err) != nil {
log.Println(err) log.Println(err)
continue continue
} }
@ -709,25 +560,18 @@ func main() {
_, err = client.Send(xmpp.Chat{ _, err = client.Send(xmpp.Chat{
Remote: recipient.Jid, Remote: recipient.Jid,
Type: msgType, Ooburl: message, Text: message, Type: msgType, Ooburl: message, Text: message,
Subject: *flagSubject,
}) })
if err != nil { if errors.Unwrap(err) != nil {
fmt.Println("Couldn't send message to", fmt.Println("Couldn't send message to",
recipient.Jid) recipient.Jid)
} }
// (Hopefully) temporary workaround due to go-xmpp choking on URL encoding. // (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. // Once this is fixed in the lib the http-upload case above can be reused.
case *flagOOBFile != "": case *flagOOBFile != "":
var msg string _, err = client.SendOrg("<message to='" + recipient.Jid + "' type='" +
if *flagSubject != "" { msgType + "'><body>" + message + "</body><x xmlns='jabber:x:oob'><url>" +
msg = fmt.Sprintf("<message to='%s' type='%s'><subject>%s</subject><body>%s</body><x xmlns='jabber:x:oob'><url>%s</url></x></message>", message + "</url></x></message>")
recipient.Jid, msgType, *flagSubject, message, message) if errors.Unwrap(err) != nil {
} else {
msg = fmt.Sprintf("<message to='%s' type='%s'><body>%s</body><x xmlns='jabber:x:oob'><url>%s</url></x></message>",
recipient.Jid, msgType, message, message)
}
_, err = client.SendOrg(msg)
if err != nil {
fmt.Println("Couldn't send message to", fmt.Println("Couldn't send message to",
recipient.Jid) recipient.Jid)
} }
@ -736,34 +580,29 @@ func main() {
continue continue
} }
oxMessage, err := oxEncrypt(client, oxPrivKey, oxMessage, err := oxEncrypt(client, oxPrivKey,
recipient.Jid, *recipient.OxKeyRing, message, *flagSubject) recipient.Jid, recipient.OxKeyRing, message)
if err != nil { if errors.Unwrap(err) != nil {
fmt.Printf("Ox: couldn't encrypt to %s: %v\n", fmt.Println("Ox: couldn't encrypt to", recipient.Jid)
recipient.Jid, err)
failure = err
continue continue
} }
_, err = client.SendOrg(oxMessage) _, err = client.SendOrg(oxMessage + "\n")
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
default: default:
_, err = client.Send(xmpp.Chat{ _, err = client.Send(xmpp.Chat{
Remote: recipient.Jid, Remote: recipient.Jid,
Type: msgType, Text: message, Type: msgType, Text: message,
Subject: *flagSubject,
}) })
if err != nil { if errors.Unwrap(err) != nil {
cancel() log.Fatal(err)
closeAndExit(client, err)
} }
} }
} }
} }
cancel()
if failure != nil { // Wait for a short time as some messages are not delivered by the server
closeAndExit(client, failure) // if the connection is closed immediately after sending a message.
} time.Sleep(defaultSleepTime * time.Millisecond)
closeAndExit(client, nil) client.Close()
} }

@ -1,12 +1,12 @@
.\" generated with Ronn-NG/v0.9.1 .\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "GO\-SENDXMPP" "1" "June 2024" "" .TH "GO\-SENDXMPP" "1" "June 2023" ""
.SH "NAME" .SH "NAME"
\fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\. \fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\.
.SH "SYNOPSIS" .SH "SYNOPSIS"
\fBgo\-sendxmpp [\-cdilnt] [\-a value] [\-\-fast\-off] [\-f value] [\-\-headline] [\-\-help] [\-h value] [\-j value] [\-m value] [\-\-muc\-password value] [\-\-oob\-file value] [\-\-ox] [\-\-ox\-delete\-nodes] [\-\-ox\-genprivkey\-rsa] [\-\-ox\-genprivkey\-x25519] [\-\-ox\-import\-privkey value] [\-\-ox\-passphrase value] [\-p value] [\-\-raw] [\-\-scram\-mech\-pinning value] [\-\-ssdp\-off] [\-s value] [\-\-timeout value] [\-\-tls\-version value] [\-u value] [\-\-version] [recipients…]\fR \fBgo\-sendxmpp\fR [\-cdintx] [\-f value] [\-\-help] [\-j value] [\-m value] [\-p value] [\-r value] [\-u value] [parameters \|\.\|\.\|\.]
.SH "DESCRIPTION" .SH "DESCRIPTION"
A tool to send messages to an XMPP contact or MUC inspired by \fBsendxmpp\fR\. A tool to send messages to an XMPP contact or MUC inspired by (but not as powerful as) \fBsendxmpp\fR\.
.br .br
You can either pipe a programs output to \fBgo\-sendxmpp\fR, write in your terminal (put \fB^D\fR in a new line to finish) or send the input from a file (\fB\-m\fR or \fB\-\-message\fR)\. The account data is expected at \fB~/\.config/go\-sendxmpp/config\fR (preferred), \fB~/\.config/go\-sendxmpp/sendxmpprc\fR (deprecated) \fB~/\.sendxmpprc\fR (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with \fB\-f\fR or \fB\-\-file\fR\. You can either pipe a programs output to \fBgo\-sendxmpp\fR, write in your terminal (put \fB^D\fR in a new line to finish) or send the input from a file (\fB\-m\fR or \fB\-\-message\fR)\. The account data is expected at \fB~/\.config/go\-sendxmpp/config\fR (preferred), \fB~/\.config/go\-sendxmpp/sendxmpprc\fR (deprecated) \fB~/\.sendxmpprc\fR (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with \fB\-f\fR or \fB\-\-file\fR\.
.SH "OPTIONS" .SH "OPTIONS"
@ -20,9 +20,6 @@ Send message to a chatroom\.
\fB\-d\fR, \fB\-\-debug\fR \fB\-d\fR, \fB\-\-debug\fR
Show debugging info\. Show debugging info\.
.TP .TP
\fB\-\-fast\-off\fR
Disable XEP\-0484: Fast Authentication Streamlining Tokens\.
.TP
\fB\-f\fR, \fB\-\-file\fR=[\fIvalue\fR] \fB\-f\fR, \fB\-\-file\fR=[\fIvalue\fR]
Set configuration file\. (Default: ~/\.config/go\-sendxmpp/config) Set configuration file\. (Default: ~/\.config/go\-sendxmpp/config)
.TP .TP
@ -80,20 +77,17 @@ Import an existing private OpenPGP key\.
\fB\-\-ox\-passphrase\fR=[\fIvalue\fR] \fB\-\-ox\-passphrase\fR=[\fIvalue\fR]
Passphrase for locking and unlocking the private OpenPGP key\. Passphrase for locking and unlocking the private OpenPGP key\.
.TP .TP
\fB\-\-tls\-version\fR=[\fIvalue\fR]
Minimal TLS version\. 10 (TLSv1\.0), 11 (TLSv1\.1), 12 (TLSv1\.2), 13 (TLSv1\.3) (Default: 12)
.TP
\fB\-p\fR, \fB\-\-password\fR=[\fIvalue\fR] \fB\-p\fR, \fB\-\-password\fR=[\fIvalue\fR]
Password for XMPP account\. Password for XMPP account\.
.TP .TP
\fB\-\-raw\fR \fB\-\-raw\fR
Send raw XML\. To send raw XML to a contact as normal chat message no contact must be specified\. To send raw XML to a MUC you have to specify the MUC via \fB\-c\fR and go\-sendxmpp will join the MUC\. Send raw XML\. To send raw XML to a contact as normal chat message no contact must be specified\. To send raw XML to a MUC you have to specify the MUC via \fB\-c\fR and go\-sendxmpp will join the MUC\.
.TP .TP
\fB\-\-scram\-mech\-pinning=[<value>]\fR \fB\-r\fR, \fB\-\-resource\fR=[\fIvalue\fR]
Enforce the use of a certain SCRAM authentication mechanism\. Currently go\-sendxmpp supports \fBSCRAM\-SHA\-1\fR, \fBSCRAM\-SHA\-1\-PLUS\fR, \fBSCRAM\-SHA\-256\fR, \fBSCRAM\-SHA\-256\-PLUS\fR, \fBSCRAM\-SHA\-512\fR and \fBSCRAM\-SHA\-512\-PLUS\fR\. You should know what you are doing when using this setting and make sure the chosen mechanism is supported by the server\. If not set, go\-sendxmpp will use XEP\-0474 to prevent downgrade attacks (needs server support)\. DEPRECATED: Set resource\. When sending to a chatroom this is used as 'alias'\.
.TP
\fB\-\-ssdp\-off\fR
Disable XEP\-0474: SASL SCRAM Downgrade Protection\.
.TP
\fB\-s\fR, \fB\-\-subject\fR=[\fIvalue\fR]
Set message subject\.
.TP .TP
\fB\-\-timeout=\fR[\fIvalue\fR] \fB\-\-timeout=\fR[\fIvalue\fR]
Connection timeout in seconds\. (Default: 10) Connection timeout in seconds\. (Default: 10)
@ -101,26 +95,11 @@ Connection timeout in seconds\. (Default: 10)
\fB\-t\fR, \fB\-\-tls\fR \fB\-t\fR, \fB\-\-tls\fR
Use direct TLS\. Use direct TLS\.
.TP .TP
\fB\-\-tls\-version\fR=[\fIvalue\fR]
Minimal TLS version\. 10 (TLSv1\.0), 11 (TLSv1\.1), 12 (TLSv1\.2), 13 (TLSv1\.3) (Default: 12)
.TP
\fB\-u\fR, \fB\-\-username\fR=[\fIvalue\fR] \fB\-u\fR, \fB\-\-username\fR=[\fIvalue\fR]
Username for XMPP account (JID)\. Username for XMPP account (JID)\.
.TP .TP
\fB\-\-version\fR \fB\-\-version\fR
Show version information\. Show version information\.
.SH "ENVIRONMENT VARIABLES"
.SS "HTTP_PROXY"
A SOCKS5 proxy can be used by setting the environment variable \fBHTTP_PROXY\fR\. This feature is considered experimental and there is no guarantee that there won't be any connections not using the proxy although it didn't happen during testing\.
.P
\fBHTTP_PROXY="socks5://127\.0\.0\.1:9050" go\-sendxmpp \-\-http\-upload file\.txt user@example\.org\fR
.SH "TIPS"
.SS "USAGE BY ROOT"
In general it's a good advice to only perform commands as root when it is strictly necessary\. To be able to send the output from commands, that need to be performed as root, with go\-sendxmpp without invoking go\-sendxmpp by root sudo can be used\.
.P
In this example there is a user \fBsendxmpp\fR with a go\-sendxmpp config in its \fB$HOME\fR:
.P
\fB# command\-that\-requires\-root|sudo \-H \-u sendxmpp go\-sendxmpp me@example\.org\fR
.SH "SHELL COMPLETIONS" .SH "SHELL COMPLETIONS"
.SS "ZSH" .SS "ZSH"
There are no shell completions yet (contributions welcome) but for zsh it is possible to automatically create completions from \fB\-\-help\fR which might work good enough\. There are no shell completions yet (contributions welcome) but for zsh it is possible to automatically create completions from \fB\-\-help\fR which might work good enough\.
@ -128,10 +107,6 @@ There are no shell completions yet (contributions welcome) but for zsh it is pos
Just place the following in your \fB~/\.zshrc\fR or \fB~/\.zshrc\.local\fR: Just place the following in your \fB~/\.zshrc\fR or \fB~/\.zshrc\.local\fR:
.P .P
\fBcompdef _gnu_generic go\-sendxmpp\fR \fBcompdef _gnu_generic go\-sendxmpp\fR
.SS "FISH"
There are no shell completions yet, but FISH can generate them from the man page with following command:
.P
\fBfish_update_completions\fR
.SH "CHAT" .SH "CHAT"
Feel free to join \fIhttps://join\.jabber\.network/#go\-sendxmpp@chat\.mdosch\.de?join\fR\. Feel free to join \fIhttps://join\.jabber\.network/#go\-sendxmpp@chat\.mdosch\.de?join\fR\.
.SH "AUTHOR" .SH "AUTHOR"

@ -57,8 +57,6 @@
<a href="#SYNOPSIS">SYNOPSIS</a> <a href="#SYNOPSIS">SYNOPSIS</a>
<a href="#DESCRIPTION">DESCRIPTION</a> <a href="#DESCRIPTION">DESCRIPTION</a>
<a href="#OPTIONS">OPTIONS</a> <a href="#OPTIONS">OPTIONS</a>
<a href="#ENVIRONMENT-VARIABLES">ENVIRONMENT VARIABLES</a>
<a href="#TIPS">TIPS</a>
<a href="#SHELL-COMPLETIONS">SHELL COMPLETIONS</a> <a href="#SHELL-COMPLETIONS">SHELL COMPLETIONS</a>
<a href="#CHAT">CHAT</a> <a href="#CHAT">CHAT</a>
<a href="#AUTHOR">AUTHOR</a> <a href="#AUTHOR">AUTHOR</a>
@ -81,14 +79,11 @@
</p> </p>
<h2 id="SYNOPSIS">SYNOPSIS</h2> <h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>go-sendxmpp [-cdilnt] [-a value] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [-m value] [--muc-password value] <p><code>go-sendxmpp</code> [-cdintx] [-f value] [--help] [-j value] [-m value] [-p value] [-r value] [-u value] [parameters ...]</p>
[--oob-file value] [--ox] [--ox-delete-nodes] [--ox-genprivkey-rsa] [--ox-genprivkey-x25519] [--ox-import-privkey value]
[--ox-passphrase value] [-p value] [--raw] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--timeout value]
[--tls-version value] [-u value] [--version] [recipients…]</code></p>
<h2 id="DESCRIPTION">DESCRIPTION</h2> <h2 id="DESCRIPTION">DESCRIPTION</h2>
<p>A tool to send messages to an XMPP contact or MUC inspired by <code>sendxmpp</code>. <br> <p>A tool to send messages to an XMPP contact or MUC inspired by (but not as powerful as) <code>sendxmpp</code>. <br>
You can either pipe a programs output to <code>go-sendxmpp</code>, write in your terminal (put <code>^D</code> in a new line to You can either pipe a programs output to <code>go-sendxmpp</code>, write in your terminal (put <code>^D</code> in a new line to
finish) or send the input from a file (<code>-m</code> or <code>--message</code>). finish) or send the input from a file (<code>-m</code> or <code>--message</code>).
The account data is expected at <code>~/.config/go-sendxmpp/config</code> (preferred), <code>~/.config/go-sendxmpp/sendxmpprc</code> The account data is expected at <code>~/.config/go-sendxmpp/config</code> (preferred), <code>~/.config/go-sendxmpp/sendxmpprc</code>
@ -108,8 +103,6 @@ file location is specified with <code>-f</code> or <code>--file</code>.</p>
<code>-d</code>, <code>--debug</code> <code>-d</code>, <code>--debug</code>
</dt> </dt>
<dd>Show debugging info.</dd> <dd>Show debugging info.</dd>
<dt><code>--fast-off</code></dt>
<dd>Disable XEP-0484: Fast Authentication Streamlining Tokens.</dd>
<dt> <dt>
<code>-f</code>, <code>--file</code>=[<var>value</var>]</dt> <code>-f</code>, <code>--file</code>=[<var>value</var>]</dt>
<dd>Set configuration file. (Default: ~/.config/go-sendxmpp/config)</dd> <dd>Set configuration file. (Default: ~/.config/go-sendxmpp/config)</dd>
@ -180,22 +173,17 @@ it might be imported using <code>--ox-import-privkey</code>.</dd>
<code>--ox-passphrase</code>=[<var>value</var>]</dt> <code>--ox-passphrase</code>=[<var>value</var>]</dt>
<dd>Passphrase for locking and unlocking the private OpenPGP key.</dd> <dd>Passphrase for locking and unlocking the private OpenPGP key.</dd>
<dt> <dt>
<code>--tls-version</code>=[<var>value</var>]</dt>
<dd>Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2), 13 (TLSv1.3) (Default: 12)</dd>
<dt>
<code>-p</code>, <code>--password</code>=[<var>value</var>]</dt> <code>-p</code>, <code>--password</code>=[<var>value</var>]</dt>
<dd>Password for XMPP account.</dd> <dd>Password for XMPP account.</dd>
<dt><code>--raw</code></dt> <dt><code>--raw</code></dt>
<dd>Send raw XML. To send raw XML to a contact as normal chat message no contact must be specified. <dd>Send raw XML. To send raw XML to a contact as normal chat message no contact must be specified.
To send raw XML to a MUC you have to specify the MUC via <code>-c</code> and go-sendxmpp will join the MUC.</dd> To send raw XML to a MUC you have to specify the MUC via <code>-c</code> and go-sendxmpp will join the MUC.</dd>
<dt><code>--scram-mech-pinning=[&lt;value&gt;]</code></dt>
<dd>Enforce the use of a certain SCRAM authentication mechanism. Currently go-sendxmpp supports
<strong>SCRAM-SHA-1</strong>, <strong>SCRAM-SHA-1-PLUS</strong>, <strong>SCRAM-SHA-256</strong>, <strong>SCRAM-SHA-256-PLUS</strong>, <strong>SCRAM-SHA-512</strong>
and <strong>SCRAM-SHA-512-PLUS</strong>. You should know what you are doing when using this setting and
make sure the chosen mechanism is supported by the server. If not set, go-sendxmpp will use XEP-0474
to prevent downgrade attacks (needs server support).</dd>
<dt><code>--ssdp-off</code></dt>
<dd>Disable XEP-0474: SASL SCRAM Downgrade Protection.</dd>
<dt> <dt>
<code>-s</code>, <code>--subject</code>=[<var>value</var>]</dt> <code>-r</code>, <code>--resource</code>=[<var>value</var>]</dt>
<dd>Set message subject.</dd> <dd>DEPRECATED: Set resource. When sending to a chatroom this is used as 'alias'.</dd>
<dt> <dt>
<code>--timeout=</code>[<var>value</var>]</dt> <code>--timeout=</code>[<var>value</var>]</dt>
<dd>Connection timeout in seconds. (Default: 10)</dd> <dd>Connection timeout in seconds. (Default: 10)</dd>
@ -204,41 +192,12 @@ to prevent downgrade attacks (needs server support).</dd>
</dt> </dt>
<dd>Use direct TLS.</dd> <dd>Use direct TLS.</dd>
<dt> <dt>
<code>--tls-version</code>=[<var>value</var>]</dt>
<dd>Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2), 13 (TLSv1.3) (Default: 12)</dd>
<dt>
<code>-u</code>, <code>--username</code>=[<var>value</var>]</dt> <code>-u</code>, <code>--username</code>=[<var>value</var>]</dt>
<dd>Username for XMPP account (JID).</dd> <dd>Username for XMPP account (JID).</dd>
<dt><code>--version</code></dt> <dt><code>--version</code></dt>
<dd>Show version information.</dd> <dd>Show version information.</dd>
</dl> </dl>
<h2 id="ENVIRONMENT-VARIABLES">ENVIRONMENT VARIABLES</h2>
<h3 id="HTTP_PROXY">HTTP_PROXY</h3>
<p>A SOCKS5 proxy can be used by setting the environment variable <code>HTTP_PROXY</code>. This feature is considered experimental
and there is no guarantee that there won't be any connections not using the proxy although it didn't happen during
testing.</p>
<p><code>
HTTP_PROXY="socks5://127.0.0.1:9050" go-sendxmpp --http-upload file.txt user@example.org
</code></p>
<h2 id="TIPS">TIPS</h2>
<h3 id="USAGE-BY-ROOT">USAGE BY ROOT</h3>
<p>In general it's a good advice to only perform commands as root when it is strictly necessary. To be able to send
the output from commands, that need to be performed as root, with go-sendxmpp without invoking go-sendxmpp by
root sudo can be used.</p>
<p>In this example there is a user <strong>sendxmpp</strong> with a go-sendxmpp config in its <code>$HOME</code>:</p>
<p><code>
# command-that-requires-root|sudo -H -u sendxmpp go-sendxmpp me@example.org
</code></p>
<h2 id="SHELL-COMPLETIONS">SHELL COMPLETIONS</h2> <h2 id="SHELL-COMPLETIONS">SHELL COMPLETIONS</h2>
<h3 id="ZSH">ZSH</h3> <h3 id="ZSH">ZSH</h3>
@ -253,14 +212,6 @@ good enough.</p>
compdef _gnu_generic go-sendxmpp compdef _gnu_generic go-sendxmpp
</code></p> </code></p>
<h3 id="FISH">FISH</h3>
<p>There are no shell completions yet, but FISH can generate them from the man page with following command:</p>
<p><code>
fish_update_completions
</code></p>
<h2 id="CHAT">CHAT</h2> <h2 id="CHAT">CHAT</h2>
<p>Feel free to join <a href="https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join" data-bare-link="true">https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join</a>.</p> <p>Feel free to join <a href="https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join" data-bare-link="true">https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join</a>.</p>
@ -284,7 +235,7 @@ License: BSD 2-clause License</p>
<ol class='man-decor man-foot man foot'> <ol class='man-decor man-foot man foot'>
<li class='tl'></li> <li class='tl'></li>
<li class='tc'>June 2024</li> <li class='tc'>June 2023</li>
<li class='tr'>go-sendxmpp(1)</li> <li class='tr'>go-sendxmpp(1)</li>
</ol> </ol>

@ -3,14 +3,11 @@ go-sendxmpp(1) -- A tool to send messages to an XMPP contact or MUC.
## SYNOPSIS ## SYNOPSIS
`go-sendxmpp [-cdilnt] [-a value] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [-m value] [--muc-password value] `go-sendxmpp` [-cdintx] [-f value] [--help] [-j value] [-m value] [-p value] [-r value] [-u value] [parameters ...]
[--oob-file value] [--ox] [--ox-delete-nodes] [--ox-genprivkey-rsa] [--ox-genprivkey-x25519] [--ox-import-privkey value]
[--ox-passphrase value] [-p value] [--raw] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--timeout value]
[--tls-version value] [-u value] [--version] [recipients…]`
## DESCRIPTION ## DESCRIPTION
A tool to send messages to an XMPP contact or MUC inspired by `sendxmpp`. A tool to send messages to an XMPP contact or MUC inspired by (but not as powerful as) `sendxmpp`.
You can either pipe a programs output to `go-sendxmpp`, write in your terminal (put `^D` in a new line to You can either pipe a programs output to `go-sendxmpp`, write in your terminal (put `^D` in a new line to
finish) or send the input from a file (`-m` or `--message`). finish) or send the input from a file (`-m` or `--message`).
The account data is expected at `~/.config/go-sendxmpp/config` (preferred), `~/.config/go-sendxmpp/sendxmpprc` The account data is expected at `~/.config/go-sendxmpp/config` (preferred), `~/.config/go-sendxmpp/sendxmpprc`
@ -28,9 +25,6 @@ Send message to a chatroom.
* `-d`, `--debug`: * `-d`, `--debug`:
Show debugging info. Show debugging info.
* `--fast-off`:
Disable XEP-0484: Fast Authentication Streamlining Tokens.
* `-f`, `--file`=[<value>]: * `-f`, `--file`=[<value>]:
Set configuration file. (Default: ~/.config/go-sendxmpp/config) Set configuration file. (Default: ~/.config/go-sendxmpp/config)
@ -103,6 +97,9 @@ Import an existing private OpenPGP key.
* `--ox-passphrase`=[<value>]: * `--ox-passphrase`=[<value>]:
Passphrase for locking and unlocking the private OpenPGP key. Passphrase for locking and unlocking the private OpenPGP key.
* `--tls-version`=[<value>]:
Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2), 13 (TLSv1.3) (Default: 12)
* `-p`, `--password`=[<value>]: * `-p`, `--password`=[<value>]:
Password for XMPP account. Password for XMPP account.
@ -110,18 +107,8 @@ Password for XMPP account.
Send raw XML. To send raw XML to a contact as normal chat message no contact must be specified. Send raw XML. To send raw XML to a contact as normal chat message no contact must be specified.
To send raw XML to a MUC you have to specify the MUC via `-c` and go-sendxmpp will join the MUC. To send raw XML to a MUC you have to specify the MUC via `-c` and go-sendxmpp will join the MUC.
* `--scram-mech-pinning=[<value>]`: * `-r`, `--resource`=[<value>]:
Enforce the use of a certain SCRAM authentication mechanism. Currently go-sendxmpp supports DEPRECATED: Set resource. When sending to a chatroom this is used as 'alias'.
**SCRAM-SHA-1**, **SCRAM-SHA-1-PLUS**, **SCRAM-SHA-256**, **SCRAM-SHA-256-PLUS**, **SCRAM-SHA-512**
and **SCRAM-SHA-512-PLUS**. You should know what you are doing when using this setting and
make sure the chosen mechanism is supported by the server. If not set, go-sendxmpp will use XEP-0474
to prevent downgrade attacks (needs server support).
* `--ssdp-off`:
Disable XEP-0474: SASL SCRAM Downgrade Protection.
* `-s`, `--subject`=[<value>]:
Set message subject.
* `--timeout=`[<value>]: * `--timeout=`[<value>]:
Connection timeout in seconds. (Default: 10) Connection timeout in seconds. (Default: 10)
@ -129,42 +116,12 @@ Connection timeout in seconds. (Default: 10)
* `-t`, `--tls`: * `-t`, `--tls`:
Use direct TLS. Use direct TLS.
* `--tls-version`=[<value>]:
Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2), 13 (TLSv1.3) (Default: 12)
* `-u`, `--username`=[<value>]: * `-u`, `--username`=[<value>]:
Username for XMPP account (JID). Username for XMPP account (JID).
* `--version`: * `--version`:
Show version information. Show version information.
## ENVIRONMENT VARIABLES
### HTTP_PROXY
A SOCKS5 proxy can be used by setting the environment variable `HTTP_PROXY`. This feature is considered experimental
and there is no guarantee that there won't be any connections not using the proxy although it didn't happen during
testing.
```
HTTP_PROXY="socks5://127.0.0.1:9050" go-sendxmpp --http-upload file.txt user@example.org
```
## TIPS
### USAGE BY ROOT
In general it's a good advice to only perform commands as root when it is strictly necessary. To be able to send
the output from commands, that need to be performed as root, with go-sendxmpp without invoking go-sendxmpp by
root sudo can be used.
In this example there is a user **sendxmpp** with a go-sendxmpp config in its `$HOME`:
```
# command-that-requires-root|sudo -H -u sendxmpp go-sendxmpp me@example.org
```
## SHELL COMPLETIONS ## SHELL COMPLETIONS
### ZSH ### ZSH
@ -179,14 +136,6 @@ Just place the following in your `~/.zshrc` or `~/.zshrc.local`:
compdef _gnu_generic go-sendxmpp compdef _gnu_generic go-sendxmpp
``` ```
### FISH
There are no shell completions yet, but FISH can generate them from the man page with following command:
```
fish_update_completions
```
## CHAT ## CHAT
Feel free to join [https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join](https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join). Feel free to join [https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join](https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join).

@ -1,10 +1,10 @@
.\" generated with Ronn-NG/v0.9.1 .\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "GO\-SENDXMPP" "5" "October 2023" "" .TH "GO\-SENDXMPP" "5" "May 2023" ""
.SH "NAME" .SH "NAME"
\fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\. \fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\.
.SH "LOCATION" .SH "LOCATION"
The account data is expected at \fB~/\.config/go\-sendxmpp/config\fR (preferred), \fB~/\.config/go\-sendxmpp/sendxmpprc\fR (deprecated) or \fB~/\.sendxmpprc\fR (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with \-f or \-\-file\. The configuration file is expected to be in the following format: The account data is expected at \fB~/\.config/go\-sendxmpp/config\fR (preferred), \fB~/\.config/go\-sendxmpp/sendxmpprc\fR deprecated) or \fB~/\.sendxmpprc\fR (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with \-f or \-\-file\. The configuration file is expected to be in the following format:
.SH "FORMAT" .SH "FORMAT"
username: [\fIyour_jid\fR] username: [\fIyour_jid\fR]
.br .br

@ -78,7 +78,7 @@
<h2 id="LOCATION">LOCATION</h2> <h2 id="LOCATION">LOCATION</h2>
<p>The account data is expected at <code>~/.config/go-sendxmpp/config</code> (preferred), <code>~/.config/go-sendxmpp/sendxmpprc</code> <p>The account data is expected at <code>~/.config/go-sendxmpp/config</code> (preferred), <code>~/.config/go-sendxmpp/sendxmpprc</code>
(deprecated) or <code>~/.sendxmpprc</code> (for compatibility with the original perl sendxmpp) if no other configuration file deprecated) or <code>~/.sendxmpprc</code> (for compatibility with the original perl sendxmpp) if no other configuration file
location is specified with -f or --file. The configuration file is expected to be in the following format:</p> location is specified with -f or --file. The configuration file is expected to be in the following format:</p>
<h2 id="FORMAT">FORMAT</h2> <h2 id="FORMAT">FORMAT</h2>
@ -112,11 +112,11 @@ License: BSD 2-clause License</p>
<h2 id="SEE-ALSO">SEE ALSO</h2> <h2 id="SEE-ALSO">SEE ALSO</h2>
<p><span class="man-ref">go-sendxmpp<span class="s">(1)</span></span>, <span class="man-ref">sendxmpp<span class="s">(1)</span></span></p> <p><a class="man-ref" href="go-sendxmpp.1.html">go-sendxmpp<span class="s">(1)</span></a>, <span class="man-ref">sendxmpp<span class="s">(1)</span></span></p>
<ol class='man-decor man-foot man foot'> <ol class='man-decor man-foot man foot'>
<li class='tl'></li> <li class='tl'></li>
<li class='tc'>October 2023</li> <li class='tc'>May 2023</li>
<li class='tr'>go-sendxmpp(5)</li> <li class='tr'>go-sendxmpp(5)</li>
</ol> </ol>

@ -4,7 +4,7 @@ go-sendxmpp(5) -- A tool to send messages to an XMPP contact or MUC.
## LOCATION ## LOCATION
The account data is expected at `~/.config/go-sendxmpp/config` (preferred), `~/.config/go-sendxmpp/sendxmpprc` The account data is expected at `~/.config/go-sendxmpp/config` (preferred), `~/.config/go-sendxmpp/sendxmpprc`
(deprecated) or `~/.sendxmpprc` (for compatibility with the original perl sendxmpp) if no other configuration file deprecated) or `~/.sendxmpprc` (for compatibility with the original perl sendxmpp) if no other configuration file
location is specified with -f or --file. The configuration file is expected to be in the following format: location is specified with -f or --file. The configuration file is expected to be in the following format:
## FORMAT ## FORMAT

301
ox.go

@ -6,6 +6,7 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -15,7 +16,7 @@ import (
"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License "github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
"github.com/beevik/etree" // BSD-2-clause "github.com/beevik/etree" // BSD-2-clause
"github.com/xmppo/go-xmpp" // BSD-3-Clause "github.com/mattn/go-xmpp" // BSD-3-Clause
) )
func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error { func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error {
@ -24,25 +25,25 @@ func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error {
query := nodeListRequest.CreateElement("query") query := nodeListRequest.CreateElement("query")
query.CreateAttr("xmlns", nsDiscoItems) query.CreateAttr("xmlns", nsDiscoItems)
nlr, err := nodeListRequest.WriteToString() nlr, err := nodeListRequest.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxDeleteNodes: failed to create node list request %w", err) return fmt.Errorf("oxDeleteNodes: failed to create node list request %w", err)
} }
iqReply, err := sendIQ(client, iqc, jid, "get", nlr) iqReply, err := sendIQ(client, iqc, jid, "get", nlr)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxDeleteNodes: failure with node list request iq: %w", err) return fmt.Errorf("oxDeleteNodes: failure with node list request iq: %w", err)
} }
nodeListReply := etree.NewDocument() nodeListReply := etree.NewDocument()
err = nodeListReply.ReadFromBytes(iqReply.Query) err = nodeListReply.ReadFromBytes(iqReply.Query)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxDeleteNodes: failed to read node list reply iq: %w", err) return fmt.Errorf("oxDeleteNodes: failed to read node list reply iq: %w", err)
} }
query = nodeListReply.SelectElement("query") query = nodeListReply.SelectElement("query")
if query == nil { if query == nil {
return fmt.Errorf("error parsing iq reply") return errors.New("error parsing iq reply")
} }
items := query.SelectElements("item") items := query.SelectElements("item")
if items == nil { if items == nil {
return fmt.Errorf("error parsing iq reply") return errors.New("error parsing iq reply")
} }
for _, item := range items { for _, item := range items {
node := item.SelectAttr("node") node := item.SelectAttr("node")
@ -59,11 +60,11 @@ func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error {
del := pubsub.CreateElement("delete") del := pubsub.CreateElement("delete")
del.CreateAttr("node", node.Value) del.CreateAttr("node", node.Value)
dnr, err := deleteNodeRequest.WriteToString() dnr, err := deleteNodeRequest.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
continue continue
} }
_, err = sendIQ(client, iqc, jid, "set", dnr) _, err = sendIQ(client, iqc, jid, "set", dnr)
if err != nil { if errors.Unwrap(err) != nil {
continue continue
} }
} }
@ -77,7 +78,7 @@ func oxDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string,
for _, r := range m.OtherElem { for _, r := range m.OtherElem {
if r.XMLName.Space == nsOx { if r.XMLName.Space == nsOx {
cryptMsgByte, err = base64.StdEncoding.DecodeString(r.InnerXML) cryptMsgByte, err = base64.StdEncoding.DecodeString(r.InnerXML)
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
break break
@ -85,54 +86,54 @@ func oxDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string,
} }
oxMsg := crypto.NewPGPMessage(cryptMsgByte) oxMsg := crypto.NewPGPMessage(cryptMsgByte)
keyRing, err := crypto.NewKeyRing(oxPrivKey) keyRing, err := crypto.NewKeyRing(oxPrivKey)
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
senderKeyRing, err := oxGetPublicKeyRing(client, iqc, sender) senderKeyRing, err := oxGetPublicKeyRing(client, iqc, sender)
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
decryptMsg, err := keyRing.Decrypt(oxMsg, senderKeyRing, crypto.GetUnixTime()) decryptMsg, err := keyRing.Decrypt(oxMsg, senderKeyRing, crypto.GetUnixTime())
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
// Remove invalid code points. // Remove invalid code points.
message := validUTF8(string(decryptMsg.Data)) message := validUTF8(string(decryptMsg.Data))
doc := etree.NewDocument() doc := etree.NewDocument()
err = doc.ReadFromString(message) err = doc.ReadFromString(message)
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
signcrypt := doc.SelectElement("signcrypt") signcrypt := doc.SelectElement("signcrypt")
if signcrypt == nil { if signcrypt == nil {
return strError, time.Now(), fmt.Errorf("ox: no signcrypt element") return strError, time.Now(), errors.New("ox: no signcrypt element")
} }
to := signcrypt.SelectElement("to") to := signcrypt.SelectElement("to")
if to == nil { if to == nil {
return strError, time.Now(), fmt.Errorf("ox: no to element") return strError, time.Now(), errors.New("ox: no to element")
} }
jid := to.SelectAttr("jid") jid := to.SelectAttr("jid")
if jid == nil { if jid == nil {
return strError, time.Now(), fmt.Errorf("ox: no jid attribute") return strError, time.Now(), errors.New("ox: no jid attribute")
} }
if strings.Split(jid.Value, "/")[0] != user { if strings.Split(jid.Value, "/")[0] != user {
return strError, time.Now(), fmt.Errorf("ox: encrypted for wrong user") return strError, time.Now(), errors.New("ox: encrypted for wrong user")
} }
timestamp := signcrypt.SelectElement("time") timestamp := signcrypt.SelectElement("time")
if timestamp == nil { if timestamp == nil {
return strError, time.Now(), fmt.Errorf("ox: no time element") return strError, time.Now(), errors.New("ox: no time element")
} }
stamp := timestamp.SelectAttr("stamp") stamp := timestamp.SelectAttr("stamp")
if stamp == nil { if stamp == nil {
return strError, time.Now(), fmt.Errorf("ox: no stamp attribute") return strError, time.Now(), errors.New("ox: no stamp attribute")
} }
msgStamp, err := time.Parse("2006-01-02T15:04:05Z0700", stamp.Value) msgStamp, err := time.Parse("2006-01-02T15:04:05Z0700", stamp.Value)
if err != nil { if errors.Unwrap(err) != nil {
return strError, time.Now(), err return strError, time.Now(), err
} }
payload := signcrypt.SelectElement("payload") payload := signcrypt.SelectElement("payload")
if payload == nil { if payload == nil {
return strError, time.Now(), fmt.Errorf("ox: no payload element") return strError, time.Now(), errors.New("ox: no payload element")
} }
body := payload.SelectElement("body") body := payload.SelectElement("body")
if body == nil { if body == nil {
@ -153,58 +154,58 @@ func isOxMsg(m xmpp.Chat) bool {
func oxImportPrivKey(jid string, privKeyLocation string, client *xmpp.Client, iqc chan xmpp.IQ) error { func oxImportPrivKey(jid string, privKeyLocation string, client *xmpp.Client, iqc chan xmpp.IQ) error {
xmppURI := "xmpp:" + jid xmppURI := "xmpp:" + jid
buffer, err := readFile(privKeyLocation) buffer, err := readFile(privKeyLocation)
if err != nil { if errors.Unwrap(err) != nil {
return err return err
} }
key, err := crypto.NewKey(buffer.Bytes()) key, err := crypto.NewKey(buffer.Bytes())
if err != nil { if errors.Unwrap(err) != nil {
key, err = crypto.NewKeyFromArmored(buffer.String()) key, err = crypto.NewKeyFromArmored(buffer.String())
if err != nil { if errors.Unwrap(err) != nil {
keyDecoded, err := base64.StdEncoding.DecodeString(buffer.String()) keyDecoded, err := base64.StdEncoding.DecodeString(buffer.String())
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to import private key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to import private key: %w", err)
} }
key, err = crypto.NewKey(keyDecoded) key, err = crypto.NewKey(keyDecoded)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to import private key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to import private key: %w", err)
} }
} }
} }
entity := key.GetEntity() entity := key.GetEntity()
if entity.Identities[xmppURI] == nil { if entity.Identities[xmppURI] == nil {
return fmt.Errorf("key identity is not %s", xmppURI) return errors.New("Key identity is not " + xmppURI)
} }
pk, err := key.GetPublicKey() pk, err := key.GetPublicKey()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to get public key associated to private key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to get public key associated to private key: %w", err)
} }
pubKey, err := crypto.NewKey(pk) pubKey, err := crypto.NewKey(pk)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to get public key associated to private key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to get public key associated to private key: %w", err)
} }
fingerprint := strings.ToUpper(pubKey.GetFingerprint()) fingerprint := strings.ToUpper(pubKey.GetFingerprint())
_, err = oxRecvPublicKeys(client, iqc, jid, fingerprint) _, err = oxRecvPublicKeys(client, iqc, jid, fingerprint)
if err != nil { if errors.Unwrap(err) != nil {
err = oxPublishPubKey(jid, client, iqc, pubKey) err = oxPublishPubKey(jid, client, iqc, pubKey)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to publish public key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to publish public key: %w", err)
} }
} }
location, err := oxGetPrivKeyLoc(jid) location, err := oxGetPrivKeyLoc(jid)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to determine private key location: %w", err) return fmt.Errorf("oxImportPrivKey: failed to determine private key location: %w", err)
} }
keySerialized, err := key.Serialize() keySerialized, err := key.Serialize()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to serialize private key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to serialize private key: %w", err)
} }
err = oxStoreKey(location, err = oxStoreKey(location,
base64.StdEncoding.EncodeToString(keySerialized)) base64.StdEncoding.EncodeToString(keySerialized))
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
pubKeyRing, err := oxGetPublicKeyRing(client, iqc, jid) pubKeyRing, err := oxGetPublicKeyRing(client, iqc, jid)
if err == nil { if errors.Unwrap(err) == nil {
pubKeys := pubKeyRing.GetKeys() pubKeys := pubKeyRing.GetKeys()
for _, r := range pubKeys { for _, r := range pubKeys {
if strings.ToUpper(r.GetFingerprint()) == fingerprint { if strings.ToUpper(r.GetFingerprint()) == fingerprint {
@ -213,7 +214,7 @@ func oxImportPrivKey(jid string, privKeyLocation string, client *xmpp.Client, iq
} }
} }
err = oxPublishPubKey(jid, client, iqc, pubKey) err = oxPublishPubKey(jid, client, iqc, pubKey)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to publish public key: %w", err) return fmt.Errorf("oxImportPrivKey: failed to publish public key: %w", err)
} }
return nil return nil
@ -223,7 +224,7 @@ func oxPublishPubKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, pubKey *
keyCreated := time.Now().UTC().Format("2006-01-02T15:04:05Z") keyCreated := time.Now().UTC().Format("2006-01-02T15:04:05Z")
fingerprint := strings.ToUpper(pubKey.GetFingerprint()) fingerprint := strings.ToUpper(pubKey.GetFingerprint())
keySerialized, err := pubKey.Serialize() keySerialized, err := pubKey.Serialize()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: failed to serialize pubkey: %w", err) return fmt.Errorf("oxPublishPubKey: failed to serialize pubkey: %w", err)
} }
pubKeyBase64 := base64.StdEncoding.EncodeToString(keySerialized) pubKeyBase64 := base64.StdEncoding.EncodeToString(keySerialized)
@ -253,27 +254,27 @@ func oxPublishPubKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, pubKey *
value = field.CreateElement("value") value = field.CreateElement("value")
value.CreateText("open") value.CreateText("open")
xmlstring, err := root.WriteToString() xmlstring, err := root.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: failed to create publish public key iq xml: %w", err) return fmt.Errorf("oxPublishPubKey: failed to create publish public key iq xml: %w", err)
} }
iqReply, err := sendIQ(client, iqc, jid, "set", xmlstring) iqReply, err := sendIQ(client, iqc, jid, "set", xmlstring)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: iq failure publishing public key: %w", err) return fmt.Errorf("oxPublishPubKey: iq failure publishing public key: %w", err)
} }
if iqReply.Type != strResult { if iqReply.Type != strResult {
return fmt.Errorf("error while publishing public key") return errors.New("error while publishing public key")
} }
ownPubKeyRingFromPubsub, err := oxRecvPublicKeys(client, iqc, jid, fingerprint) ownPubKeyRingFromPubsub, err := oxRecvPublicKeys(client, iqc, jid, fingerprint)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("couldn't successfully verify public key upload") return errors.New("couldn't successfully verify public key upload")
} }
ownPubKeyFromPubsub := ownPubKeyRingFromPubsub.GetKeys()[0] ownPubKeyFromPubsub := ownPubKeyRingFromPubsub.GetKeys()[0]
ownPubKeyFromPubsubSerialized, err := ownPubKeyFromPubsub.Serialize() ownPubKeyFromPubsubSerialized, err := ownPubKeyFromPubsub.Serialize()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("couldn't successfully verify public key upload") return errors.New("couldn't successfully verify public key upload")
} }
if pubKeyBase64 != base64.StdEncoding.EncodeToString(ownPubKeyFromPubsubSerialized) { if pubKeyBase64 != base64.StdEncoding.EncodeToString(ownPubKeyFromPubsubSerialized) {
return fmt.Errorf("couldn't successfully verify public key upload") return errors.New("couldn't successfully verify public key upload")
} }
root = etree.NewDocument() root = etree.NewDocument()
root.WriteSettings.AttrSingleQuote = true root.WriteSettings.AttrSingleQuote = true
@ -301,59 +302,80 @@ func oxPublishPubKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, pubKey *
value = field.CreateElement("value") value = field.CreateElement("value")
value.CreateText("open") value.CreateText("open")
xmlstring, err = root.WriteToString() xmlstring, err = root.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: failed to create xml for iq to publish public key list: %w", err) return fmt.Errorf("oxPublishPubKey: failed to create xml for iq to publish public key list: %w", err)
} }
iqReply, err = sendIQ(client, iqc, jid, "set", xmlstring) iqReply, err = sendIQ(client, iqc, jid, "set", xmlstring)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: iq failure publishing public key list: %w", err) return fmt.Errorf("oxPublishPubKey: iq failure publishing public key list: %w", err)
} }
if iqReply.Type != strResult { if iqReply.Type != strResult {
return fmt.Errorf("couldn't publish public key list") return errors.New("couldn't publish public key list")
} }
return nil return nil
} }
func oxGetPrivKeyLoc(jid string) (string, error) { func oxGetPrivKeyLoc(jid string) (string, error) {
dataDir, err := getDataPath(strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1)) var err error
if err != nil { var homeDir, dataDir string
return strError, fmt.Errorf("oxGetPrivKeyLoc: %w", err) switch {
} case os.Getenv("$XDG_DATA_HOME") != "":
oldDataDir, err := getDataPath("oxprivkeys/") dataDir = os.Getenv("$XDG_DATA_HOME")
if err != nil { case os.Getenv("$XDG_HOME") != "":
return strError, fmt.Errorf("oxGetPrivKeyLoc: %w", err) homeDir = os.Getenv("$XDG_HOME")
} dataDir = homeDir + "/.local/share"
// TODO: Remove handling of oldDataFile in a later version when it's very likely that there are no case os.Getenv("$HOME") != "":
// more versions in use using the oldDataFile (<0.8.3). homeDir = os.Getenv("$HOME")
oldDataFile := oldDataDir + base64.StdEncoding.EncodeToString([]byte(jid)) dataDir = homeDir + "/.local/share"
oldDataFile2 := oldDataDir + strings.Replace(jid, "@", "_at_", -1) default:
oldDataFile3 := oldDataDir + strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1) homeDir, err = os.UserHomeDir()
dataFile := dataDir + "oxprivkey" if errors.Unwrap(err) != nil {
if _, err := os.Stat(oldDataFile); err == nil { return strError, fmt.Errorf("oxGetPrivKeyLoc: failed to determine user dir: %w", err)
err := os.Rename(oldDataFile, dataFile)
if err != nil {
return dataFile, err
} }
} if homeDir == "" {
if _, err := os.Stat(oldDataFile2); err == nil { return strError, errors.New("oxGetPrivKeyLoc: received empty string for home directory")
err := os.Rename(oldDataFile2, dataFile)
if err != nil {
return dataFile, err
} }
dataDir = homeDir + "/.local/share"
} }
if _, err := os.Stat(oldDataFile3); err == nil { dataDir += "/go-sendxmpp/oxprivkeys/"
err := os.Rename(oldDataFile3, dataFile) if _, err = os.Stat(dataDir); os.IsNotExist(err) {
if err != nil { err = os.MkdirAll(dataDir, defaultDirRights)
return dataFile, err if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxGetPrivKeyLoc: could not create folder for private keys: %w", err)
} }
} }
dataFile := dataDir + base64.StdEncoding.EncodeToString([]byte(jid))
return dataFile, nil return dataFile, nil
} }
func oxGetPubKeyLoc(fingerprint string) (string, error) { func oxGetPubKeyLoc(fingerprint string) (string, error) {
dataDir, err := getDataPath("oxpubkeys/") var err error
if err != nil { var homeDir, dataDir string
return strError, fmt.Errorf("oxGetPubKeyLoc: %w", err) switch {
case os.Getenv("$XDG_DATA_HOME") != "":
dataDir = os.Getenv("$XDG_DATA_HOME")
case os.Getenv("$XDG_HOME") != "":
homeDir = os.Getenv("$XDG_HOME")
dataDir = homeDir + "/.local/share"
case os.Getenv("$HOME") != "":
homeDir = os.Getenv("$HOME")
dataDir = homeDir + "/.local/share"
default:
homeDir, err = os.UserHomeDir()
if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxGetPubKeyLoc: failed to determine user dir: %w", err)
}
if homeDir == "" {
return strError, errors.New("oxGetPubKeyLoc: received empty string for home directory")
}
dataDir = homeDir + "/.local/share"
}
dataDir += "/go-sendxmpp/oxpubkeys/"
if _, err = os.Stat(dataDir); os.IsNotExist(err) {
err = os.MkdirAll(dataDir, defaultDirRights)
if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxGetPubKeyLoc: could not create folder for public keys: %w", err)
}
} }
dataFile := dataDir + fingerprint dataFile := dataDir + fingerprint
return dataFile, nil return dataFile, nil
@ -361,37 +383,37 @@ func oxGetPubKeyLoc(fingerprint string) (string, error) {
func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) { func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
dataFile, err := oxGetPrivKeyLoc(jid) dataFile, err := oxGetPrivKeyLoc(jid)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
keyBuffer, err := readFile(dataFile) keyBuffer, err := readFile(dataFile)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
keyString := keyBuffer.String() keyString := keyBuffer.String()
decodedPrivKey, err := base64.StdEncoding.DecodeString(keyString) decodedPrivKey, err := base64.StdEncoding.DecodeString(keyString)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPrivKey: failed to decode private key: %w", err) return nil, fmt.Errorf("oxGetPrivKey: failed to decode private key: %w", err)
} }
key, err := crypto.NewKey(decodedPrivKey) key, err := crypto.NewKey(decodedPrivKey)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPrivKey: failed to decode private key: %w", err) return nil, fmt.Errorf("oxGetPrivKey: failed to decode private key: %w", err)
} }
if passphrase != "" { if passphrase != "" {
key, err = key.Unlock([]byte(passphrase)) key, err = key.Unlock([]byte(passphrase))
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal("Ox: couldn't unlock private key.") log.Fatal("Ox: couldn't unlock private key.")
} }
} }
isLocked, err := key.IsLocked() isLocked, err := key.IsLocked()
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPrivKey: failed to check whether private key is locked: %w", err) return nil, fmt.Errorf("oxGetPrivKey: failed to check whether private key is locked: %w", err)
} }
if isLocked { if isLocked {
log.Fatal("Ox: private key is locked.") log.Fatal("Ox: private key is locked.")
} }
if key.IsExpired() { if key.IsExpired() {
return nil, fmt.Errorf("ox: private key is expired: %s", key.GetFingerprint()) return nil, errors.New("Ox: private key is expired: " + key.GetFingerprint())
} }
return key, nil return key, nil
} }
@ -399,7 +421,7 @@ func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
func oxStoreKey(location string, key string) error { func oxStoreKey(location string, key string) error {
var file *os.File var file *os.File
file, err := os.Create(location) file, err := os.Create(location)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxStoreKey: failed to create key location: %w", err) return fmt.Errorf("oxStoreKey: failed to create key location: %w", err)
} }
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
@ -408,11 +430,11 @@ func oxStoreKey(location string, key string) error {
_ = file.Chmod(os.FileMode(defaultFileRightsWin)) _ = file.Chmod(os.FileMode(defaultFileRightsWin))
} }
_, err = file.Write([]byte(key)) _, err = file.Write([]byte(key))
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxStoreKey: failed to write key to file: %w", err) return fmt.Errorf("oxStoreKey: failed to write key to file: %w", err)
} }
err = file.Close() err = file.Close()
if err != nil { if errors.Unwrap(err) != nil {
fmt.Println("error while closing file:", err) fmt.Println("error while closing file:", err)
} }
return nil return nil
@ -423,38 +445,38 @@ func oxGenPrivKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ,
) error { ) error {
xmppURI := "xmpp:" + jid xmppURI := "xmpp:" + jid
key, err := crypto.GenerateKey(xmppURI, "", keyType, defaultRSABits) key, err := crypto.GenerateKey(xmppURI, "", keyType, defaultRSABits)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to generate private key: %w", err) return fmt.Errorf("oxGenPrivKey: failed to generate private key: %w", err)
} }
if passphrase != "" { if passphrase != "" {
key, err = key.Lock([]byte(passphrase)) key, err = key.Lock([]byte(passphrase))
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to lock key with passphrase: %w", err) return fmt.Errorf("oxGenPrivKey: failed to lock key with passphrase: %w", err)
} }
} }
keySerialized, err := key.Serialize() keySerialized, err := key.Serialize()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to serialize private key: %w", err) return fmt.Errorf("oxGenPrivKey: failed to serialize private key: %w", err)
} }
location, err := oxGetPrivKeyLoc(jid) location, err := oxGetPrivKeyLoc(jid)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to get private key location: %w", err) return fmt.Errorf("oxGenPrivKey: failed to get private key location: %w", err)
} }
err = oxStoreKey(location, err = oxStoreKey(location,
base64.StdEncoding.EncodeToString(keySerialized)) base64.StdEncoding.EncodeToString(keySerialized))
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
decodedPubKey, err := key.GetPublicKey() decodedPubKey, err := key.GetPublicKey()
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err) return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err)
} }
pubKey, err := crypto.NewKey(decodedPubKey) pubKey, err := crypto.NewKey(decodedPubKey)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err) return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err)
} }
err = oxPublishPubKey(jid, client, iqc, pubKey) err = oxPublishPubKey(jid, client, iqc, pubKey)
if err != nil { if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to publish public key: %w", err) return fmt.Errorf("oxGenPrivKey: failed to publish public key: %w", err)
} }
return nil return nil
@ -469,37 +491,40 @@ func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, f
opkrPsItems.CreateAttr("node", nsOxPubKeys+":"+fingerprint) opkrPsItems.CreateAttr("node", nsOxPubKeys+":"+fingerprint)
opkrPsItems.CreateAttr("max_items", "1") opkrPsItems.CreateAttr("max_items", "1")
opkrString, err := opkr.WriteToString() opkrString, err := opkr.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed to generate xml for public key request: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed to generate xml for public key request: %w", err)
} }
oxPublicKey, err := sendIQ(client, iqc, recipient, "get", opkrString) oxPublicKey, err := sendIQ(client, iqc, recipient, "get", opkrString)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: iq error requesting public keys: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: iq error requesting public keys: %w", err)
} }
if oxPublicKey.Type != strResult { if oxPublicKey.Type != strResult {
return nil, fmt.Errorf("error while requesting public key for %s", return nil, errors.New("error while requesting public key for " +
recipient) recipient)
} }
oxPublicKeyXML := etree.NewDocument() oxPublicKeyXML := etree.NewDocument()
err = oxPublicKeyXML.ReadFromBytes(oxPublicKey.Query) err = oxPublicKeyXML.ReadFromBytes(oxPublicKey.Query)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed parsing iq reply to public key request: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed parsing iq reply to public key request: %w", err)
} }
keyring, err := crypto.NewKeyRing(nil) keyring, err := crypto.NewKeyRing(nil)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed reading public key: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed reading public key: %w", err)
} }
oxPublicKeyXMLPubsub := oxPublicKeyXML.SelectElement("pubsub") oxPublicKeyXMLPubsub := oxPublicKeyXML.SelectElement("pubsub")
if oxPublicKeyXMLPubsub == nil { if oxPublicKeyXMLPubsub == nil {
return nil, fmt.Errorf("ox: no pubsub element in reply to public key request") return nil, errors.New("ox: no pubsub element in reply to public " +
"key request")
} }
oxPublicKeyXMLItems := oxPublicKeyXMLPubsub.SelectElement("items") oxPublicKeyXMLItems := oxPublicKeyXMLPubsub.SelectElement("items")
if oxPublicKeyXMLItems == nil { if oxPublicKeyXMLItems == nil {
return nil, fmt.Errorf("ox: no items element in reply to public key request") return nil, errors.New("ox: no items element in reply to public " +
"key request")
} }
oxPublicKeyXMLItem := oxPublicKeyXMLItems.SelectElement("item") oxPublicKeyXMLItem := oxPublicKeyXMLItems.SelectElement("item")
if oxPublicKeyXMLItem == nil { if oxPublicKeyXMLItem == nil {
return nil, fmt.Errorf("ox: no item element in reply to public key request") return nil, errors.New("ox: no item element in reply to public " +
"key request")
} }
oxPublicKeyXMLPubkeys := oxPublicKeyXMLItem.SelectElements("pubkey") oxPublicKeyXMLPubkeys := oxPublicKeyXMLItem.SelectElements("pubkey")
for _, r := range oxPublicKeyXMLPubkeys { for _, r := range oxPublicKeyXMLPubkeys {
@ -508,18 +533,18 @@ func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, f
continue continue
} }
decodedPubKey, err := base64.StdEncoding.DecodeString(data.Text()) decodedPubKey, err := base64.StdEncoding.DecodeString(data.Text())
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed to decode public key: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed to decode public key: %w", err)
} }
key, err := crypto.NewKey(decodedPubKey) key, err := crypto.NewKey(decodedPubKey)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed to decode public key: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed to decode public key: %w", err)
} }
if key.IsExpired() { if key.IsExpired() {
return nil, fmt.Errorf("key is expired: %s", fingerprint) return nil, errors.New("Key is expired: " + fingerprint)
} }
err = keyring.AddKey(key) err = keyring.AddKey(key)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed adding public key to keyring: %w", err) return nil, fmt.Errorf("oxRecvPublicKeys: failed adding public key to keyring: %w", err)
} }
} }
@ -528,7 +553,7 @@ func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, f
func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string) (*crypto.KeyRing, error) { func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string) (*crypto.KeyRing, error) {
publicKeyRing, err := crypto.NewKeyRing(nil) publicKeyRing, err := crypto.NewKeyRing(nil)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to create a new keyring: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to create a new keyring: %w", err)
} }
@ -540,44 +565,44 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
oxPubKeyListReqPsItems.CreateAttr("node", nsOxPubKeys) oxPubKeyListReqPsItems.CreateAttr("node", nsOxPubKeys)
oxPubKeyListReqPsItems.CreateAttr("max_items", "1") oxPubKeyListReqPsItems.CreateAttr("max_items", "1")
opkl, err := oxPubKeyListReq.WriteToString() opkl, err := oxPubKeyListReq.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
oxPublicKeyList, err := sendIQ(client, iqc, recipient, "get", opkl) oxPublicKeyList, err := sendIQ(client, iqc, recipient, "get", opkl)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
if oxPublicKeyList.Type != strResult { if oxPublicKeyList.Type != strResult {
return nil, fmt.Errorf("error while requesting public openpgp keys for %s", return nil, errors.New("error while requesting public openpgp keys for " +
recipient) recipient)
} }
oxPubKeyListXML := etree.NewDocument() oxPubKeyListXML := etree.NewDocument()
err = oxPubKeyListXML.ReadFromBytes(oxPublicKeyList.Query) err = oxPubKeyListXML.ReadFromBytes(oxPublicKeyList.Query)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse answer to public key list request: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse answer to public key list request: %w", err)
} }
pubKeyRingID := "none" pubKeyRingID := "none"
newestKey, err := time.Parse(time.RFC3339, "1900-01-01T00:00:00Z") newestKey, err := time.Parse(time.RFC3339, "1900-01-01T00:00:00Z")
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to set time for newest key to 1900-01-01: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to set time for newest key to 1900-01-01: %w", err)
} }
oxPubKeyListXMLPubsub := oxPubKeyListXML.SelectElement("pubsub") oxPubKeyListXMLPubsub := oxPubKeyListXML.SelectElement("pubsub")
if oxPubKeyListXMLPubsub == nil { if oxPubKeyListXMLPubsub == nil {
return nil, fmt.Errorf("ox: no pubsub element in public key list") return nil, errors.New("ox: no pubsub element in public key list")
} }
oxPubKeyListXMLPubsubItems := oxPubKeyListXMLPubsub.SelectElement("items") oxPubKeyListXMLPubsubItems := oxPubKeyListXMLPubsub.SelectElement("items")
if oxPubKeyListXMLPubsubItems == nil { if oxPubKeyListXMLPubsubItems == nil {
return nil, fmt.Errorf("ox: no items element in public key list") return nil, errors.New("ox: no items element in public key list")
} }
oxPubKeyListXMLPubsubItemsItem := oxPubKeyListXMLPubsubItems.SelectElement("item") oxPubKeyListXMLPubsubItemsItem := oxPubKeyListXMLPubsubItems.SelectElement("item")
if oxPubKeyListXMLPubsubItemsItem == nil { if oxPubKeyListXMLPubsubItemsItem == nil {
return nil, fmt.Errorf("ox: no item element in public key list") return nil, errors.New("ox: no item element in public key list")
} }
oxPubKeyListXMLPubsubItemsItemPkl := oxPubKeyListXMLPubsubItemsItem.SelectElement("public-keys-list") oxPubKeyListXMLPubsubItemsItemPkl := oxPubKeyListXMLPubsubItemsItem.SelectElement("public-keys-list")
if oxPubKeyListXMLPubsubItemsItemPkl == nil { if oxPubKeyListXMLPubsubItemsItemPkl == nil {
return nil, fmt.Errorf("ox: no public-keys-list element") return nil, errors.New("ox: no public-keys-list element")
} }
oxPubKeyListXMLPubsubItemsItemPklPm := oxPubKeyListXMLPubsubItemsItemPkl.SelectElements("pubkey-metadata") oxPubKeyListXMLPubsubItemsItemPklPm := oxPubKeyListXMLPubsubItemsItemPkl.SelectElements("pubkey-metadata")
for _, r := range oxPubKeyListXMLPubsubItemsItemPklPm { for _, r := range oxPubKeyListXMLPubsubItemsItemPklPm {
@ -590,7 +615,7 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
continue continue
} }
keyDate, err := time.Parse(time.RFC3339, date.Value) keyDate, err := time.Parse(time.RFC3339, date.Value)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse time stamp for key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse time stamp for key: %w", err)
} }
if keyDate.After(newestKey) { if keyDate.After(newestKey) {
@ -599,40 +624,40 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
} }
} }
if pubKeyRingID == "none" { if pubKeyRingID == "none" {
return nil, fmt.Errorf("server didn't provide public key fingerprints for %s", recipient) return nil, errors.New("server didn't provide public key fingerprints for " + recipient)
} }
pubKeyRingLocation, err := oxGetPubKeyLoc(pubKeyRingID) pubKeyRingLocation, err := oxGetPubKeyLoc(pubKeyRingID)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to get public key ring location: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to get public key ring location: %w", err)
} }
pubKeyReadXML := etree.NewDocument() pubKeyReadXML := etree.NewDocument()
err = pubKeyReadXML.ReadFromFile(pubKeyRingLocation) err = pubKeyReadXML.ReadFromFile(pubKeyRingLocation)
if err == nil { if errors.Unwrap(err) == nil {
date := pubKeyReadXML.SelectElement("date") date := pubKeyReadXML.SelectElement("date")
if date != nil { if date != nil {
savedKeysDate, err := time.Parse(time.RFC3339, date.Text()) savedKeysDate, err := time.Parse(time.RFC3339, date.Text())
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse time for saved key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse time for saved key: %w", err)
} }
if !savedKeysDate.Before(newestKey) { if !savedKeysDate.Before(newestKey) {
pubKeys := pubKeyReadXML.SelectElements("pubkey") pubKeys := pubKeyReadXML.SelectElements("pubkey")
if pubKeys == nil { if pubKeys == nil {
return nil, fmt.Errorf("couldn't read public keys from cache") return nil, errors.New("couldn't read public keys from cache")
} }
for _, r := range pubKeys { for _, r := range pubKeys {
keyByte, err := base64.StdEncoding.DecodeString(r.Text()) keyByte, err := base64.StdEncoding.DecodeString(r.Text())
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to decode saved key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to decode saved key: %w", err)
} }
key, err := crypto.NewKey(keyByte) key, err := crypto.NewKey(keyByte)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse saved key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to parse saved key: %w", err)
} }
if !key.IsExpired() { if !key.IsExpired() {
err = publicKeyRing.AddKey(key) err = publicKeyRing.AddKey(key)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to add key to public keyring: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to add key to public keyring: %w", err)
} }
} }
@ -644,7 +669,7 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
} }
} }
pubKeyRing, err := oxRecvPublicKeys(client, iqc, recipient, pubKeyRingID) pubKeyRing, err := oxRecvPublicKeys(client, iqc, recipient, pubKeyRingID)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to get public keyring: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to get public keyring: %w", err)
} }
pubKeySaveXML := etree.NewDocument() pubKeySaveXML := etree.NewDocument()
@ -652,31 +677,31 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
date.SetText(newestKey.Format(time.RFC3339)) date.SetText(newestKey.Format(time.RFC3339))
for _, key := range pubKeyRing.GetKeys() { for _, key := range pubKeyRing.GetKeys() {
keySerialized, err := key.Serialize() keySerialized, err := key.Serialize()
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to serialize key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to serialize key: %w", err)
} }
saveKey := pubKeySaveXML.CreateElement("pubkey") saveKey := pubKeySaveXML.CreateElement("pubkey")
saveKey.SetText(base64.StdEncoding.EncodeToString(keySerialized)) saveKey.SetText(base64.StdEncoding.EncodeToString(keySerialized))
} }
err = pubKeySaveXML.WriteToFile(pubKeyRingLocation) err = pubKeySaveXML.WriteToFile(pubKeyRingLocation)
if err != nil { if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to create xml for saving public key: %w", err) return nil, fmt.Errorf("oxGetPublicKeyRing: failed to create xml for saving public key: %w", err)
} }
return pubKeyRing, nil return pubKeyRing, nil
} }
func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, keyRing crypto.KeyRing, message string, subject string) (string, error) { func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, keyRing *crypto.KeyRing, message string) (string, error) {
if message == "" { if message == "" {
return "", nil return "", nil
} }
privKeyRing, err := crypto.NewKeyRing(oxPrivKey) privKeyRing, err := crypto.NewKeyRing(oxPrivKey)
if err != nil { if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxEncrypt: failed to create private keyring: %w", err) return strError, fmt.Errorf("oxEncrypt: failed to create private keyring: %w", err)
} }
ownJid := strings.Split(client.JID(), "/")[0] ownJid := strings.Split(client.JID(), "/")[0]
if recipient != ownJid { if recipient != ownJid {
opk, err := oxPrivKey.GetPublicKey() opk, err := oxPrivKey.GetPublicKey()
if err == nil { if errors.Unwrap(err) == nil {
ownKey, _ := crypto.NewKey(opk) ownKey, _ := crypto.NewKey(opk)
_ = keyRing.AddKey(ownKey) _ = keyRing.AddKey(ownKey)
} }
@ -692,20 +717,16 @@ func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, key
oxCryptMessageScRpad := oxCryptMessageSc.CreateElement("rpad") oxCryptMessageScRpad := oxCryptMessageSc.CreateElement("rpad")
oxCryptMessageScRpad.CreateText(getRpad(len(message))) oxCryptMessageScRpad.CreateText(getRpad(len(message)))
oxCryptMessageScPayload := oxCryptMessageSc.CreateElement("payload") oxCryptMessageScPayload := oxCryptMessageSc.CreateElement("payload")
if subject != "" {
oxCryptMessageScPayloadSub := oxCryptMessageScPayload.CreateElement("subject")
oxCryptMessageScPayloadSub.CreateText(subject)
}
oxCryptMessageScPayloadBody := oxCryptMessageScPayload.CreateElement("body") oxCryptMessageScPayloadBody := oxCryptMessageScPayload.CreateElement("body")
oxCryptMessageScPayloadBody.CreateAttr("xmlns", nsJabberClient) oxCryptMessageScPayloadBody.CreateAttr("xmlns", nsJabberClient)
oxCryptMessageScPayloadBody.CreateText(message) oxCryptMessageScPayloadBody.CreateText(message)
ocm, err := oxCryptMessage.WriteToString() ocm, err := oxCryptMessage.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxEncrypt: failed to create xml for ox crypt message: %w", err) return strError, fmt.Errorf("oxEncrypt: failed to create xml for ox crypt message: %w", err)
} }
plainMessage := crypto.NewPlainMessage([]byte(ocm)) plainMessage := crypto.NewPlainMessage([]byte(ocm))
pgpMessage, err := keyRing.Encrypt(plainMessage, privKeyRing) pgpMessage, err := keyRing.Encrypt(plainMessage, privKeyRing)
if err != nil { if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxEncrypt: failed to create pgp message: %w", err) return strError, fmt.Errorf("oxEncrypt: failed to create pgp message: %w", err)
} }
om := etree.NewDocument() om := etree.NewDocument()
@ -724,7 +745,7 @@ func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, key
omMessageBody := omMessage.CreateElement("body") omMessageBody := omMessage.CreateElement("body")
omMessageBody.CreateText(oxAltBody) omMessageBody.CreateText(oxAltBody)
oms, err := om.WriteToString() oms, err := om.WriteToString()
if err != nil { if errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxEncrypt: failed to create xml for ox message: %w", err) return strError, fmt.Errorf("oxEncrypt: failed to create xml for ox message: %w", err)
} }

@ -6,6 +6,7 @@ package main
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -19,13 +20,13 @@ import (
func findConfig() (string, error) { func findConfig() (string, error) {
// Get the current user. // Get the current user.
curUser, err := user.Current() curUser, err := user.Current()
if err != nil { if errors.Unwrap(err) != nil {
return "", fmt.Errorf("findConfig: failed to get current user: %w", err) return "", fmt.Errorf("findConfig: failed to get current user: %w", err)
} }
// Get home directory. // Get home directory.
home := curUser.HomeDir home := curUser.HomeDir
if home == "" { if home == "" {
return "", fmt.Errorf("findConfig: no home directory found") return "", errors.New("no home directory found")
} }
osConfigDir := os.Getenv("$XDG_CONFIG_HOME") osConfigDir := os.Getenv("$XDG_CONFIG_HOME")
if osConfigDir == "" { if osConfigDir == "" {
@ -40,11 +41,11 @@ func findConfig() (string, error) {
for _, r := range configFiles { for _, r := range configFiles {
// Check that the config file is existing. // Check that the config file is existing.
_, err := os.Stat(r) _, err := os.Stat(r)
if err == nil { if errors.Unwrap(err) == nil {
return r, nil return r, nil
} }
} }
return "", fmt.Errorf("findConfig: no configuration file found") return "", errors.New("no configuration file found")
} }
// Opens the config file and returns the specified values // Opens the config file and returns the specified values
@ -61,7 +62,7 @@ func parseConfig(configPath string) (configuration, error) {
// Get systems user config path. // Get systems user config path.
if configPath == "" { if configPath == "" {
configPath, err = findConfig() configPath, err = findConfig()
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -69,23 +70,23 @@ func parseConfig(configPath string) (configuration, error) {
// Only check file permissions if we are not running on windows. // Only check file permissions if we are not running on windows.
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
info, err := os.Stat(configPath) info, err := os.Stat(configPath)
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
// Check for file permissions. Must be 600, 640, 440 or 400. // Check for file permissions. Must be 600, 640, 440 or 400.
perm := info.Mode().Perm() perm := info.Mode().Perm()
permissions := strconv.FormatInt(int64(perm), 8) permissions := strconv.FormatInt(int64(perm), 8)
if permissions != "600" && permissions != "640" && permissions != "440" && permissions != "400" { if permissions != "600" && permissions != "640" && permissions != "440" && permissions != "400" {
return output, fmt.Errorf("parseConfig: wrong permissions for %s: %s instead of 400 (recommended), 440, 600 or 640", configPath, permissions) return output, errors.New("Wrong permissions for " + configPath + ": " +
permissions + " instead of 400, 440, 600 or 640.")
} }
} }
// Open config file. // Open config file.
file, err := os.Open(configPath) file, err := os.Open(configPath)
if err != nil { if errors.Unwrap(err) != nil {
return output, fmt.Errorf("parseConfig: failed to open config file: %w", err) return output, fmt.Errorf("parseConfig: failed to open config file: %w", err)
} }
defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@ -95,22 +96,22 @@ func parseConfig(configPath string) (configuration, error) {
continue continue
} }
column := strings.SplitN(scanner.Text(), " ", defaultConfigColumnSep) row := strings.SplitN(scanner.Text(), " ", defaultConfigRowSep)
switch column[0] { switch row[0] {
case "username:": case "username:":
output.username = column[1] output.username = row[1]
case "jserver:": case "jserver:":
output.jserver = column[1] output.jserver = row[1]
case "password:": case "password:":
output.password = column[1] output.password = row[1]
case "eval_password:": case "eval_password:":
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")
if shell == "" { if shell == "" {
shell = "/bin/sh" shell = "/bin/sh"
} }
out, err := exec.Command(shell, "-c", column[1]).Output() out, err := exec.Command(shell, "-c", row[1]).Output()
if err != nil { if errors.Unwrap(err) != nil {
log.Fatal(err) log.Fatal(err)
} }
output.password = string(out) output.password = string(out)
@ -118,41 +119,45 @@ func parseConfig(configPath string) (configuration, error) {
output.password = output.password[:len(output.password)-1] output.password = output.password[:len(output.password)-1]
} }
case "port:": case "port:":
output.port = column[1] output.port = row[1]
case "resource:":
output.resource = row[1]
fmt.Println(configPath+":", "Deprecated option: resource.")
case "alias:": case "alias:":
output.alias = column[1] output.alias = row[1]
default: default:
// Try to parse legacy sendxmpp config files. if len(row) >= defaultConfigRowSep {
if len(column) >= defaultConfigColumnSep {
if strings.Contains(scanner.Text(), ";") { if strings.Contains(scanner.Text(), ";") {
output.username = strings.Split(column[0], ";")[0] output.username = strings.Split(row[0], ";")[0]
output.jserver = strings.Split(column[0], ";")[1] output.jserver = strings.Split(row[0], ";")[1]
output.password = column[1] output.password = row[1]
} else { } else {
output.username = strings.Split(column[0], ":")[0] output.username = strings.Split(row[0], ":")[0]
if strings.Contains(output.username, "@") { jserver := strings.Split(row[0], "@")
jserver := strings.SplitAfter(output.username, "@")[1] if len(jserver) < defaultLenServerConf {
if len(jserver) < defaultLenServerConf { log.Fatal("Couldn't parse config: ", row[0])
log.Fatal("Couldn't parse config: ", column[0])
}
output.jserver = jserver
} }
output.password = column[1] output.jserver = jserver[0]
output.password = row[1]
} }
} }
} }
} }
err = file.Close()
if errors.Unwrap(err) != nil {
fmt.Println("error closing file:", err)
}
// Check if the username is a valid JID // Check if the username is a valid JID
output.username, err = MarshalJID(output.username) output.username, err = MarshalJID(output.username)
if err != nil { if errors.Unwrap(err) != nil {
// Check whether only the local part was used by appending an @ and the // Check whether only the local part was used by appending an @ and the
// server part. // server part.
output.username = output.username + "@" + output.jserver output.username = output.username + "@" + output.jserver
// Check if the username is a valid JID now // Check if the username is a valid JID now
output.username, err = MarshalJID(output.username) output.username, err = MarshalJID(output.username)
if err != nil { if errors.Unwrap(err) != nil {
return output, fmt.Errorf("parseConfig: invalid username/JID: %s", output.username) return output, errors.New("invalid username/JID: " + output.username)
} }
} }

@ -5,32 +5,25 @@
package main package main
import ( import (
"context" "errors"
"fmt" "fmt"
"io"
"log" "log"
"runtime"
"time"
"github.com/beevik/etree" // BSD-2-clause "github.com/beevik/etree" // BSD-2-clause
"github.com/xmppo/go-xmpp" // BSD-3-Clause "github.com/mattn/go-xmpp" // BSD-3-Clause
) )
func sendIQ(client *xmpp.Client, iqc chan xmpp.IQ, target string, iQtype string, content string) (xmpp.IQ, error) { func sendIQ(client *xmpp.Client, iqc chan xmpp.IQ, target string, iQtype string, content string) (xmpp.IQ, error) {
var iq xmpp.IQ var iq xmpp.IQ
id := getID() id := getID()
c := make(chan xmpp.IQ, defaultBufferSize) c := make(chan xmpp.IQ)
go getIQ(id, c, iqc) go getIQ(id, c, iqc)
_, err := client.RawInformation(client.JID(), target, id, _, err := client.RawInformation(client.JID(), target, id,
iQtype, content) iQtype, content+"\n")
if err != nil { if errors.Unwrap(err) != nil {
return iq, fmt.Errorf("sendIQ: failed to send iq: %w", err) return iq, fmt.Errorf("sendIQ: failed to send iq: %w", err)
} }
select { iq = <-c
case iq = <-c:
case <-time.After(60 * time.Second):
return iq, fmt.Errorf("sendIQ: server didn't reply to IQ: %s", content)
}
return iq, nil return iq, nil
} }
@ -44,50 +37,22 @@ func getIQ(id string, c chan xmpp.IQ, iqc chan xmpp.IQ) {
} }
} }
func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc chan xmpp.Chat) { func rcvStanzas(client *xmpp.Client, iqc chan xmpp.IQ, msgc chan xmpp.Chat) {
var received interface{}
r := make(chan interface{}, defaultBufferSize)
e := make(chan error, defaultBufferSize)
go func() {
for {
select {
case <-ctx.Done():
return
default:
}
rcv, err := client.Recv()
if err != nil {
e <- err
} else {
r <- rcv
}
}
}()
for { for {
select { received, err := client.Recv()
case <-ctx.Done(): if errors.Unwrap(err) != nil {
return log.Println(err)
case err := <-e:
if err != nil {
if err != io.EOF {
closeAndExit(client, err)
return
}
return
}
case received = <-r:
} }
switch v := received.(type) { switch v := received.(type) {
case xmpp.Chat: case xmpp.Chat:
msgc <- v msgc <- v
case xmpp.IQ: case xmpp.IQ:
switch v.Type { switch v.Type {
case "get": case "get":
var xmlns *etree.Attr var xmlns *etree.Attr
iq := etree.NewDocument() iq := etree.NewDocument()
err := iq.ReadFromBytes(v.Query) err = iq.ReadFromBytes(v.Query)
if err != nil { if errors.Unwrap(err) != nil {
log.Println("Couldn't parse IQ:", log.Println("Couldn't parse IQ:",
string(v.Query), err) string(v.Query), err)
} }
@ -95,9 +60,6 @@ func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc
if query != nil { if query != nil {
xmlns = query.SelectAttr("xmlns") xmlns = query.SelectAttr("xmlns")
} }
if xmlns == nil {
break
}
switch xmlns.Value { switch xmlns.Value {
case nsDiscoInfo: case nsDiscoInfo:
root := etree.NewDocument() root := etree.NewDocument()
@ -115,35 +77,10 @@ func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc
identity.CreateAttr("name", "go-sendxmpp") identity.CreateAttr("name", "go-sendxmpp")
feat := replyQuery.CreateElement("feature") feat := replyQuery.CreateElement("feature")
feat.CreateAttr("var", nsDiscoInfo) feat.CreateAttr("var", nsDiscoInfo)
feat2 := replyQuery.CreateElement("feature")
feat2.CreateAttr("var", nsVersion)
xmlString, err := root.WriteToString()
if err == nil {
_, err = client.SendOrg(xmlString)
if err != nil {
log.Println(err)
}
}
case nsVersion:
root := etree.NewDocument()
root.WriteSettings.AttrSingleQuote = true
reply := root.CreateElement("iq")
reply.CreateAttr("type", "result")
reply.CreateAttr("from", client.JID())
reply.CreateAttr("to", v.From)
reply.CreateAttr("id", v.ID)
replyQuery := reply.CreateElement("query")
replyQuery.CreateAttr("xmlns", nsVersion)
rqName := replyQuery.CreateElement("name")
rqName.CreateText("go-sendxmpp")
rqVersion := replyQuery.CreateElement("version")
rqVersion.CreateText(version)
rqOS := replyQuery.CreateElement("os")
rqOS.CreateText(runtime.GOOS)
xmlString, err := root.WriteToString() xmlString, err := root.WriteToString()
if err == nil { if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString) _, err = client.SendOrg(xmlString)
if err != nil { if errors.Unwrap(err) != nil {
log.Println(err) log.Println(err)
} }
} }
@ -160,9 +97,9 @@ func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc
su := errorReply.CreateElement("service-unavailable") su := errorReply.CreateElement("service-unavailable")
su.CreateAttr("xmlns", nsXMPPStanzas) su.CreateAttr("xmlns", nsXMPPStanzas)
xmlString, err := root.WriteToString() xmlString, err := root.WriteToString()
if err == nil { if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString) _, err = client.SendOrg(xmlString)
if err != nil { if errors.Unwrap(err) != nil {
log.Println(err) log.Println(err)
} }
} }
@ -180,9 +117,9 @@ func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc
su := errorReply.CreateElement("service-unavailable") su := errorReply.CreateElement("service-unavailable")
su.CreateAttr("xmlns", nsXMPPStanzas) su.CreateAttr("xmlns", nsXMPPStanzas)
xmlString, err := root.WriteToString() xmlString, err := root.WriteToString()
if err == nil { if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString) _, err = client.SendOrg(xmlString)
if err != nil { if errors.Unwrap(err) != nil {
log.Println(err) log.Println(err)
} }
} }

Loading…
Cancel
Save