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 .'
#- golangci-lint run
#allow_failure: true
artifacts:
expire_in: 1 year
compile:
stage: build
only:
- 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:
- 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
- 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
- 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
- upx $CI_PROJECT_DIR/win64/go-sendxmpp.exe || true
artifacts:
paths:
- linux-amd64/go-sendxmpp
- linux-arm64/go-sendxmpp
- linux-386/go-sendxmpp
- linux-arm/go-sendxmpp
- win386/go-sendxmpp.exe
- win64/go-sendxmpp.exe
- CI_JOB_ID.txt
expire_in: 2 years
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`" \
--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 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\"}"
artifacts:
expire_in: 2 years

@ -1,95 +1,5 @@
# Changelog
## UNRELEASED
### Changed
- Fix Ox encryption in interactive mode (do not add the same recipient key to the keyring over and over again).
## [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
### Changed
- Properly close connection to server.

@ -15,7 +15,7 @@ sendxmpp incarnations. :)
## requirements
* [go](https://golang.org/) >= 1.21
* [go](https://golang.org/) >= 1.17
## 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:
```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.
-c, --chatroom Send message to a chatroom.
-d, --debug Show debugging info.
--fast-off Disable XEP-0484: Fast Authentication Streamlining Tokens.
-f, --file=value Set configuration file. (Default:
~/.config/go-sendxmpp/sendxmpprc)
--headline Send message as type headline.
@ -115,11 +114,9 @@ Usage: go-sendxmpp [-cdilnt] [-a value] [--fast-off] [-f value] [--headline] [--
-p, --password=value
Password for XMPP account.
--raw Send raw XML.
--scram-mech-pinning=value
Enforce the use of a certain SCRAM authentication mechanism.
--ssdp-off Disable XEP-0474: SASL SCRAM Downgrade Protection.
-s, --subject=value
Set message subject.
-r, --resource=value
Set resource. When sending to a chatroom this is used as
'alias'. DEPRECATED: Use --alias instead.
--timeout=value
Connection timeout in seconds. [10]
-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.
```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.
@ -156,11 +153,6 @@ Send file changes to two groupchats (`-c`) using a configuration file.
```bash
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 successfully."|go-sendxmpp recipient@example.com || echo "Rsync failed."|go-sendxmpp recipient@example.com
```
### shell completion

@ -5,65 +5,55 @@
package main
import (
"errors"
"fmt"
"net"
"os"
"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
)
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.
if options.Host == "" {
// Don't do SRV look ups if proxy is set.
if proxy == "" {
// Look up xmpp-client SRV records.
srvMixed, err := xmppsrv.LookupClient(server)
if len(srvMixed) > 0 && err == nil {
for _, adr := range srvMixed {
if !directTLS && adr.Type != "xmpps-client" {
// Use StartTLS
options.NoTLS = true
options.StartTLS = true
} else if adr.Type == "xmpps-client" {
// Use direct TLS
options.NoTLS = false
options.StartTLS = false
}
options.Host = net.JoinHostPort(adr.Target, fmt.Sprint(adr.Port))
// Connect to server
client, err := options.NewClient()
if err == nil {
return client, nil
}
server := options.User[strings.Index(options.User, "@")+1:]
// Look up xmpp-client SRV records.
srvMixed, err := xmppsrv.LookupClient(server)
if len(srvMixed) > 0 && err == nil {
for _, adr := range srvMixed {
if !directTLS && adr.Type != "xmpps-client" {
// Use StartTLS
options.NoTLS = true
options.StartTLS = true
} else if adr.Type == "xmpps-client" {
// Use direct TLS
options.NoTLS = false
options.StartTLS = false
}
options.Host = net.JoinHostPort(adr.Target, fmt.Sprint(adr.Port))
// Connect to server
client, err := options.NewClient()
if errors.Unwrap(err) == nil {
return client, nil
}
}
}
}
_, 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.
// Try port 5223 if directTLS is set and no xmpp-client SRV records are provided.
if directTLS {
options.NoTLS = false
options.StartTLS = false
options.Host = net.JoinHostPort(options.Host, "5223")
options.Host = net.JoinHostPort(server, "5223")
} 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.StartTLS = true
options.Host = net.JoinHostPort(options.Host, "5222")
options.Host = net.JoinHostPort(server, "5222")
}
}
// Connect to server
client, err := options.NewClient()
if err == nil {
if errors.Unwrap(err) == nil {
return client, nil
}
return client, fmt.Errorf("failed to connect to server: %w", err)

@ -5,7 +5,7 @@
package main
const (
version = "0.11.1-dev"
version = "0.6.1"
// defaults
defaultBufferSize = 100
defaultConfigRowSep = 2
@ -17,6 +17,7 @@ const (
defaultRpadMultiple = 100
defaultRSABits = 4096
defaultShortIDBytes = 4
defaultSleepTime = 100
defaultTimeout = 10
defaultTLSMinVersion = 12
defaultTLS10 = 10
@ -35,13 +36,11 @@ const (
nsOxPubKeys = "urn:xmpp:openpgp:0:public-keys"
nsPubsub = "http://jabber.org/protocol/pubsub"
nsPubsubOwner = "http://jabber.org/protocol/pubsub#owner"
nsVersion = "jabber:iq:version"
nsXMPPStanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"
// strings
oxAltBody = "This message is encrypted (XEP-0373: OpenPGP for XMPP)."
pubsubPubOptions = "http://jabber.org/protocol/pubsub#publish-options"
strChat = "chat"
strEmpty = ""
strError = "error"
strGroupchat = "groupchat"
strHeadline = "headline"

@ -1,24 +1,23 @@
module salsa.debian.org/mdosch/go-sendxmpp
go 1.21.5
go 1.17
require (
github.com/ProtonMail/gopenpgp/v2 v2.8.0-alpha.1
github.com/beevik/etree v1.4.0
github.com/gabriel-vasile/mimetype v1.4.4
github.com/google/uuid v1.6.0
github.com/ProtonMail/gopenpgp/v2 v2.7.2
github.com/beevik/etree v1.2.0
github.com/gabriel-vasile/mimetype v1.4.2
github.com/mattn/go-xmpp v0.0.1
github.com/pborman/getopt/v2 v2.1.0
github.com/xmppo/go-xmpp v0.2.2-0.20240621203446-ca6fa9ced5fa
golang.org/x/crypto v0.24.0
salsa.debian.org/mdosch/xmppsrv v0.2.6
salsa.debian.org/mdosch/xmppsrv v0.2.5
)
require (
github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton // 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/cloudflare/circl v1.3.9 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
)

@ -1,44 +1,48 @@
github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton h1:HKz85FwoXx86kVtTvFke7rgHvq/HoloSUvW5semjFWs=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
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-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/gopenpgp/v2 v2.8.0-alpha.1 h1:FzlISNTMQiDhsg7q92Cd3Bkvu+7i/iEoAthswamhYrQ=
github.com/ProtonMail/gopenpgp/v2 v2.8.0-alpha.1/go.mod h1:TvBY11CZdahgzQ4iSUMv80emDXYwKlTl3FB1R4EuKnU=
github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs=
github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/ProtonMail/gopenpgp/v2 v2.7.2 h1:mIwxSUPezxNYq0RA5106VPWyKC+Ly3FvBUnBJh/7GWw=
github.com/ProtonMail/gopenpgp/v2 v2.7.2/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw=
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/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/mattn/go-xmpp v0.0.1 h1:njHom/3EP3ynacLHX9lBpKMMknYL76ic/19fPsR6MB8=
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/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xmppo/go-xmpp v0.2.1 h1:8Bw6W6RNGTq6ajgMiKxn0iJKYt6Atef5Y0A5AkGWn9Q=
github.com/xmppo/go-xmpp v0.2.1/go.mod h1:H46WSy/5uHW1SWsyJYI6fRZqFrK326qWmFRpVJ2Wwhs=
github.com/xmppo/go-xmpp v0.2.2-0.20240621203446-ca6fa9ced5fa h1:Mq5ks6uzJXT1lLJY8+7A6IcYaFQ8svTkD6eOUt27cWw=
github.com/xmppo/go-xmpp v0.2.2-0.20240621203446-ca6fa9ced5fa/go.mod h1:M7bq312fy4uiXD+3DghCC99YlINWhMmzTqokfR4/QJQ=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
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.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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -47,25 +51,36 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.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.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.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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
salsa.debian.org/mdosch/xmppsrv v0.2.6 h1:+S7ZxRP7BQwQHGJvYhr408UD0XGAd7+RZaVA/xN4EIc=
salsa.debian.org/mdosch/xmppsrv v0.2.6/go.mod h1:udWXnWFa9zkcyN9YSB/u44BCnnRDpeQ0eDy3MVLjHZQ=
salsa.debian.org/mdosch/xmppsrv v0.2.5 h1:ACPk8EhmCAUMl59TnGe5kvvwSnW065CJrhN7uvt25xY=
salsa.debian.org/mdosch/xmppsrv v0.2.5/go.mod h1:udWXnWFa9zkcyN9YSB/u44BCnnRDpeQ0eDy3MVLjHZQ=

@ -6,29 +6,22 @@ package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/gob"
"errors"
"fmt"
"log"
"math/big"
"net/url"
"os"
"regexp"
"runtime"
"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 {
// 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}]`)
s = reg.ReplaceAllString(s, "<EFBFBD>")
s = reg.ReplaceAllString(s, "")
return s
}
@ -36,190 +29,24 @@ func validUTF8(s string) string {
func validURI(s string) (*url.URL, error) {
// Check if URI is valid
uri, err := url.ParseRequestURI(s)
if err != nil {
return uri, fmt.Errorf("validURI: %w", err)
}
return uri, nil
return uri, fmt.Errorf("validURI: %w", err)
}
func readFile(path string) (*bytes.Buffer, error) {
file, err := os.Open(path)
if err != nil {
if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("readFile: %w", err)
}
defer file.Close()
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(file)
if err != nil {
if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("readFile: %w", err)
}
return buffer, nil
}
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)
err = file.Close()
if errors.Unwrap(err) != nil {
fmt.Println("error while closing file:", err)
}
return 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
return buffer, nil
}
func getRpad(messageLength int) string {
@ -229,7 +56,7 @@ func getRpad(messageLength int) string {
rpad := make([]rune, length)
for i := range rpad {
randInt, err := rand.Int(rand.Reader, max)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
rpad[i] = rpadRunes[randInt.Int64()]
@ -238,9 +65,19 @@ func getRpad(messageLength int) 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 {
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 (
"bytes"
"encoding/xml"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/beevik/etree" // BSD-2-clause
"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,
timeout time.Duration,
) (string, error) {
func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath string) string {
var uploadComponent string
var maxFileSize int64
var iqDiscoItemsXMLQuery, iqDiscoInfoXMLQuery *etree.Element
// Get file size
fileInfo, err := os.Stat(filePath)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
fileSize := fileInfo.Size()
// Read file
buffer, err := readFile(filePath)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
// Get mime type
@ -50,24 +46,24 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
// Get file name
fileName := filepath.Base(filePath)
// 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\+\-\_\.]+`)
fileNameEscaped := reg.ReplaceAllString(fileName, "_")
// Query server for disco#items
iqContent, err := sendIQ(client, iqc, jserver, "get",
"<query xmlns='http://jabber.org/protocol/disco#items'/>")
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
iqDiscoItemsXML := etree.NewDocument()
err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
iqDiscoItemsXMLQuery = iqDiscoItemsXML.SelectElement("query")
iqDiscoItemsXMLQuery := iqDiscoItemsXML.SelectElement("query")
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")
@ -79,22 +75,22 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query")
iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo)
iqdi, err := iqDiscoInfoReqXML.WriteToString()
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
if iqDiscoInfo.Type != strResult {
continue
}
iqDiscoInfoXML := etree.NewDocument()
err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
iqDiscoInfoXMLQuery = iqDiscoInfoXML.SelectElement("query")
iqDiscoInfoXMLQuery := iqDiscoInfoXML.SelectElement("query")
if iqDiscoInfoXMLQuery == nil {
continue
}
@ -113,13 +109,12 @@ func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath
if iqDiscoInfoXMLType.Value == "file" &&
iqDiscoInfoXMLCategory.Value == "store" {
uploadComponent = jid.Value
break
}
}
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 {
field := r.SelectElements("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 {
continue
}
prevFieldVal := field[i-1].SelectElement("value")
if prevFieldVal == nil {
continue
}
curFieldVal := t.SelectElement("value")
if curFieldVal == nil {
continue
}
if varAttr.Value == "max-file-size" {
var prevFieldVal *etree.Element
if i > 0 {
prevFieldVal = field[i-1].SelectElement("value")
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.")
}
if varAttr.Value == "max-file-size" && prevFieldVal.Text() == nsHTTPUpload {
maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 10, 64)
if errors.Unwrap(err) != nil {
log.Fatal("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.
if maxFileSize != 0 {
if fileSize > maxFileSize {
return "", fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB).",
strconv.FormatInt(fileSize/1024/1024, 10), strconv.FormatInt(maxFileSize/1024/1024, 10))
log.Fatal("File size " + strconv.FormatInt(fileSize/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("content-type", mimeType)
r, err := request.WriteToString()
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
// Request http upload slot
uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
if uploadSlot.Type != strResult {
return "", fmt.Errorf("http-upload: error while requesting upload slot.")
log.Fatal("error while requesting upload slot.")
}
iqHTTPUploadSlotXML := etree.NewDocument()
err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot")
if iqHTTPUploadSlotXMLSlot == nil {
return "", fmt.Errorf("http-upload: no slot element")
log.Fatal("http-upload: no slot element")
}
iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put")
if iqHTTPUploadSlotXMLPut == nil {
return "", fmt.Errorf("http-upload: no put element")
log.Fatal("http-upload: no put element")
}
iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url")
if iqHTTPUploadSlotXMLPutURL == nil {
return "", fmt.Errorf("http-upload: no url attribute")
}
if !strings.HasPrefix(iqHTTPUploadSlotXMLPutURL.Value, "https://") {
return "", fmt.Errorf("http-upload: upload slot does not provide https")
log.Fatal("http-upload: no url attribute")
}
// Upload file
httpTransport := &http.Transport{
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}
httpClient := &http.Client{}
req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value,
buffer)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", mimeTypeEscaped.String())
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)
if err != nil {
return "", err
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
// Test for http status code "200 OK" or "201 Created"
if resp.StatusCode != 200 && resp.StatusCode != 201 {
return "", fmt.Errorf("http-upload: upload failed.")
log.Fatal("Http upload failed.")
}
// Return http link
iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get")
if iqHTTPUploadSlotXMLGet == nil {
return "", fmt.Errorf("http-upload: no get element")
log.Fatal("http-upload: no get element")
}
iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url")
if iqHTTPUploadSlotXMLGetURL == nil {
return "", fmt.Errorf("http-upload: no url attribute")
log.Fatal("http-upload: no url attribute")
}
err = resp.Body.Close()
if err != nil {
fmt.Println("http-upload: error while closing http request body:", err)
if errors.Unwrap(err) != nil {
fmt.Println("error while closing http request body:", err)
}
return iqHTTPUploadSlotXMLGetURL.Value, nil
return iqHTTPUploadSlotXMLGetURL.Value
}

@ -8,7 +8,7 @@
package main
import (
"fmt"
"errors"
"strings"
"unicode/utf8"
)
@ -35,7 +35,7 @@ func MarshalJID(input string) (string, error) {
} else {
// If the resource part exists, make sure it isn't empty.
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:]
s = s[:sep]
@ -52,7 +52,7 @@ func MarshalJID(input string) (string, error) {
domainpart = s
case sep == 0:
// 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
default:
domainpart = s[sep+1:]
@ -73,11 +73,11 @@ func MarshalJID(input string) (string, error) {
var jid string
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 == "" {
return input, fmt.Errorf("invalid JID: %s", input)
return input, errors.New("Invalid JID: " + input)
}
if localpart == "" {

@ -6,22 +6,19 @@ package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
osUser "os/user"
"runtime"
"strings"
"time"
"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/xmppo/go-xmpp" // BSD-3-Clause
)
type configuration struct {
@ -29,17 +26,10 @@ type configuration struct {
jserver string
port string
password string
resource 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) {
var (
output string
@ -48,16 +38,15 @@ func readMessage(messageFilePath string) (string, error) {
// Check that message file is existing.
_, err = os.Stat(messageFilePath)
if err != nil {
if os.IsNotExist(err) {
return output, fmt.Errorf("readMessage: %w", err)
}
// Open message file.
file, err := os.Open(messageFilePath)
if err != nil {
if errors.Unwrap(err) != nil {
return output, fmt.Errorf("readMessage: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
@ -68,13 +57,18 @@ func readMessage(messageFilePath string) (string, error) {
}
}
if err = scanner.Err(); err != nil {
if err != io.EOF {
if err := scanner.Err(); errors.Unwrap(err) != nil {
if errors.Unwrap(err) != io.EOF {
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() {
@ -84,11 +78,10 @@ func main() {
}
var (
err error
message, user, server, password, alias string
oxPrivKey *crypto.Key
recipients []recipientsType
fast xmpp.Fast
err error
message, user, server, password, resource, alias string
oxPrivKey *crypto.Key
recipients []recipientsType
)
// Define command line flags.
@ -100,6 +93,8 @@ func main() {
flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.")
flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.")
flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
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"+
"for chatrooms.")
flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+
@ -129,10 +124,6 @@ func main() {
flagOxDeleteNodes := getopt.BoolLong("ox-delete-nodes", 0, "Delete existing OpenPGP nodes on the server.")
flagOOBFile := getopt.StringLong("oob-file", 0, "", "URL to send a file as out of band data.")
flagHeadline := getopt.BoolLong("headline", 0, "Send message as type headline.")
flagSCRAMPinning := getopt.StringLong("scram-mech-pinning", 0, "", "Enforce the use of a certain SCRAM authentication mechanism.")
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.
getopt.Parse()
@ -144,9 +135,7 @@ func main() {
os.Exit(0)
case *flagVersion:
// If requested, show version and quit.
fmt.Println("Go-sendxmpp", version)
system := runtime.GOOS + "/" + runtime.GOARCH
fmt.Println("System:", system, runtime.Version())
fmt.Println("go-sendxmpp", version)
fmt.Println("License: BSD-2-clause")
os.Exit(0)
// Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like
@ -163,35 +152,10 @@ func main() {
" http upload.")
case *flagOx && *flagOOBFile != "":
log.Fatal("No encryption possible for OOB data.")
case *flagOx && *flagHeadline:
log.Fatal("No Ox support for headline messages.")
case *flagHeadline && *flagChatroom:
log.Fatal("Can't use message type headline for groupchat messages.")
}
// 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.
// 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).
@ -206,13 +170,14 @@ func main() {
if *flagUser == "" || *flagPassword == "" {
// Read configuration from file.
config, err := parseConfig(*flagFile)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal("Error parsing ", *flagFile, ": ", err)
}
// Set connection options according to config.
user = config.username
server = config.jserver
password = config.password
resource = config.resource
alias = config.alias
if config.port != "" {
server = net.JoinHostPort(server, fmt.Sprint(config.port))
@ -233,24 +198,16 @@ func main() {
if *flagPassword != "" {
password = *flagPassword
}
// If no server part is specified in the username but a server is specified
// just assume the server is identical to the server part and hope for the
// best. This is for compatibility with the old perl sendxmpp config files.
var serverpart string
if !strings.Contains(user, "@") && server != "" {
// Remove port if server contains it.
if strings.Contains(server, ":") {
serverpart, _, err = net.SplitHostPort(server)
if err != nil {
log.Fatal(err)
}
} else {
serverpart = server
}
user = user + "@" + serverpart
}
switch {
// Use 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.
case alias == "" && *flagAlias == "":
alias = "go-sendxmpp"
@ -259,29 +216,23 @@ func main() {
alias = *flagAlias
}
// Timeout
timeout := time.Duration(*flagTimeout) * time.Second
clientID, err := getClientID(user)
if err != nil {
fmt.Println(err)
// Overwrite resource if specified via command line flag.
if *flagResource != "" {
fmt.Println("Deprecated flag: --resource.")
resource = *flagResource
} else if resource == "" {
// Use "go-sendxmpp" plus a random string if no other resource is specified.
resource = "go-sendxmpp." + getShortID()
}
if !*flagFastOff {
fast, _ = getFastData(user, password)
// Reset FAST token and mechanism if expired.
if time.Now().After(fast.Expiry) {
fast.Token = ""
fast.Mechanism = ""
}
}
// Timeout
timeout := time.Duration(*flagTimeout) * time.Second
// Use ALPN
var tlsConfig tls.Config
tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
tlsConfig.InsecureSkipVerify = *flagSkipVerify
tlsConfig.Renegotiation = tls.RenegotiateNever
switch *flagTLSMinVersion {
case defaultTLS10:
tlsConfig.MinVersion = tls.VersionTLS10
@ -296,8 +247,6 @@ func main() {
os.Exit(0)
}
resource := "go-sendxmpp." + getShortID()
// Set XMPP connection options.
options := xmpp.Options{
Host: server,
@ -310,24 +259,17 @@ func main() {
// an unencrypted connection is established. As StartTLS is
// set when NoTLS is set go-sendxmpp won't use unencrypted
// client-to-server connections.
// See https://pkg.go.dev/github.com/xmppo/go-xmpp#Options
NoTLS: !*flagDirectTLS,
StartTLS: !*flagDirectTLS,
Debug: *flagDebug,
TLSConfig: &tlsConfig,
Mechanism: *flagSCRAMPinning,
SSDP: !*flagSSDPOff,
UserAgentSW: resource,
UserAgentID: clientID,
Fast: !*flagFastOff,
FastToken: fast.Token,
FastMechanism: fast.Mechanism,
// See https://pkg.go.dev/github.com/mattn/go-xmpp#Options
NoTLS: !*flagDirectTLS,
StartTLS: !*flagDirectTLS,
Debug: *flagDebug,
TLSConfig: &tlsConfig,
}
// Read message from file.
if *flagMessageFile != "" {
message, err = readMessage(*flagMessageFile)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
@ -348,7 +290,7 @@ func main() {
}
if err := scanner.Err(); err != nil {
if err != io.EOF {
if errors.Unwrap(err) != io.EOF {
log.Fatal(err)
}
}
@ -365,48 +307,20 @@ func main() {
// Connect to server.
client, err := connect(options, *flagDirectTLS)
if err != nil {
if fast.Token != "" {
// 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)
}
if errors.Unwrap(err) != nil {
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)
msgc := make(chan xmpp.Chat, defaultBufferSize)
ctx, cancel := context.WithCancel(context.Background())
go rcvStanzas(client, ctx, iqc, msgc)
go rcvStanzas(client, iqc, msgc)
for _, r := range getopt.Args() {
var re recipientsType
re.Jid = r
if *flagOx {
re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r)
if err != nil {
if errors.Unwrap(err) != nil {
re.OxKeyRing = nil
fmt.Println("ox: error fetching key for", r+":", err)
}
@ -417,9 +331,8 @@ func main() {
// Check that all recipient JIDs are valid.
for i, recipient := range recipients {
validatedJid, err := MarshalJID(recipient.Jid)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
recipients[i].Jid = validatedJid
}
@ -427,73 +340,59 @@ func main() {
switch {
case *flagOxGenPrivKeyX25519:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
os.Exit(0)
case *flagOxGenPrivKeyRSA:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
os.Exit(0)
case *flagOxImportPrivKey != "":
validatedOwnJid, err := MarshalJID(user)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
client, iqc)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
os.Exit(0)
case *flagOxDeleteNodes:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
err = oxDeleteNodes(validatedOwnJid, client, iqc)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
os.Exit(0)
case *flagOx:
validatedOwnJid, err := MarshalJID(user)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
if *flagHTTPUpload != "" {
message, err = httpUpload(client, iqc, tlsConfig.ServerName,
*flagHTTPUpload, timeout)
if err != nil {
cancel()
closeAndExit(client, err)
}
message = httpUpload(client, iqc, tlsConfig.ServerName,
*flagHTTPUpload)
}
if *flagOOBFile != "" {
@ -501,9 +400,8 @@ func main() {
message = validUTF8(*flagOOBFile)
// Check if the URI is valid.
uri, err := validURI(message)
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
message = uri.String()
}
@ -525,9 +423,8 @@ func main() {
} else {
_, err = client.JoinMUCNoHistory(recipient.Jid, alias)
}
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
}
@ -537,37 +434,19 @@ func main() {
break
}
// Send raw XML
_, err = client.SendOrg(message)
if err != nil {
cancel()
closeAndExit(client, err)
_, err = client.SendOrg(message + "\n")
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
case *flagInteractive:
// Send in endless loop (for usage with e.g. "tail -f").
reader := bufio.NewReader(os.Stdin)
// Quit if ^C is pressed.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cancel()
closeAndExit(client, nil)
}
}()
for {
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")
if errors.Unwrap(err) != nil {
log.Fatal("failed to read from stdin")
}
// Remove invalid code points.
message = validUTF8(message)
@ -581,47 +460,35 @@ func main() {
continue
}
oxMessage, err := oxEncrypt(client, oxPrivKey,
recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
if err != nil {
recipient.Jid, recipient.OxKeyRing, message)
if errors.Unwrap(err) != nil {
fmt.Println("Ox: couldn't encrypt to",
recipient.Jid)
continue
}
_, err = client.SendOrg(oxMessage)
if err != nil {
cancel()
closeAndExit(client, err)
_, err = client.SendOrg(oxMessage + "\n")
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
Subject: *flagSubject,
})
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
}
}
case *flagListen:
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()
closeAndExit(client, nil)
}
}()
for {
v := <-msgc
switch {
case isOxMsg(v) && *flagOx:
msg, t, err := oxDecrypt(v, client, iqc, user, oxPrivKey)
if err != nil {
if errors.Unwrap(err) != nil {
log.Println(err)
continue
}
@ -693,25 +560,18 @@ func main() {
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Ooburl: message, Text: message,
Subject: *flagSubject,
})
if err != nil {
if errors.Unwrap(err) != nil {
fmt.Println("Couldn't send message to",
recipient.Jid)
}
// (Hopefully) temporary workaround due to go-xmpp choking on URL encoding.
// Once this is fixed in the lib the http-upload case above can be reused.
case *flagOOBFile != "":
var msg string
if *flagSubject != "" {
msg = fmt.Sprintf("<message to='%s' type='%s'><subject>%s</subject><body>%s</body><x xmlns='jabber:x:oob'><url>%s</url></x></message>",
recipient.Jid, msgType, *flagSubject, message, message)
} 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 {
_, err = client.SendOrg("<message to='" + recipient.Jid + "' type='" +
msgType + "'><body>" + message + "</body><x xmlns='jabber:x:oob'><url>" +
message + "</url></x></message>")
if errors.Unwrap(err) != nil {
fmt.Println("Couldn't send message to",
recipient.Jid)
}
@ -720,29 +580,29 @@ func main() {
continue
}
oxMessage, err := oxEncrypt(client, oxPrivKey,
recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
if err != nil {
recipient.Jid, recipient.OxKeyRing, message)
if errors.Unwrap(err) != nil {
fmt.Println("Ox: couldn't encrypt to", recipient.Jid)
continue
}
_, err = client.SendOrg(oxMessage)
if err != nil {
cancel()
closeAndExit(client, err)
_, err = client.SendOrg(oxMessage + "\n")
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
default:
_, err = client.Send(xmpp.Chat{
Remote: recipient.Jid,
Type: msgType, Text: message,
Subject: *flagSubject,
})
if err != nil {
cancel()
closeAndExit(client, err)
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
}
}
cancel()
closeAndExit(client, nil)
// Wait for a short time as some messages are not delivered by the server
// if the connection is closed immediately after sending a message.
time.Sleep(defaultSleepTime * time.Millisecond)
client.Close()
}

@ -1,12 +1,12 @@
.\" generated with Ronn-NG/v0.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"
\fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\.
.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"
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
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"
@ -20,9 +20,6 @@ Send message to a chatroom\.
\fB\-d\fR, \fB\-\-debug\fR
Show debugging info\.
.TP
\fB\-\-fast\-off\fR
Disable XEP\-0484: Fast Authentication Streamlining Tokens\.
.TP
\fB\-f\fR, \fB\-\-file\fR=[\fIvalue\fR]
Set configuration file\. (Default: ~/\.config/go\-sendxmpp/config)
.TP
@ -80,20 +77,17 @@ Import an existing private OpenPGP key\.
\fB\-\-ox\-passphrase\fR=[\fIvalue\fR]
Passphrase for locking and unlocking the private OpenPGP key\.
.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]
Password for XMPP account\.
.TP
\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\.
.TP
\fB\-\-scram\-mech\-pinning=[<value>]\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)\.
.TP
\fB\-\-ssdp\-off\fR
Disable XEP\-0474: SASL SCRAM Downgrade Protection\.
.TP
\fB\-s\fR, \fB\-\-subject\fR=[\fIvalue\fR]
Set message subject\.
\fB\-r\fR, \fB\-\-resource\fR=[\fIvalue\fR]
DEPRECATED: Set resource\. When sending to a chatroom this is used as 'alias'\.
.TP
\fB\-\-timeout=\fR[\fIvalue\fR]
Connection timeout in seconds\. (Default: 10)
@ -101,26 +95,11 @@ Connection timeout in seconds\. (Default: 10)
\fB\-t\fR, \fB\-\-tls\fR
Use direct TLS\.
.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]
Username for XMPP account (JID)\.
.TP
\fB\-\-version\fR
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"
.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\.
@ -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:
.P
\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"
Feel free to join \fIhttps://join\.jabber\.network/#go\-sendxmpp@chat\.mdosch\.de?join\fR\.
.SH "AUTHOR"

@ -57,8 +57,6 @@
<a href="#SYNOPSIS">SYNOPSIS</a>
<a href="#DESCRIPTION">DESCRIPTION</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="#CHAT">CHAT</a>
<a href="#AUTHOR">AUTHOR</a>
@ -81,14 +79,11 @@
</p>
<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]
[--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>
<p><code>go-sendxmpp</code> [-cdintx] [-f value] [--help] [-j value] [-m value] [-p value] [-r value] [-u value] [parameters ...]</p>
<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
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>
@ -108,8 +103,6 @@ file location is specified with <code>-f</code> or <code>--file</code>.</p>
<code>-d</code>, <code>--debug</code>
</dt>
<dd>Show debugging info.</dd>
<dt><code>--fast-off</code></dt>
<dd>Disable XEP-0484: Fast Authentication Streamlining Tokens.</dd>
<dt>
<code>-f</code>, <code>--file</code>=[<var>value</var>]</dt>
<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>
<dd>Passphrase for locking and unlocking the private OpenPGP key.</dd>
<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>
<dd>Password for XMPP account.</dd>
<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.
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>
<code>-s</code>, <code>--subject</code>=[<var>value</var>]</dt>
<dd>Set message subject.</dd>
<code>-r</code>, <code>--resource</code>=[<var>value</var>]</dt>
<dd>DEPRECATED: Set resource. When sending to a chatroom this is used as 'alias'.</dd>
<dt>
<code>--timeout=</code>[<var>value</var>]</dt>
<dd>Connection timeout in seconds. (Default: 10)</dd>
@ -204,41 +192,12 @@ to prevent downgrade attacks (needs server support).</dd>
</dt>
<dd>Use direct TLS.</dd>
<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>
<dd>Username for XMPP account (JID).</dd>
<dt><code>--version</code></dt>
<dd>Show version information.</dd>
</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>
<h3 id="ZSH">ZSH</h3>
@ -253,14 +212,6 @@ good enough.</p>
compdef _gnu_generic go-sendxmpp
</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>
<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'>
<li class='tl'></li>
<li class='tc'>June 2024</li>
<li class='tc'>June 2023</li>
<li class='tr'>go-sendxmpp(1)</li>
</ol>

@ -3,14 +3,11 @@ go-sendxmpp(1) -- A tool to send messages to an XMPP contact or MUC.
## SYNOPSIS
`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…]`
`go-sendxmpp` [-cdintx] [-f value] [--help] [-j value] [-m value] [-p value] [-r value] [-u value] [parameters ...]
## 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
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`
@ -28,9 +25,6 @@ Send message to a chatroom.
* `-d`, `--debug`:
Show debugging info.
* `--fast-off`:
Disable XEP-0484: Fast Authentication Streamlining Tokens.
* `-f`, `--file`=[<value>]:
Set configuration file. (Default: ~/.config/go-sendxmpp/config)
@ -103,6 +97,9 @@ Import an existing private OpenPGP key.
* `--ox-passphrase`=[<value>]:
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>]:
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.
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>]`:
Enforce the use of a certain SCRAM authentication mechanism. Currently go-sendxmpp supports
**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.
* `-r`, `--resource`=[<value>]:
DEPRECATED: Set resource. When sending to a chatroom this is used as 'alias'.
* `--timeout=`[<value>]:
Connection timeout in seconds. (Default: 10)
@ -129,42 +116,12 @@ Connection timeout in seconds. (Default: 10)
* `-t`, `--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>]:
Username for XMPP account (JID).
* `--version`:
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
### ZSH
@ -179,14 +136,6 @@ Just place the following in your `~/.zshrc` or `~/.zshrc.local`:
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
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
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "GO\-SENDXMPP" "5" "October 2023" ""
.TH "GO\-SENDXMPP" "5" "May 2023" ""
.SH "NAME"
\fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\.
.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"
username: [\fIyour_jid\fR]
.br

@ -78,7 +78,7 @@
<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>
(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>
<h2 id="FORMAT">FORMAT</h2>
@ -112,11 +112,11 @@ License: BSD 2-clause License</p>
<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'>
<li class='tl'></li>
<li class='tc'>October 2023</li>
<li class='tc'>May 2023</li>
<li class='tr'>go-sendxmpp(5)</li>
</ol>

@ -4,7 +4,7 @@ go-sendxmpp(5) -- A tool to send messages to an XMPP contact or MUC.
## LOCATION
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:
## FORMAT

301
ox.go

@ -6,6 +6,7 @@ package main
import (
"encoding/base64"
"errors"
"fmt"
"log"
"os"
@ -15,7 +16,7 @@ import (
"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
"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 {
@ -24,25 +25,25 @@ func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error {
query := nodeListRequest.CreateElement("query")
query.CreateAttr("xmlns", nsDiscoItems)
nlr, err := nodeListRequest.WriteToString()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxDeleteNodes: failed to create node list request %w", err)
}
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)
}
nodeListReply := etree.NewDocument()
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)
}
query = nodeListReply.SelectElement("query")
if query == nil {
return fmt.Errorf("error parsing iq reply")
return errors.New("error parsing iq reply")
}
items := query.SelectElements("item")
if items == nil {
return fmt.Errorf("error parsing iq reply")
return errors.New("error parsing iq reply")
}
for _, item := range items {
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.CreateAttr("node", node.Value)
dnr, err := deleteNodeRequest.WriteToString()
if err != nil {
if errors.Unwrap(err) != nil {
continue
}
_, err = sendIQ(client, iqc, jid, "set", dnr)
if err != nil {
if errors.Unwrap(err) != nil {
continue
}
}
@ -77,7 +78,7 @@ func oxDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string,
for _, r := range m.OtherElem {
if r.XMLName.Space == nsOx {
cryptMsgByte, err = base64.StdEncoding.DecodeString(r.InnerXML)
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
break
@ -85,54 +86,54 @@ func oxDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string,
}
oxMsg := crypto.NewPGPMessage(cryptMsgByte)
keyRing, err := crypto.NewKeyRing(oxPrivKey)
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
senderKeyRing, err := oxGetPublicKeyRing(client, iqc, sender)
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
decryptMsg, err := keyRing.Decrypt(oxMsg, senderKeyRing, crypto.GetUnixTime())
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
// Remove invalid code points.
message := validUTF8(string(decryptMsg.Data))
doc := etree.NewDocument()
err = doc.ReadFromString(message)
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
signcrypt := doc.SelectElement("signcrypt")
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")
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")
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 {
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")
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")
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)
if err != nil {
if errors.Unwrap(err) != nil {
return strError, time.Now(), err
}
payload := signcrypt.SelectElement("payload")
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")
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 {
xmppURI := "xmpp:" + jid
buffer, err := readFile(privKeyLocation)
if err != nil {
if errors.Unwrap(err) != nil {
return err
}
key, err := crypto.NewKey(buffer.Bytes())
if err != nil {
if errors.Unwrap(err) != nil {
key, err = crypto.NewKeyFromArmored(buffer.String())
if err != nil {
if errors.Unwrap(err) != nil {
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)
}
key, err = crypto.NewKey(keyDecoded)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to import private key: %w", err)
}
}
}
entity := key.GetEntity()
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()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to get public key associated to private key: %w", err)
}
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)
}
fingerprint := strings.ToUpper(pubKey.GetFingerprint())
_, err = oxRecvPublicKeys(client, iqc, jid, fingerprint)
if err != nil {
if errors.Unwrap(err) != nil {
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)
}
}
location, err := oxGetPrivKeyLoc(jid)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to determine private key location: %w", err)
}
keySerialized, err := key.Serialize()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to serialize private key: %w", err)
}
err = oxStoreKey(location,
base64.StdEncoding.EncodeToString(keySerialized))
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
pubKeyRing, err := oxGetPublicKeyRing(client, iqc, jid)
if err == nil {
if errors.Unwrap(err) == nil {
pubKeys := pubKeyRing.GetKeys()
for _, r := range pubKeys {
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)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxImportPrivKey: failed to publish public key: %w", err)
}
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")
fingerprint := strings.ToUpper(pubKey.GetFingerprint())
keySerialized, err := pubKey.Serialize()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxPublishPubKey: failed to serialize pubkey: %w", err)
}
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.CreateText("open")
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)
}
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)
}
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)
if err != nil {
return fmt.Errorf("couldn't successfully verify public key upload")
if errors.Unwrap(err) != nil {
return errors.New("couldn't successfully verify public key upload")
}
ownPubKeyFromPubsub := ownPubKeyRingFromPubsub.GetKeys()[0]
ownPubKeyFromPubsubSerialized, err := ownPubKeyFromPubsub.Serialize()
if err != nil {
return fmt.Errorf("couldn't successfully verify public key upload")
if errors.Unwrap(err) != nil {
return errors.New("couldn't successfully verify public key upload")
}
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.WriteSettings.AttrSingleQuote = true
@ -301,59 +302,80 @@ func oxPublishPubKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, pubKey *
value = field.CreateElement("value")
value.CreateText("open")
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)
}
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)
}
if iqReply.Type != strResult {
return fmt.Errorf("couldn't publish public key list")
return errors.New("couldn't publish public key list")
}
return nil
}
func oxGetPrivKeyLoc(jid string) (string, error) {
dataDir, err := getDataPath(strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1))
if err != nil {
return strError, fmt.Errorf("oxGetPrivKeyLoc: %w", err)
}
oldDataDir, err := getDataPath("oxprivkeys/")
if err != nil {
return strError, fmt.Errorf("oxGetPrivKeyLoc: %w", err)
}
// TODO: Remove handling of oldDataFile in a later version when it's very likely that there are no
// more versions in use using the oldDataFile (<0.8.3).
oldDataFile := oldDataDir + base64.StdEncoding.EncodeToString([]byte(jid))
oldDataFile2 := oldDataDir + strings.Replace(jid, "@", "_at_", -1)
oldDataFile3 := oldDataDir + strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1)
dataFile := dataDir + "oxprivkey"
if _, err := os.Stat(oldDataFile); err == nil {
err := os.Rename(oldDataFile, dataFile)
if err != nil {
return dataFile, err
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 errors.Unwrap(err) != nil {
return strError, fmt.Errorf("oxGetPrivKeyLoc: failed to determine user dir: %w", err)
}
}
if _, err := os.Stat(oldDataFile2); err == nil {
err := os.Rename(oldDataFile2, dataFile)
if err != nil {
return dataFile, err
if homeDir == "" {
return strError, errors.New("oxGetPrivKeyLoc: received empty string for home directory")
}
dataDir = homeDir + "/.local/share"
}
if _, err := os.Stat(oldDataFile3); err == nil {
err := os.Rename(oldDataFile3, dataFile)
if err != nil {
return dataFile, err
dataDir += "/go-sendxmpp/oxprivkeys/"
if _, err = os.Stat(dataDir); os.IsNotExist(err) {
err = os.MkdirAll(dataDir, defaultDirRights)
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
}
func oxGetPubKeyLoc(fingerprint string) (string, error) {
dataDir, err := getDataPath("oxpubkeys/")
if err != nil {
return strError, fmt.Errorf("oxGetPubKeyLoc: %w", err)
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 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
return dataFile, nil
@ -361,37 +383,37 @@ func oxGetPubKeyLoc(fingerprint string) (string, error) {
func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
dataFile, err := oxGetPrivKeyLoc(jid)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
keyBuffer, err := readFile(dataFile)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
keyString := keyBuffer.String()
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)
}
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)
}
if passphrase != "" {
key, err = key.Unlock([]byte(passphrase))
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal("Ox: couldn't unlock private key.")
}
}
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)
}
if isLocked {
log.Fatal("Ox: private key is locked.")
}
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
}
@ -399,7 +421,7 @@ func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
func oxStoreKey(location string, key string) error {
var file *os.File
file, err := os.Create(location)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxStoreKey: failed to create key location: %w", err)
}
if runtime.GOOS != "windows" {
@ -408,11 +430,11 @@ func oxStoreKey(location string, key string) error {
_ = file.Chmod(os.FileMode(defaultFileRightsWin))
}
_, 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)
}
err = file.Close()
if err != nil {
if errors.Unwrap(err) != nil {
fmt.Println("error while closing file:", err)
}
return nil
@ -423,38 +445,38 @@ func oxGenPrivKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ,
) error {
xmppURI := "xmpp:" + jid
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)
}
if 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)
}
}
keySerialized, err := key.Serialize()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to serialize private key: %w", err)
}
location, err := oxGetPrivKeyLoc(jid)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to get private key location: %w", err)
}
err = oxStoreKey(location,
base64.StdEncoding.EncodeToString(keySerialized))
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
decodedPubKey, err := key.GetPublicKey()
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err)
}
pubKey, err := crypto.NewKey(decodedPubKey)
if err != nil {
if errors.Unwrap(err) != nil {
return fmt.Errorf("oxGenPrivKey: failed to decode public key: %w", err)
}
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 nil
@ -469,37 +491,40 @@ func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, f
opkrPsItems.CreateAttr("node", nsOxPubKeys+":"+fingerprint)
opkrPsItems.CreateAttr("max_items", "1")
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)
}
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)
}
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)
}
oxPublicKeyXML := etree.NewDocument()
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)
}
keyring, err := crypto.NewKeyRing(nil)
if err != nil {
if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxRecvPublicKeys: failed reading public key: %w", err)
}
oxPublicKeyXMLPubsub := oxPublicKeyXML.SelectElement("pubsub")
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")
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")
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")
for _, r := range oxPublicKeyXMLPubkeys {
@ -508,18 +533,18 @@ func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, f
continue
}
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)
}
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)
}
if key.IsExpired() {
return nil, fmt.Errorf("Key is expired: %s", fingerprint)
return nil, errors.New("Key is expired: " + fingerprint)
}
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)
}
}
@ -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) {
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)
}
@ -540,44 +565,44 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
oxPubKeyListReqPsItems.CreateAttr("node", nsOxPubKeys)
oxPubKeyListReqPsItems.CreateAttr("max_items", "1")
opkl, err := oxPubKeyListReq.WriteToString()
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
oxPublicKeyList, err := sendIQ(client, iqc, recipient, "get", opkl)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
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)
}
oxPubKeyListXML := etree.NewDocument()
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)
}
pubKeyRingID := "none"
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)
}
oxPubKeyListXMLPubsub := oxPubKeyListXML.SelectElement("pubsub")
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")
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")
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")
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")
for _, r := range oxPubKeyListXMLPubsubItemsItemPklPm {
@ -590,7 +615,7 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
continue
}
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)
}
if keyDate.After(newestKey) {
@ -599,40 +624,40 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
}
}
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)
if err != nil {
if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to get public key ring location: %w", err)
}
pubKeyReadXML := etree.NewDocument()
err = pubKeyReadXML.ReadFromFile(pubKeyRingLocation)
if err == nil {
if errors.Unwrap(err) == nil {
date := pubKeyReadXML.SelectElement("date")
if date != nil {
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)
}
if !savedKeysDate.Before(newestKey) {
pubKeys := pubKeyReadXML.SelectElements("pubkey")
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 {
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)
}
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)
}
if !key.IsExpired() {
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)
}
}
@ -644,7 +669,7 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
}
}
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)
}
pubKeySaveXML := etree.NewDocument()
@ -652,31 +677,31 @@ func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string)
date.SetText(newestKey.Format(time.RFC3339))
for _, key := range pubKeyRing.GetKeys() {
keySerialized, err := key.Serialize()
if err != nil {
if errors.Unwrap(err) != nil {
return nil, fmt.Errorf("oxGetPublicKeyRing: failed to serialize key: %w", err)
}
saveKey := pubKeySaveXML.CreateElement("pubkey")
saveKey.SetText(base64.StdEncoding.EncodeToString(keySerialized))
}
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 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 == "" {
return "", nil
}
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)
}
ownJid := strings.Split(client.JID(), "/")[0]
if recipient != ownJid {
opk, err := oxPrivKey.GetPublicKey()
if err == nil {
if errors.Unwrap(err) == nil {
ownKey, _ := crypto.NewKey(opk)
_ = keyRing.AddKey(ownKey)
}
@ -692,20 +717,16 @@ func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, key
oxCryptMessageScRpad := oxCryptMessageSc.CreateElement("rpad")
oxCryptMessageScRpad.CreateText(getRpad(len(message)))
oxCryptMessageScPayload := oxCryptMessageSc.CreateElement("payload")
if subject != "" {
oxCryptMessageScPayloadSub := oxCryptMessageScPayload.CreateElement("subject")
oxCryptMessageScPayloadSub.CreateText(subject)
}
oxCryptMessageScPayloadBody := oxCryptMessageScPayload.CreateElement("body")
oxCryptMessageScPayloadBody.CreateAttr("xmlns", nsJabberClient)
oxCryptMessageScPayloadBody.CreateText(message)
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)
}
plainMessage := crypto.NewPlainMessage([]byte(ocm))
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)
}
om := etree.NewDocument()
@ -724,7 +745,7 @@ func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, key
omMessageBody := omMessage.CreateElement("body")
omMessageBody.CreateText(oxAltBody)
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)
}

@ -6,6 +6,7 @@ package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
@ -19,13 +20,13 @@ import (
func findConfig() (string, error) {
// Get the current user.
curUser, err := user.Current()
if err != nil {
if errors.Unwrap(err) != nil {
return "", fmt.Errorf("findConfig: failed to get current user: %w", err)
}
// Get home directory.
home := curUser.HomeDir
if home == "" {
return "", fmt.Errorf("findConfig: no home directory found")
return "", errors.New("no home directory found")
}
osConfigDir := os.Getenv("$XDG_CONFIG_HOME")
if osConfigDir == "" {
@ -40,11 +41,11 @@ func findConfig() (string, error) {
for _, r := range configFiles {
// Check that the config file is existing.
_, err := os.Stat(r)
if err == nil {
if errors.Unwrap(err) == 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
@ -61,7 +62,7 @@ func parseConfig(configPath string) (configuration, error) {
// Get systems user config path.
if configPath == "" {
configPath, err = findConfig()
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
}
@ -69,23 +70,23 @@ func parseConfig(configPath string) (configuration, error) {
// Only check file permissions if we are not running on windows.
if runtime.GOOS != "windows" {
info, err := os.Stat(configPath)
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
// Check for file permissions. Must be 600, 640, 440 or 400.
perm := info.Mode().Perm()
permissions := strconv.FormatInt(int64(perm), 8)
if permissions != "600" && permissions != "640" && permissions != "440" && permissions != "400" {
return output, fmt.Errorf("parseConfig: wrong permissions for %s: %s instead of 400, 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.
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)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
@ -110,7 +111,7 @@ func parseConfig(configPath string) (configuration, error) {
shell = "/bin/sh"
}
out, err := exec.Command(shell, "-c", row[1]).Output()
if err != nil {
if errors.Unwrap(err) != nil {
log.Fatal(err)
}
output.password = string(out)
@ -119,6 +120,9 @@ func parseConfig(configPath string) (configuration, error) {
}
case "port:":
output.port = row[1]
case "resource:":
output.resource = row[1]
fmt.Println(configPath+":", "Deprecated option: resource.")
case "alias:":
output.alias = row[1]
default:
@ -139,17 +143,21 @@ func parseConfig(configPath string) (configuration, error) {
}
}
}
err = file.Close()
if errors.Unwrap(err) != nil {
fmt.Println("error closing file:", err)
}
// Check if the username is a valid JID
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
// server part.
output.username = output.username + "@" + output.jserver
// Check if the username is a valid JID now
output.username, err = MarshalJID(output.username)
if err != nil {
return output, fmt.Errorf("parseConfig: invalid username/JID: %s", output.username)
if errors.Unwrap(err) != nil {
return output, errors.New("invalid username/JID: " + output.username)
}
}

@ -5,32 +5,25 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"runtime"
"time"
"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) {
var iq xmpp.IQ
id := getID()
c := make(chan xmpp.IQ, defaultBufferSize)
c := make(chan xmpp.IQ)
go getIQ(id, c, iqc)
_, err := client.RawInformation(client.JID(), target, id,
iQtype, content)
if err != nil {
iQtype, content+"\n")
if errors.Unwrap(err) != nil {
return iq, fmt.Errorf("sendIQ: failed to send iq: %w", err)
}
select {
case iq = <-c:
case <-time.After(60 * time.Second):
return iq, fmt.Errorf("sendIQ: server didn't reply to IQ: %s", content)
}
iq = <-c
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) {
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
}
}
}()
func rcvStanzas(client *xmpp.Client, iqc chan xmpp.IQ, msgc chan xmpp.Chat) {
for {
select {
case <-ctx.Done():
return
case err := <-e:
if err != nil {
if err != io.EOF {
closeAndExit(client, err)
return
}
return
}
case received = <-r:
received, err := client.Recv()
if errors.Unwrap(err) != nil {
log.Println(err)
}
switch v := received.(type) {
case xmpp.Chat:
msgc <- v
case xmpp.IQ:
switch v.Type {
case "get":
var xmlns *etree.Attr
iq := etree.NewDocument()
err := iq.ReadFromBytes(v.Query)
if err != nil {
err = iq.ReadFromBytes(v.Query)
if errors.Unwrap(err) != nil {
log.Println("Couldn't parse IQ:",
string(v.Query), err)
}
@ -95,9 +60,6 @@ func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc
if query != nil {
xmlns = query.SelectAttr("xmlns")
}
if xmlns == nil {
break
}
switch xmlns.Value {
case nsDiscoInfo:
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")
feat := replyQuery.CreateElement("feature")
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()
if err == nil {
if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString)
if err != nil {
if errors.Unwrap(err) != nil {
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.CreateAttr("xmlns", nsXMPPStanzas)
xmlString, err := root.WriteToString()
if err == nil {
if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString)
if err != nil {
if errors.Unwrap(err) != nil {
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.CreateAttr("xmlns", nsXMPPStanzas)
xmlString, err := root.WriteToString()
if err == nil {
if errors.Unwrap(err) == nil {
_, err = client.SendOrg(xmlString)
if err != nil {
if errors.Unwrap(err) != nil {
log.Println(err)
}
}

Loading…
Cancel
Save