pax_global_header00006660000000000000000000000064152006651610014514gustar00rootroot0000000000000052 comment=55bea0799389b0608929724e4993916ba737bc17 go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/000077500000000000000000000000001520066516100212725ustar00rootroot00000000000000go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/.gitignore000066400000000000000000000004501520066516100232610ustar00rootroot00000000000000config.json .vscode # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/.gitlab-ci.yml000066400000000000000000000077161520066516100237410ustar00rootroot00000000000000# To contribute improvements to CI/CD templates, please follow the Development guide at: # https://docs.gitlab.com/ee/development/cicd/templates.html # This specific template is located at: # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Go.gitlab-ci.yml image: golang:trixie variables: # Please edit to your GitLab project REPO_NAME: salsa.debian.org/mdosch/go-sendxmpp # The problem is that to be able to use go get, one needs to put # the repository in the $GOPATH. So for example if your gitlab domain # is gitlab.com, and that your repository is namespace/project, and # the default GOPATH being /go, then you'd need to have your # repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME stages: - test - build - release format_and_static_test: # image: registry.gitlab.com/gitlab-org/gitlab-build-images:golangci-lint-alpine stage: test before_script: - DEB_RELEASE=$(env -i bash -c '. /etc/os-release; echo $VERSION_CODENAME') - echo "deb https://deb.debian.org/debian/ $DEB_RELEASE-backports main" >> /etc/apt/sources.list - apt -qq update; apt -qq install -y codespell - go install mvdan.cc/gofumpt@latest - go install github.com/securego/gosec/v2/cmd/gosec@latest script: - go fmt $(go list ./... | grep -v /vendor/) - go vet $(go list ./... | grep -v /vendor/) - go test -race $(go list ./... | grep -v /vendor/) - codespell *.go *.md man/*.ronn - gofumpt -d . - gosec ./... # Taken from https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20404/diffs#diff-content-8071267ea32ba69f24a8bd50bcbddf972c295ce3 # Use default .golangci.yml file from the image if one is not present in the project root. #- '[ -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: - DEB_RELEASE=$(env -i bash -c '. /etc/os-release; echo $VERSION_CODENAME') - echo "deb https://deb.debian.org/debian/ $DEB_RELEASE-backports main" >> /etc/apt/sources.list - apt -qq update; apt -qq install -y upx-ucl || true 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=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 - win64/go-sendxmpp.exe - CI_JOB_ID.txt expire_in: 2 years release: stage: release image: registry.gitlab.com/gitlab-org/release-cli:latest only: - tags script: - | 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\":\"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 go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/CHANGELOG.md000066400000000000000000000310041520066516100231010ustar00rootroot00000000000000# Changelog ## [v0.15.8] 2026-05-12 ### Changed - Fix windows build (windows doesn't support syscalls Setgid and Setuid). ## [v0.15.7] 2026-05-12 ### Changed - Fix http-upload with legacy PGP encryption. - Fix reading of environment variables. - Fix a bug in looking up host meta 2. - Try to drop root privileges before connecting to the server. - Fix crash if a config key has no value. - Use a salt for stored FAST token. - Increase scrypt iterations for storing FAST token from 32768 to 65536. - Log a warning when `--no-tls-verify` or `-n` is set. ## [v0.15.6] 2026-05-01 ### Added - New config option `allow_plain`. ### Changed - Explain each configuration option in manpage go-sendxmpp(5). - Improve config parsing robustness. - Update link in manpage as Gitlab calls issues now work items. - Recognize stanza size limit updates after authentication (via go-xmpp >= v0.3.3). - Tell connection target in error message when failing to connect. ## [v0.15.5] 2026-01-01 ### Changed - Fix SASL SCRAM Downgrade Protection in case of server providing different mechanisms for SASL and SASL2 (requires go-xmpp >= v0.3.2). ## [v0.15.4] 2025-12-22 ### Changed - http-upload: Manually set content length for HTTP request (fixes issues with certain http modules/proxies). ## [v0.15.3] 2025-12-11 ### Changed - Fix PLAIN authentication for SASL2 (requires go-xmpp >= v0.3.1). ## [v0.15.2] 2025-12-06 ### Changed - Use UUIDv7 instead of UUIDv4 for stanza IDs (requires go-xmpp >= v0.2.19). - Fix stanza syntax error for legacy PGP messages. - Print error if legacy PGP and Ox are requested simultaneously. - Reworked OOB file sending and http-upload to use new functions from go-xmpp library (requires go-xmpp >= v0.3.0). - Try password login if FAST login fails. ## [v0.15.1] 2025-10-18 ### Added - Add XEP-0359 Origin-ID to messages (requires go-xmpp >= v0.2.18). ### Changed - HTTP upload: Ignore timeouts on disco IQs as some components do not reply. ## [v0.15.0] 2025-08-18 ### Added - Add flag `--verbose` to show debug information. - Add flag `--recipients` to specify recipients by file. - Add flag `--retry-connect` to try after a waiting time if the connection fails. - Add flag `--retry-connect-max` to specify the amount of retry attempts. - Add flag `--legacy-pgp` for using XEP-0027 PGP encryption with Ox keys. - Add support for punycode domains. ### Changed - Update gopenpgp library to v3. - Improve error detection for MUC joins. - Don't try to connect to other SRV record targets if error contains 'auth-failure'. - Remove support for old SSDP version (via go-xmpp v0.2.15). - Http-upload: Stop checking other disco items after finding upload component. - Increase default TLS version to 1.3. ## [v0.14.1] 2025-01-24 ### Changed - Use prettier date format for error messages. - Update XEP-0474 to version 0.4.0 (requires go-xmpp >= 0.2.10). ## [v0.14.0] 2024-12-27 ### Added - Add `--fast-invalidate` to allow invalidating the FAST token. ### Changed - Don't create legacy Ox private key directory in `~/.local/share/go-sendxmpp/oxprivkeys`. - Delete legacy Ox private key directory if it's empty. - Show proper error if saved FAST mechanism isn't usable with current TLS version (requires go-xmpp >= 0.2.9). - Print debug output to stdout, not stderr (requires go-xmpp >= 0.2.9). - Show `RECV:` and `SEND:` prefix for debug output (requires go-xmpp >= 0.2.9). - Delete stored fast token if `--fast-invalidate` and `--fast-off` are set. - Show error when FAST creds are stored but non-FAST mechanism is requested. ## [v0.13.0] 2024-12-14 ### Added - Add `--anonymous` to support anonymous authentication (requires go-xmpp >= 0.2.8). ### Changed - Add `XEP-0480: SASL Upgrade Tasks` support (requires go-xmpp >= 0.2.8). - Add support for `see-other-host` stream error (requires go-xmpp >= 0.2.8). ### Changed - Don't automatically try other auth mechanisms if FAST authentication fails. ## [v0.12.1] 2024-11-17 ### Changed - Print error instead of quitting if a message of type error is received. - Allow upload of multiple files. ### Added - Add flag `--suppress-root-warning` to suppress the warning when go-sendxmpp is used by the root user. ## [v0.12.0] 2024-11-03 ### Added - Add possibility to look up direct TLS connection endpoint via hostmeta2 (requires xmppsrv >= 0.3.3). - Add flag `--allow-plain` to allow PLAIN authentication (requires go-xmpp >= 0.2.5). ### Changed - Disable PLAIN authentication per default. - Disable PLAIN authentication after first use of a SCRAM auth mechanism (overrides `--allow-plain`) (requires go-xmpp >= 0.2.5). ## [v0.11.4] 2024-10-26 ### Changed - Fix bug in SCRAM-SHA-256-PLUS auth (via go-xmpp >= 0.2.4). ## [v0.11.3] 2024-10-25 ### Added - Add go-xmpp library version to `--version` output (requires go-xmpp >= 0.2.2). ### Changed - Fix XEP-0474: SASL SCRAM Downgrade Protection hash calculation bug (via go-xmpp >= 0.2.3). - [gocritic]: Improve code quality. ## [v0.11.2] 2024-09-17 ### Changed - Add Gopenpgp and Xmppsrv version to `--version` output (requires xmppsrv >= 0.3.2). - Improve selection between StartTLS and DirectTLS. ## [v0.11.1] 2024-07-11 ### Changed - Fix Ox encryption in interactive mode (do not add the same recipient key to the keyring over and over again). - Exit with error code if Ox encryption for one recipient fails. - Improved handling of perl sendxmpp config files. ## [v0.11.0] 2024-05-29 ### Changed - Move private Ox key into JID folder in ~/.local/share/go-sendxmpp. - Use `fmt.Errorf()` instead of `errors.New()` to create new error messages. ### Added - Add new parameter `--subject`. - Added flag `--fast-off` to disable XEP-0484: Fast Authentication Streamlining Tokens (requires go-xmpp >= 0.2.1). ## [v0.10.0] 2024-04-13 ### Changed - Fixed a race condition in receiving stanzas (requires go-xmpp >= v0.1.5). ### Added - Add support for SASL2 and BIND2 (via go-xmpp >= v0.2.0). - Add support for FAST authentication (via go-xmpp >= v0.2.0). - Add a warning when run by the user *root*. ## [v0.9.0] 2024-03-28 ### Changed - Properly close stream if `Ctrl+C` is pressed in interactive mode. - Properly close stream if `Ctrl+C` is pressed in listening mode. - Print OS, architecture and go version for flag `--version`. - Improve closing of connection (via go-xmpp v0.1.4). - Don't send stanzas that exceed the size limit provided by XEP-0478 (requires go-xmpp >= v0.1.4). - Fixed hanging forever in stream close if the server doesn't reply with a closing stream element (via go-xmpp >= v0.1.4). ### Added - New command line flag `ssdp-off` to disable XEP-0474: SASL SCRAM Downgrade Protection (requires go-xmpp >= v0.1.4). ## [v0.8.4] 2024-03-09 ### Changed - Properly handle lost connection. - Better compatibility with perl sendxmpp config files. - Improve file name for private Ox keys. - Improve fallback behavior if no SRV records are provided. - Remove 100ms sleep before closing the connection. This should be no more needed since go-xmpp commit 9684a8ff690f0d75e284f8845696c5057926d276. - Return an error if there is no answer to an IQ within 60s. - Check for errors after sending the auth message during SCRAM authentication (via go-xmpp v0.1.2). ## [v0.8.3] 2024-02-17 ### Changed - Use a human readable file name for private Ox keys. - Fix specifying a message via command line flag `-m`. ## [v0.8.2] 2024-01-19 ### Changed - Fix an issue in look up of SRV records (via xmppsrv v0.2.6). Thx mtp. ## [v0.8.1] 2024-01-16 ### Added - Add support for `tls-server-end-point` channel binding (via go-xmpp commit 3f0cbac30767faa562ad198ee69f36055f5924bc). - Add experimental support for SOCKS5 proxies using the `HTTP_PROXY` environment variable (requires go-xmpp commit 685570cbd85c31ea3b426bea34dd4af404aac8cf). ### Changed - http-upload: Improve error handling. ## [v0.8.0] 2024-01-09 ### Added - Add new parameter `--scram-mech-pinning`. ### Changed - Refuse to upload a file if upload slot doesn't provide https. - Use XEP-0474 instead of SCRAM mechanism pinning to prevent downgrade attacks (requires go-xmpp commit 39f5b80375b6f6f266df37b4a4adcbeb606ffec2). ## [v0.7.0] 2023-11-11 ### Added - Reply to XEP-0092 software version requests. - Add support for PLUS variants of SCRAM authentication mechanisms (requires go-xmpp commit 4c385a334c606e8bc387f0a3d4d84975802b3984). - Add pinning of last used authentication mechanism if a SCRAM mechanism was used. ### Changed - Print every stanza in a new line (requires go-xmpp commit 31c7eb6919b67b18e901dc45a8e5681040ea7f31). ## [v0.6.2] 2023-09-29 ### Changed - Properly close connection to server if ^C is pressed in interactive mode. - Replace invalid characters by UTF8 replacement char. - Add warning that there is no Ox support for messages of type headline. - Suppress warnings about reading from closed connection if go-sendxmpp closes the connection before exiting. - Remove unnecessary newlines after stanzas. - Fix segfault when authentication fails due to invalid username or password. ## Removed - Removed deprecated flag and config option `resource`. ## [v0.6.1] 2023-07-25 ### Changed - Properly close connection to server. ## [v0.6.0] 2023-07-02 ### Added - Add support for sending Out of Band Data (OOB). - Add SCRAM-SHA-1, SCRAM-SHA-256 and SCRAM-SHA512 support (via go-xmpp commit bef3e54). - Add support for messages of type headline. ### Changed - Print `--help` output to stdout instead of stderr. - Print sent stanzas when `--debug` is used (via go-xmpp commit 9129a110df1b). - Allow JIDs without localpart. - Use single quotes for attributes in stanzas created by github.com/beevik/etree (vial etree v1.1.4). - Fix SRV lookup when the domain has a CNAME (via xmppsrv v0.2.5). - Fall back to directTLS on port 5223 (instead of StartTLS on port 5222) if no SRV records are provided and `-t` is set. - Remove trailing newlines in interactive mode. ## [v0.5.6] 2022-11-11 ### Added - Add short option `-h` for http-upload. ### Changed - Fix Ox key generation. ## [v0.5.5] 2022-10-16 ### Changed - Improve interactive mode. ## [v0.5.4] 2022-09-30 ### Changed - Fix http-upload. ## [v0.5.3] 2022-09-29 ### Changed - Don't check for empty message when `--listen` is used. ## [v0.5.2] 2022-09-29 ### Added - Reply to `disco#info` queries. - Send `service-unavailable` errors for all other IQs of type `get` and `set`. - New config option and command line flag `alias` to uncouple MUC nickname/alias from resource. ### Changed - Ox: Improve error messages for failed key requests. - Ox: Do not encrypt empty messages. - Check for empty messages. - Always look up CNAME before doing SRV lookups (via xmppsrv v0.2.3). - Detect CNAME loops (max. 5 CNAMEs) (via xmppsrv >= v0.2.4). - Deprecate resource config option and command line flag. - Improve error handling in XML parsing. - Don't connect to the XMPP server if the message is empty. ## [v0.5.1] 2022-05-22 ### Changed - Improve listening mode for groupchats. - Fix nickname in MUCs if not manually set. ## [v0.5.0] 2022-05-05 ### Added - Add possibility to delete existing OpenPGP for XMPP nodes. ### Changed - Ox: Improve rpad generation. - Rework receiving of stanzas. - Ox: Improve private key import. ## [v0.4.0] 2022-04-30 ### Added - Experimental support for Ox (OpenPGP for XMPP) encryption. ### Changed - Shorter random ID for resource. - Changed to the more flexible "github.com/beevik/etree" instead of encoding/xml for creating and interpreting stanzas. - Changed to use message stamp, if provided, for printing the time stamp of received messages. ## [v0.3.0] 2022-03-21 ### Added - Added support for joining password protected MUCs. ### Changed - Removed invalid code points from input. - Fixed crash when reading a config with wrong syntax. - Fixed crash when a non-existing or non-readable config was supplied by `-f`. - Changed config file location from `~/.config/go-sendxmpp/sendxmpprc` to `~/.config/go-sendxmpp/config`. - Fixed blocking of go-sendxmpp if an IQ reply of type "error" is received (via go-xmpp v0.0.0-20220319135856-e773596ea0b0). ## [v0.2.0] 2022-02-12 ### Added - Added listening function. - Added flag to configure connection timeout. - Added flag to configure minimum TLS version. - Added flag to show version. ### Removed - Removed deprecated option `-x`. ## [v0.1.3] 2022-01-29 ### Changed - Rename files to use a limited character set (alpha numerical and some extra characters) file name before uploading. Workaround for https://github.com/mattn/go-xmpp/issues/132 ## [v0.1.2] 2021-11-18 ### Changed - Use xml.Marshal to safely build HTTP Upload request. - Use salsa.debian.org/mdosch/xmppsrv for SRV look ups. ## [v0.1.1] 2021-09-12 ### Changed - Xml-escape file name in http-upload. - Xml-escape mimetype in http-upload. ## [v0.1.0] 2021-09-11 ### Added - Initial release go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/LICENSE000066400000000000000000000024341520066516100223020ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) Martin Dosch All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/LICENSE-mellium000066400000000000000000000024271520066516100237460ustar00rootroot00000000000000Copyright © 2014 The Mellium Contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/README.md000066400000000000000000000171121520066516100225530ustar00rootroot00000000000000# go-sendxmpp ## about A tool to send messages to an XMPP contact or MUC inspired by [sendxmpp](https://sendxmpp.hostname.sk/). You can find other sendxmpp alternatives in the [XSF wiki](https://wiki.xmpp.org/web/User:MDosch/Sendxmpp_incarnations). ## support You might join the [chat](https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join) if you have problems, want to contribute or just want to talk about the project. You might also talk about any of the other sendxmpp incarnations. :) [![Go-sendxmpp MUC badge](https://chat.mdosch.de/muc_badge/go-sendxmpp@chat.mdosch.de)](https://join.jabber.network/#go-sendxmpp@chat.mdosch.de?join) ## installation ### repositories including go-sendxmpp [![Packaging status](https://repology.org/badge/vertical-allrepos/go:sendxmpp.svg)](https://repology.org/project/go:sendxmpp/versions) ### manual installation #### requirements * [go](https://golang.org/) #### instructions Latest release: ```plain $ go install salsa.debian.org/mdosch/go-sendxmpp@latest ``` Current development version: ```plain $ go install salsa.debian.org/mdosch/go-sendxmpp@master ``` You will find the binary in `$GOPATH/bin` or, if set, `$GOBIN`. ### binaries There are some (automatically built and untested) binaries linked to the [release](https://salsa.debian.org/mdosch/go-sendxmpp/-/releases). ## usage 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` if no other configuration file location is specified with `-f` or `--file`. The configuration file is expected to be in the following format: ```plain username: password: ``` If this is not sufficient to connect you might also specify `jserver` and `port`. It is also possible to use a password manager. In this case the `password` setting should be replaced by the `eval_password` setting: ``` eval_password: ``` You can also configure the alias/nickname for MUCs via the `alias` setting: ``` alias: ``` If no configuration file is present or if the values should be overridden it is possible to define the account details via command line options: ```plain Usage: go-sendxmpp [-cdilntv] [-a value] [--allow-plain] [--anonymous] [--fast-invalidate] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [--legacy-pgp] [-m value] [--muc-password value] [--no-sasl-upgrade] [--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] [--retry-connect value] [--retry-connect-max value] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--suppress-root-warning] [--timeout value] [--tls-version value] [-u value] [--version] [recipients…] -a, --alias=value Set alias/nicknamefor chatrooms. --allow-plain Allow PLAIN authentication. --anonymous Use anonymous authentication. -c, --chatroom Send message to a chatroom. -d, --debug Show XMPP stanzas. --fast-invalidate Invalidate XEP-0484: Fast Authentication Streamlining Tokens. --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. --help Show help. -h, --http-upload=value Send a file via http-upload. Can be invoked several times to upload multiple files. -i, --interactive Interactive mode (for use with e.g. 'tail -f'). -j, --jserver=value XMPP server address. --legacy-pgp Use "Legacy PGP" encryption using the Ox key infrastructure (experimental and not encouraged). -l, --listen Listen for messages and print them to stdout. -m, --message=value Set file including the message. --muc-password=value Password for password protected MUCs. --no-sasl-upgrade Disable XEP-0480: SASL Upgrade Tasks. -n, --no-tls-verify Skip verification of TLS certificates (not recommended). --oob-file=value URL to send a file as out of band data. --ox Use "OpenPGP for XMPP" encryption (experimental). --ox-delete-nodes Delete existing OpenPGP nodes on the server. --ox-genprivkey-rsa Generate a private OpenPGP key (RSA 4096 bit) for the given JID and publish the corresponding public key. --ox-genprivkey-x25519 Generate a private OpenPGP key (x25519) for the given JID and publish the corresponding public key. --ox-import-privkey=value Import an existing private OpenPGP key. --ox-passphrase=value Passphrase for locking and unlocking the private OpenPGP key. -p, --password=value Password for XMPP account. --raw Send raw XML. -r, --recipients=value Read recipients from file. --retry-connect=value Time to retry to connect after failed connection in seconds. (0 = disabled) --retry-connect-max=value Number of maximum retries to perform for '--retry-connect'. (0 = unlimited) --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. --suppress-root-warning Suppress warning when run as root. --timeout=value Connection timeout in seconds. [10] -t, --tls Use direct TLS. --tls-version=value Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3). [12] -u, --username=value Username for XMPP account. -v, --verbose Show debug information. --version Show version information. ``` ### examples Send a message to two recipients using a configuration file. ```bash cat message.txt|go-sendxmpp -f ./sendxmpp recipient1@example.com recipient2@example.com ``` 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 ``` Send a message to two groupchats (`-c`) using a configuration file. ```bash cat message.txt|go-sendxmpp -cf ./sendxmpp chat1@conference.example.com chat2@conference.example.com ``` 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."|go-sendxmpp recipient@example.com ``` ### shell completion There are no shell completions yet (contributions welcome) but for zsh it is possible to automatically create completions from `--help` and for fish it is possible to create completions from the man page. Those might work *good enough*. #### zsh Just place the following in your `~/.zshrc`: ``` bash compdef _gnu_generic go-sendxmpp ``` #### fish Fish can generate them from the man page with following command: ``` bash fish_update_completions ``` go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/authpinning.go000066400000000000000000000025331520066516100241500ustar00rootroot00000000000000package main import ( "fmt" "log/slog" "os" "strings" ) func findAuthPinFilePath(username string) (string, error) { slog.Info("getting datapath for", "user", username) dataPath, err := getDataPath(fsFriendlyJid(username), true) if err != nil { return strEmpty, fmt.Errorf("find auth pin file path: %w", err) } return fmt.Sprintf("%sauthpin", dataPath), nil } func parseAuthPinFile(username string) (bool, error) { // Find auth pin file. authPinFile, err := findAuthPinFilePath(username) if err != nil { return false, fmt.Errorf("parse auth pin file: %w", err) } // Read file. slog.Info("reading auth pin", "file", authPinFile) // #nosec G304 -- auth pin file comes from the local system, it's no untrusted input from the network f, err := os.ReadFile(authPinFile) if err != nil { return false, fmt.Errorf("parse auth pin file: %w", err) } // Strip trailing newline. content := strings.TrimSuffix(string(f), "\n") noPLAIN := content == "noPLAIN" slog.Info("parsing content", "noPLAIN", noPLAIN) return noPLAIN, nil } func writeAuthPinFile(username string) error { // Find auth pin file. authPinFile, err := findAuthPinFilePath(username) if err != nil { return fmt.Errorf("write auth pin file: %w", err) } slog.Info("writing auth pin", "file", authPinFile) err = os.WriteFile(authPinFile, []byte("noPLAIN\n"), 0o400) return err } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/connect.go000066400000000000000000000117131520066516100232550ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "fmt" "log/slog" "net" "os" "strings" "time" "github.com/xmppo/go-xmpp" // BSD-3-Clause "golang.org/x/net/idna" // BSD-3-Clause "salsa.debian.org/mdosch/xmppsrv" // BSD-2-Clause ) func connect(options xmpp.Options, directTLS bool, anon bool, retryWait int, retryMax int) (client *xmpp.Client, err error) { var server string proxy := os.Getenv("HTTP_PROXY") slog.Info("getting environment variable:", "HTTP_PROXY", proxy) if !anon { server = options.User[strings.Index(options.User, "@")+1:] } else { server = options.User } retry := true for i := 0; retry; i++ { // Look up SRV records or hostmeta2 if server is not specified manually // or if anon authentication is used. if options.Host == "" || anon { // Don't do SRV or hostmeta2 look ups if proxy is set. // TODO: 2024-10-30: Support hostmeta2 look ups using proxy. if proxy == "" { // Look up xmpp-client SRV records. slog.Info("looking up client SRV records for", "server", server) serverASCII, err := idna.ToASCII(server) if err != nil { return client, err } srvMixed, err := xmppsrv.LookupClient(serverASCII) if len(srvMixed) > 0 && err == nil { for _, adr := range srvMixed { switch { case directTLS && adr.Type == "xmpp-client": slog.Info("skipping as direct TLS is requested:", "type", adr.Type, "target", adr.Target, "port", adr.Port) continue case adr.Type == "xmpp-client": // Use StartTLS options.NoTLS = true slog.Info("received SRV record:", "type", adr.Type, "target", adr.Target, "port", adr.Port) slog.Info("setting xmpp connection option:", "NoTLS", options.NoTLS) slog.Info("setting xmpp connection option:", "StartTLS", options.StartTLS) options.StartTLS = true case adr.Type == "xmpps-client": // Use direct TLS options.NoTLS = false options.StartTLS = false slog.Info("received SRV record::", "type", adr.Type, "target", adr.Target, "port", adr.Port) slog.Info("setting xmpp connection option:", "NoTLS", options.NoTLS) slog.Info("setting xmpp connection option:", "StartTLS", options.StartTLS) default: continue } options.Host = net.JoinHostPort(adr.Target, fmt.Sprint(adr.Port)) // Connect to server slog.Info("connecting to", "host", options.Host) client, err = options.NewClient() if err == nil || strings.Contains(err.Error(), "not-authorized") { // Don't try to connect on other port if error contains // "not-authorized". return client, err } slog.Info("connecting failed") } } // Look up hostmeta2 file. slog.Info("looking up host meta 2 for", "server", server) hm2, httpStatus, err := xmppsrv.LookupHostmeta2(serverASCII) if httpStatus != 404 && err == nil { for _, link := range hm2.Links { if link.Rel == nsC2SdTLS { options.NoTLS = false options.StartTLS = false slog.Info("setting xmpp connection option:", "NoTLS", options.NoTLS) slog.Info("setting xmpp connection option:", "StartTLS", options.StartTLS) options.Host = net.JoinHostPort(link.Sni, fmt.Sprint(link.Port)) // Connect to server slog.Info("connecting to", "host", options.Host) client, err = options.NewClient() if err == nil || strings.Contains(err.Error(), "not-authorized") { // Don't try to connect on other port if error contains // "not-authorized". return client, err } } } } } } _, port, _ := net.SplitHostPort(options.Host) if port == "" { if options.Host == "" { options.Host = server } // Try port 5223 if directTLS is set and no port is provided. if directTLS { options.NoTLS = false options.StartTLS = false options.Host = net.JoinHostPort(options.Host, "5223") slog.Info("trying direct TLS fallback on port 5223") } else { // Try port 5222 if no port is provided and directTLS is not set. options.NoTLS = true options.StartTLS = true options.Host = net.JoinHostPort(options.Host, "5222") slog.Info("trying StartTLS fallback on port 5222") } } // Connect to server slog.Info("connecting to", "host", options.Host) client, err = options.NewClient() if err == nil { return client, err } if retryWait == 0 { slog.Info("giving up trying to connect…") break } if retryMax != 0 && retryMax == i { retry = false } // Reset options.Host as otherwise DNS look up is skipped. options.Host = "" sleepDuration := time.Duration(retryWait) * time.Second slog.Info("connecting failed, retry after", "sleep", sleepDuration) time.Sleep(sleepDuration) slog.Info("connecting to server:", "retry", i) } return client, fmt.Errorf("connect: failed to connect to server %s: %w", options.Host, err) } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/const.go000066400000000000000000000037741520066516100227620ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main const ( version = "0.15.8" // defaults defaultBufferSize = 100 defaultConfigColumnSep = 2 defaultDirRights = 0o700 defaultFileRights = 0o600 defaultFileRightsWin = 0o200 defaultIDBytes = 12 defaultLenServerConf = 2 defaultRpadMultiple = 100 defaultRSABits = 4096 defaultShortIDBytes = 4 defaultTimeout = 10 defaultTLSMinVersion = 13 defaultTLS10 = 10 defaultTLS11 = 11 defaultTLS12 = 12 defaultTLS13 = 13 // namespace nsC2SdTLS = "urn:xmpp:alt-connections:tls" nsDiscoInfo = "http://jabber.org/protocol/disco#info" nsDiscoItems = "http://jabber.org/protocol/disco#items" nsEme = "urn:xmpp:eme:0" nsHints = "urn:xmpp:hints" nsHTTPUpload = "urn:xmpp:http:upload:0" nsJabberClient = "jabber:client" nsJabberData = "jabber:x:data" nsJabberEncrypted = "jabber:x:encrypted" nsOx = "urn:xmpp:openpgp:0" nsOxPubKeys = "urn:xmpp:openpgp:0:public-keys" nsPubsub = "http://jabber.org/protocol/pubsub" nsPubsubOwner = "http://jabber.org/protocol/pubsub#owner" nsSid = "urn:xmpp:sid:0" nsSigned = "jabber:x:signed" nsVersion = "jabber:iq:version" nsXMPPStanzas = "urn:ietf:params:xml:ns:xmpp-stanzas" // strings legacyPGPMsgBegin = "-----BEGIN PGP MESSAGE-----\n\n" legacyPGPMsgEnd = "\n-----END PGP MESSAGE-----" legacyPGPSigBegin = "-----BEGIN PGP SIGNATURE-----\n\n" legacyPGPSigEnd = "\n-----END PGP SIGNATURE-----" 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" strResult = "result" ) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/dropprivs_unix.go000066400000000000000000000024131520066516100247140ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. //go:build !windows package main import ( "fmt" "log/slog" osUser "os/user" "strconv" "syscall" ) func dropPrivs() error { slog.Info("Trying to drop root privileges before connecting to the server") nobody, err := osUser.Lookup("nobody") if err != nil { slog.Info("failed to look up user nobody, will not drop privileges") return fmt.Errorf("failed to drop root privilege: %w", err) } else { gid, err := strconv.Atoi(nobody.Gid) if err != nil { return fmt.Errorf("failed to drop root privilege: %w", err) } uid, err := strconv.Atoi(nobody.Uid) if err != nil { return fmt.Errorf("failed to drop root privilege: %w", err) } err = syscall.Setgid(gid) if err != nil { slog.Info("failed to drop privileges to user nobody", "GID", gid) return fmt.Errorf("failed to drop root privilege: %w", err) } err = syscall.Setuid(uid) if err != nil { slog.Info("failed to drop privileges to user nobody", "UID", uid) return fmt.Errorf("failed to drop root privilege: %w", err) } } slog.Info("user identification", "UID", syscall.Getuid()) slog.Info("user identification", "GID", syscall.Getgid()) return nil } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/dropprivs_windows.go000066400000000000000000000004171520066516100254250ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. //go:build windows package main func dropPrivs() error { // Windows doesn't support syscall.Setuid and syscall.Setgid return nil } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/go.mod000066400000000000000000000010731520066516100224010ustar00rootroot00000000000000module salsa.debian.org/mdosch/go-sendxmpp go 1.25.0 require ( github.com/ProtonMail/gopenpgp/v3 v3.4.1 github.com/beevik/etree v1.6.0 github.com/gabriel-vasile/mimetype v1.4.13 github.com/google/uuid v1.6.0 github.com/pborman/getopt/v2 v2.1.0 github.com/xmppo/go-xmpp v0.3.4 golang.org/x/crypto v0.51.0 golang.org/x/net v0.54.0 salsa.debian.org/mdosch/xmppsrv v0.3.3 ) require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect ) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/go.sum000066400000000000000000000054711520066516100224340ustar00rootroot00000000000000github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/ProtonMail/gopenpgp/v3 v3.4.1 h1:K7uUhSHSJxORZ+RuHpilTT6S4MA2whCRlXNwLqd0+ys= github.com/ProtonMail/gopenpgp/v3 v3.4.1/go.mod h1:bGdV9f6edhmd581wzXsQCTKdH8bXBbyhkgDKPjwPc6U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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/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/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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xmppo/go-xmpp v0.3.4 h1:w7EBGsEzz2TBGJ2FQcOEn0CDzkuJ9CpUBIA+oJB5EuE= github.com/xmppo/go-xmpp v0.3.4/go.mod h1:03Za3Szic72CqdDIdlZtnay9R7t2MWJsj6ZpCvxlGDs= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= salsa.debian.org/mdosch/xmppsrv v0.3.3 h1:F8FGyw1Q1LkAs/UbIXd6Obd33q2CKWrIxxrzvuLSVuM= salsa.debian.org/mdosch/xmppsrv v0.3.3/go.mod h1:udWXnWFa9zkcyN9YSB/u44BCnnRDpeQ0eDy3MVLjHZQ= go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/helpers.go000066400000000000000000000214021520066516100232620ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/gob" "fmt" "log" "log/slog" "math/big" "net/url" "os" "regexp" "runtime" "strings" "time" "github.com/google/uuid" // BSD-3-Clause "github.com/xmppo/go-xmpp" // BSD-3-Clause "golang.org/x/crypto/scrypt" // BSD-3-Clause ) type prettyLogger struct{} func (pl prettyLogger) Write(bytes []byte) (int, error) { return fmt.Print(time.Now().Format("2006-01-02 15:04:05 ") + string(bytes)) } func validUTF8(s string) string { // Remove invalid code points. s = strings.ToValidUTF8(s, "�") reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`) s = reg.ReplaceAllString(s, "�") return s } func validURI(s string) (*url.URL, error) { // Check if URI is valid uri, err := url.ParseRequestURI(s) if err != nil { return uri, fmt.Errorf("valid uri: %w", err) } return uri, nil } func readFile(path string) (*bytes.Buffer, error) { // #nosec G304 -- path comes from the local system, it's no untrusted input from the network file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) } defer func() { derr := file.Close() if derr != nil { slog.Warn("closing file failed", "error", derr) } }() buffer := new(bytes.Buffer) _, err = buffer.ReadFrom(file) if err != nil { return nil, fmt.Errorf("read file: %w", err) } return buffer, nil } func getFastData(jid string, password string, clientID string) (xmpp.Fast, error) { folder := fsFriendlyJid(jid) var fast xmpp.Fast fastPath, err := getDataPath(folder, true) if err != nil { return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache folder: %w", err) } fastFileLoc := fastPath + "fast.bin" slog.Info("reading FAST data:", "file", fastFileLoc) buf, err := readFile(fastFileLoc) if err != nil { return xmpp.Fast{}, fmt.Errorf("get fast data: 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("get fast data: failed to read fast cache file: %w", err) } salt := []byte(clientID) key, err := scrypt.Key([]byte(password), salt, 65536, 8, 1, 32) if err != nil { return xmpp.Fast{}, fmt.Errorf("get fast data: failed to create aes key: %w", err) } c, err := aes.NewCipher([]byte(key)) if err != nil { return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err) } gcm, err := cipher.NewGCM(c) if err != nil { return xmpp.Fast{}, fmt.Errorf("get fast data: 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("get fast data: failed to read fast cache file: %w", err) } fast.Token = string(tokenBuf) return fast, nil } func deleteFastData(jid string) error { folder := fsFriendlyJid(jid) fastPath, err := getDataPath(folder, true) if err != nil { return fmt.Errorf("write fast data: failed to write fast cache file: %w", err) } fastFileLoc := fastPath + "fast.bin" slog.Info("deleting FAST data:", "file", fastFileLoc) return os.Remove(fastFileLoc) } func writeFastData(jid string, password string, clientID string, fast xmpp.Fast) error { var encBuf bytes.Buffer folder := fsFriendlyJid(jid) fastPath, err := getDataPath(folder, true) if err != nil { return fmt.Errorf("write fast data: failed to write fast cache file: %w", err) } fastFileLoc := fastPath + "fast.bin" slog.Info("writing FAST data:", "file", fastFileLoc) salt := []byte(clientID) key, err := scrypt.Key([]byte(password), salt, 65536, 8, 1, 32) if err != nil { return fmt.Errorf("write fast data: failed to create aes cipher: %w", err) } c, err := aes.NewCipher(key) if err != nil { return fmt.Errorf("write fast data: failed to create aes cipher: %w", err) } gcm, err := cipher.NewGCM(c) if err != nil { return fmt.Errorf("write fast data: failed to create aes cipher: %w", err) } nonce := make([]byte, gcm.NonceSize()) _, err = rand.Read(nonce) if err != nil { return fmt.Errorf("write fast data: 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("write fast data: failed to create fast token file: %w", err) } // #nosec G304 -- fast file loc comes from the local system, it's no untrusted input from the network file, err := os.Create(fastFileLoc) if err != nil { return fmt.Errorf("write fast data: failed to create fast token file: %w", err) } defer func() { derr := file.Close() if derr != nil { slog.Warn("closing file failed", "error", derr) } }() 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("write fast data: failed to write fast token file: %w", err) } return nil } func getClientID(jid string) (string, error) { var clientID string folder := fsFriendlyJid(jid) clientIDLoc, err := getClientIDLoc(folder) if err != nil { return strError, err } buf, err := readFile(clientIDLoc) if err != nil { clientID = uuid.NewString() // #nosec G304 -- client ID location comes from the local system, // it's no untrusted input from the network file, err := os.Create(clientIDLoc) if err != nil { return strEmpty, fmt.Errorf("get client id: failed to create clientid file: %w", err) } defer func() { derr := file.Close() if derr != nil { slog.Warn("closing file failed", "error", derr) } }() 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("get client id: failed to write client id file: %w", err) } } else { clientID = buf.String() } return clientID, nil } func getDataPath(folder string, create bool) (string, error) { var err error var homeDir, dataDir string switch { case os.Getenv("XDG_DATA_HOME") != "": dataDir = os.Getenv("XDG_DATA_HOME") slog.Info("using", "XDG_DATA_HOME", dataDir) case os.Getenv("XDG_HOME") != "": homeDir = os.Getenv("XDG_HOME") slog.Info("using", "XDG_HOME", homeDir) dataDir = homeDir + "/.local/share" case os.Getenv("HOME") != "": homeDir = os.Getenv("HOME") slog.Info("using", "HOME", homeDir) dataDir = homeDir + "/.local/share" default: homeDir, err = os.UserHomeDir() if err != nil { return strError, fmt.Errorf("get data path: failed to determine user dir: %w", err) } if homeDir == "" { return strError, fmt.Errorf("get data path: received empty string for home directory") } slog.Info("using", "HOME", homeDir) dataDir = homeDir + "/.local/share" } if folder != "" && !strings.HasSuffix(folder, "/") { folder = fmt.Sprintf("%s/", folder) } dataDir = fmt.Sprintf("%s/go-sendxmpp/%s", dataDir, folder) slog.Info("using data", "dir", dataDir) // #nosec G703 -- data dir comes from the local system, it's no untrusted input from the network _, err = os.Stat(dataDir) if create && os.IsNotExist(err) { // #nosec G703 -- data dir comes from the local system, it's no untrusted input from the network err = os.MkdirAll(dataDir, defaultDirRights) if err != nil { return strError, fmt.Errorf("get data path: could not create folder: %w", err) } } return dataDir, nil } func getClientIDLoc(folder string) (string, error) { dataDir, err := getDataPath(folder, true) if err != nil { return strError, fmt.Errorf("get client id location: %w", err) } dataFile := dataDir + "clientid" slog.Info("using client id", "location", dataDir) return dataFile, nil } func getRpad(messageLength int) string { rpadRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") length := defaultRpadMultiple - messageLength%defaultRpadMultiple max := big.NewInt(int64(len(rpadRunes))) rpad := make([]rune, length) for i := range rpad { randInt, err := rand.Int(rand.Reader, max) if err != nil { log.Fatal(err) } rpad[i] = rpadRunes[randInt.Int64()] } return string(rpad) } func getID() string { id, err := uuid.NewV7() if err != nil { log.Fatal(err) } return id.String() } func getShortID() string { return uuid.NewString()[:6] } // Remove @ and dots func fsFriendlyJid(jid string) string { jid = strings.ReplaceAll(jid, "@", "_at_") return strings.ReplaceAll(jid, ".", "_") } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/helpers_test.go000066400000000000000000000026471520066516100243330ustar00rootroot00000000000000package main import ( "fmt" "testing" ) func TestValidUTF8(t *testing.T) { invalidCodePoints := [...]rune{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, } for _, icp := range invalidCodePoints { teststring := fmt.Sprintf("bla%cbla", icp) cleanstring := validUTF8(teststring) if cleanstring != "bla�bla" { t.Errorf("Expected 'bla�bla', got '%s'", cleanstring) } } } func TestGetRpad(t *testing.T) { rpad := getRpad(10) if len(rpad) != 90 { t.Errorf("Expected a rpad length of 90, got length of %d", len(rpad)) } rpad = getRpad(99) if len(rpad) != 1 { t.Errorf("Expected a rpad length of 1, got length of %d", len(rpad)) } rpad = getRpad(100) if len(rpad) != 100 { t.Errorf("Expected a rpad length of 100, got length of %d", len(rpad)) } rpad = getRpad(101) if len(rpad) != 99 { t.Errorf("Expected a rpad length of 99, got length of %d", len(rpad)) } rpad = getRpad(199) if len(rpad) != 1 { t.Errorf("Expected a rpad length of 1, got length of %d", len(rpad)) } rpad = getRpad(201) if len(rpad) != 99 { t.Errorf("Expected a rpad length of 99, got length of %d", len(rpad)) } } func TestFsFriendlyJid(t *testing.T) { jid := fsFriendlyJid("user@example.org") if jid != "user_at_example_org" { t.Errorf("Expected 'user_at_example_org', got '%s'", jid) } } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/httpupload.go000066400000000000000000000211471520066516100240120ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "bytes" "encoding/xml" "fmt" "log/slog" "net/http" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/ProtonMail/gopenpgp/v3/crypto" // MIT License "github.com/beevik/etree" // BSD-2-clause "github.com/gabriel-vasile/mimetype" // MIT License "github.com/xmppo/go-xmpp" // BSD-3-Clause ) func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, disc chan xmpp.DiscoItems, drc chan xmpp.DiscoResult, slc chan xmpp.Slot, jserver string, filePaths []string, timeout time.Duration, oxPrivKey *crypto.Key, keyRing *crypto.KeyRing, ) (urls []string, err error) { var ( uploadComponent string maxFileSize int64 req *http.Request ) buffer := new(bytes.Buffer) // Query server for disco#items slog.Info("http-upload: querying disco items", "server", jserver) discoItems, err := getDiscoItems(client, disc, jserver) if err != nil { return urls, err } // Check the services reported by disco#items for the http upload service for _, r := range discoItems.Items { slog.Info("http-upload: querying disco info", "disco-item", r.Jid) // Don't check error as it seems some components in the wild do not reply. discoResult, err := getDiscoInfo(client, drc, r.Jid) if err != nil { continue } slog.Info("http-upload: checking identities for type = \"file\" and category = \"store\"") for _, identity := range discoResult.Identities { if identity.Type == "file" && identity.Category == "store" { uploadComponent = r.Jid slog.Info("http-upload:", "upload component", uploadComponent) for _, x := range discoResult.X { for i, field := range x.Field { if field.Var == "max-file-size" { if i > 0 { if x.Field[i-1].Value[0] == nsHTTPUpload { maxFileSize, err = strconv.ParseInt(field.Value[0], 10, 64) if err != nil { return urls, fmt.Errorf("http-upload: error while checking server maximum http upload file size") } slog.Info("http-upload:", "max file size", maxFileSize) break } } } } } break } if uploadComponent != "" { break } } if uploadComponent != "" { break } } if uploadComponent == "" { return urls, fmt.Errorf("http-upload: no http upload component found") } for _, filePath := range filePaths { // Read file // #nosec G304 -- file path is supplied by the user, it's no untrusted input from the network file, err := os.Open(filePath) if err != nil { return urls, fmt.Errorf("http-upload: read file: %w", err) } fileStat, err := file.Stat() if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: stat file: %w", err) } fileSize := fileStat.Size() slog.Info("http-upload: checking file size:", "file", filePath, "size", fileSize) // Get mime type mtype, err := mimetype.DetectFile(filePath) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: detect mimetype: %w", err) } mimeType := mtype.String() slog.Info("http-upload:", "mime type", mimeType) var mimeTypeEscaped bytes.Buffer xml.Escape(&mimeTypeEscaped, []byte(mimeType)) if oxPrivKey != nil { buf, err := readFile(filePath) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return nil, fmt.Errorf("http-upload: %w", err) } buffer, err = legacyPGPEncryptFile(oxPrivKey, keyRing, buf) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } fileSize = int64(buffer.Len()) slog.Info("http-upload: updated file size after encrypting:", "file", filePath, "size", fileSize) } // Get file name fileName := filepath.Base(filePath) if oxPrivKey != nil { fileName = getID() + ".pgp" } // Just use alphanumerical and some special characters for now // to work around https://github.com/xmppo/go-xmpp/issues/132 reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`) fileNameEscaped := reg.ReplaceAllString(fileName, "_") slog.Info("http-upload: escape file", "name", fileNameEscaped) // Check if the file size doesn't exceed the maximum file size of the http upload // component if a maximum file size is reported, if not just continue and hope for // the best. if maxFileSize != 0 { if int64(fileSize) > maxFileSize { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB)", strconv.FormatInt(int64(fileSize)/1024/1024, 10), strconv.FormatInt(maxFileSize/1024/1024, 10)) } } request := etree.NewDocument() request.WriteSettings.AttrSingleQuote = true requestReq := request.CreateElement("request") requestReq.CreateAttr("xmlns", nsHTTPUpload) requestReq.CreateAttr("filename", fileNameEscaped) requestReq.CreateAttr("size", fmt.Sprint(fileSize)) requestReq.CreateAttr("content-type", mimeType) r, err := request.WriteToString() if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } // Request http upload slot slog.Info("http-upload: requesting upload slot") uploadSlot, err := getSlot(client, slc, uploadComponent, r) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } if !strings.HasPrefix(uploadSlot.Put.Url, "https://") { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: upload slot does not provide https") } // Upload file slog.Info("http-upload: uploading file to", "slot", uploadSlot.Put.Url) httpTransport := &http.Transport{ IdleConnTimeout: timeout, TLSHandshakeTimeout: timeout, } proxyEnv := os.Getenv("HTTP_PROXY") slog.Info("http-upload: getting environment variable:", "HTTP_PROXY", proxyEnv) if proxyEnv != "" { proxyURL, err := url.Parse(proxyEnv) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } slog.Info("http-upload: using http transport", "proxy", proxyURL) httpTransport.Proxy = http.ProxyURL(proxyURL) } httpClient := &http.Client{Transport: httpTransport} if oxPrivKey != nil { req, err = http.NewRequest(http.MethodPut, uploadSlot.Put.Url, buffer) } else { req, err = http.NewRequest(http.MethodPut, uploadSlot.Put.Url, file) } if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } req.ContentLength = fileSize req.Header.Set("Content-Type", mimeTypeEscaped.String()) for _, h := range uploadSlot.Put.Headers { if h.Name == strEmpty { continue } switch h.Name { case "Authorization", "Cookie", "Expires": req.Header.Set(h.Name, h.Value) } } resp, err := httpClient.Do(req) if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, err } slog.Info("http-upload: received http", "status", resp.StatusCode) // Test for http status code "200 OK" or "201 Created" if resp.StatusCode != 200 && resp.StatusCode != 201 { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: upload failed") } // Return http link if uploadSlot.Get.Url == strEmpty { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: no url attribute") } if !strings.HasPrefix(uploadSlot.Get.Url, "https://") { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err2) } return urls, fmt.Errorf("http-upload: get url does not provide https") } slog.Info("http-upload: received get", "URL", uploadSlot.Get.Url) err = resp.Body.Close() if err != nil { fmt.Println("http-upload: error while closing http request body:", err) } urls = append(urls, uploadSlot.Get.Url) err = file.Close() if err != nil { slog.Warn("closing file failed", "error", err) } } return urls, nil } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/jid.go000066400000000000000000000047551520066516100224020ustar00rootroot00000000000000// Copyright 2014 The Mellium Contributors. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE-mellium file. // Original taken from mellium.im/xmpp/jid (BSD-2-Clause) and adjusted for my needs. // Copyright Martin Dosch package main import ( "fmt" "strings" "unicode/utf8" ) // MarshalJID checks that JIDs include localpart and serverpart // and return it marshaled. Shamelessly stolen from // mellium.im/xmpp/jid func MarshalJID(input string) (string, error) { var ( err error localpart string domainpart string resourcepart string ) s := input // Remove any portion from the first '/' character to the end of the // string (if there is a '/' character present). sep := strings.Index(s, "/") if sep == -1 { resourcepart = "" } 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) } resourcepart = s[sep+1:] s = s[:sep] } // Remove any portion from the beginning of the string to the first // '@' character (if there is an '@' character present). sep = strings.Index(s, "@") switch sep { case -1: // There is no @ sign, and therefore no localpart. domainpart = s case 0: // The JID starts with an @ sign (invalid empty localpart) err = fmt.Errorf("invalid jid: %s", input) return input, err default: domainpart = s[sep+1:] localpart = s[:sep] } // We'll throw out any trailing dots on domainparts, since they're ignored: // // If the domainpart includes a final character considered to be a label // separator (dot) by [RFC1034], this character MUST be stripped from // the domainpart before the JID of which it is a part is used for the // purpose of routing an XML stanza, comparing against another JID, or // constructing an XMPP URI or IRI [RFC5122]. In particular, such a // character MUST be stripped before any other canonicalization steps // are taken. domainpart = strings.TrimSuffix(domainpart, ".") var jid string if !utf8.ValidString(localpart) || !utf8.ValidString(domainpart) || !utf8.ValidString(resourcepart) { return input, fmt.Errorf("invalid jid: %s", input) } if domainpart == "" { return input, fmt.Errorf("invalid jid: %s", input) } if localpart == "" { jid = domainpart } else { jid = localpart + "@" + domainpart } if resourcepart == "" { return jid, err } return jid + "/" + resourcepart, err } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/jid_test.go000066400000000000000000000011471520066516100234310ustar00rootroot00000000000000package main import ( "testing" ) func TestMarshalJID(t *testing.T) { jids := [...]string{"user@example.com", "user@example.com."} for _, jid := range jids { marshalledJid, err := MarshalJID(jid) if err != nil { t.Errorf("Failed to marshal JID: %v", err) } if marshalledJid != "user@example.com" { t.Errorf("Expected 'user@example.com', got '%s'", marshalledJid) } } badJids := [...]string{ "@user@example.com.", "@user@example.com", "user@example.com/", } for _, jid := range badJids { _, err := MarshalJID(jid) if err == nil { t.Errorf("Expected error, but got none") } } } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/legacypgp.go000066400000000000000000000131671520066516100236040ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. // TODO: Add more logging package main import ( "bytes" "fmt" "log/slog" "strings" "time" "github.com/ProtonMail/gopenpgp/v3/crypto" // MIT License "github.com/beevik/etree" // BSD-2-clause "github.com/xmppo/go-xmpp" // BSD-3-Clause ) func legacyPGPEncryptFile(oxPrivKey *crypto.Key, keyRing *crypto.KeyRing, file *bytes.Buffer) (encryptedFileBuffer *bytes.Buffer, err error) { pgpEncrypt, err := crypto.PGP().Encryption(). Recipients(keyRing). SigningKey(oxPrivKey).New() if err != nil { return encryptedFileBuffer, err } encryptedFile, err := pgpEncrypt.Encrypt(file.Bytes()) if err != nil { return encryptedFileBuffer, err } encryptedFileArmored, err := encryptedFile.ArmorBytes() if err != nil { return encryptedFileBuffer, err } encryptedFileBuffer = bytes.NewBuffer(encryptedFileArmored) return encryptedFileBuffer, err } func legacyPGPEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string, keyRing crypto.KeyRing, message string) (string, error) { const statusOnline = "Online" if message == "" { return "", nil } ownJid := strings.Split(client.JID(), "/")[0] if recipient != ownJid { opk, err := oxPrivKey.GetPublicKey() if err == nil { ownKey, _ := crypto.NewKey(opk) _ = keyRing.AddKey(ownKey) } } id := getID() legacyPGP := etree.NewDocument() legacyPGP.WriteSettings.AttrSingleQuote = true legacyPGPMsg := legacyPGP.CreateElement("message") legacyPGPMsg.CreateAttr("id", id) legacyPGPMsg.CreateAttr("to", recipient) legacyPGPMsgBody := legacyPGPMsg.CreateElement("body") legacyPGPMsgBody.CreateText("This message is encrypted using XEP-0027 PGP encryption.") legacyPGPMsgX := legacyPGPMsg.CreateElement("x") legacyPGPMsgX.CreateAttr("xmlns", nsJabberEncrypted) legacyPGPMsgEnc := legacyPGPMsg.CreateElement("encryption") legacyPGPMsgEnc.CreateAttr("namespace", nsJabberEncrypted) legacyPGPMsgEnc.CreateAttr("xmlns", nsEme) legacyPGPMsgOrigID := legacyPGPMsg.CreateElement("origin-id") legacyPGPMsgOrigID.CreateAttr("xmlns", nsSid) legacyPGPMsgOrigID.CreateAttr("id", id) pgpEncrypt, err := crypto.PGP().Encryption(). Recipients(&keyRing). SigningKey(oxPrivKey).New() if err != nil { return strError, fmt.Errorf("legacyPGPEncrypt: failed to create pgp encryption interface: %w", err) } pgpMessage, err := pgpEncrypt.Encrypt([]byte(message)) if err != nil { return strError, fmt.Errorf("legacyPGPEncrypt: failed to create pgp message: %w", err) } pgpMessageArmored, err := pgpMessage.Armor() pgpMessageArmored = strings.TrimPrefix(pgpMessageArmored, legacyPGPMsgBegin) pgpMessageArmored = strings.TrimSuffix(pgpMessageArmored, legacyPGPMsgEnd) if err != nil { return strError, fmt.Errorf("legacyPGPEncrypt: failed to create pgp message: %w", err) } legacyPGPMsgX.CreateText(pgpMessageArmored) lpm, err := legacyPGP.WriteToString() if err != nil { return strError, fmt.Errorf("legacyPGPEncrypt: failed to create xml for ox message: %w", err) } signer, err := crypto.PGP().Sign().SigningKey(oxPrivKey).Detached().New() if err != nil { return strError, err } signedStatusWithHeader, err := signer.Sign([]byte(statusOnline), crypto.Armor) if err != nil { return strError, err } signedStatus := strings.TrimPrefix(string(signedStatusWithHeader), legacyPGPSigBegin) signedStatus = strings.TrimSuffix(signedStatus, legacyPGPSigEnd) signedPresence := fmt.Sprintf("%s%s", recipient, ownJid, statusOnline, nsSigned, signedStatus) _, err = client.SendOrg(signedPresence) if err != nil { return strError, err } return lpm, nil } func isLegacyPGPMsg(m xmpp.Chat) bool { slog.Info("legacy pgp: checking pgp message") for _, r := range m.OtherElem { slog.Info("legacy pgp: checking for pgp message: comparing", "XMLNameSpace", r.XMLName.Space, "LegacyPGPNameSpace", nsJabberEncrypted) if r.XMLName.Space == nsJabberEncrypted { slog.Info("legacy pgp: checking for pgp message:", "result", true) return true } } slog.Info("legacy pgp: checking for pgp message:", "result", false) return false } func legacyPGPDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string, oxPrivKey *crypto.Key) (string, time.Time, error) { var legacyPGPMsg *crypto.PGPMessage var err error sender := strings.Split(m.Remote, "/")[0] for _, r := range m.OtherElem { if r.XMLName.Space == nsJabberEncrypted { armored := legacyPGPMsgBegin + r.InnerXML + legacyPGPMsgEnd legacyPGPMsg, err = crypto.NewPGPMessageFromArmored(armored) if err != nil { return strError, time.Now(), err } break } } if legacyPGPMsg == nil { return strError, time.Now(), fmt.Errorf("legacypgp: could net unarmor pgp message") } keyRing, err := crypto.NewKeyRing(oxPrivKey) if err != nil { return strError, time.Now(), err } slog.Info("legacy pgp: decrypting: getting senders key", "sender", sender) senderKeyRing, err := oxGetPublicKeyRing(client, iqc, sender) if err != nil { return strError, time.Now(), err } slog.Info("legacy pgp: decrypting message") pgpDecrypt, err := crypto.PGP().Decryption(). DecryptionKeys(keyRing). VerificationKeys(senderKeyRing). New() if err != nil { return strError, time.Now(), err } decryptMsg, err := pgpDecrypt.Decrypt(legacyPGPMsg.Bytes(), crypto.Bytes) if err != nil { return strError, time.Now(), err } // Remove invalid code points. slog.Info("legacy pgp: removing invalid code points from decrypted message") message := validUTF8(decryptMsg.String()) return message, time.Now(), nil } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/main.go000066400000000000000000001075311520066516100225540ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "bufio" "context" "crypto/tls" "fmt" "io" "log" "log/slog" "net" "os" "os/signal" osUser "os/user" "runtime" "strings" "syscall" "time" gopenpgpConst "github.com/ProtonMail/gopenpgp/v3/constants" // MIT License "github.com/ProtonMail/gopenpgp/v3/crypto" // MIT License "github.com/pborman/getopt/v2" // BSD-3-Clause "github.com/xmppo/go-xmpp" // BSD-3-Clause "golang.org/x/net/idna" // BSD-3-Clause "salsa.debian.org/mdosch/xmppsrv" // BSD-2-Clause ) type configuration struct { username string jserver string port string password string alias string allowPLAIN bool } func closeAndExit(client *xmpp.Client, err error) { slog.Info("closing connection and exiting") _ = client.Close() if err != nil { log.Fatal(err) } os.Exit(0) } func readFileByLine(filePath string) (string, error) { var ( output string err error ) // Check that the file is existing. _, err = os.Stat(filePath) if err != nil { return output, fmt.Errorf("read file by line: %w", err) } // Open message file. slog.Info("opening file", "file", filePath) // #nosec G304 -- file path is supplied by the user, it's no untrusted input from the network file, err := os.Open(filePath) if err != nil { return output, fmt.Errorf("read file by line: %w", err) } defer func() { derr := file.Close() if derr != nil { slog.Warn("closing file failed", "error", derr) } }() scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) for scanner.Scan() { if output == "" { output = scanner.Text() } else { output = output + "\n" + scanner.Text() } } if err = scanner.Err(); err != nil { if err != io.EOF { return "", fmt.Errorf("read file by line: %w", err) } } return output, nil } func main() { type recipientsType struct { Jid string OxKeyRing *crypto.KeyRing } var ( err error message, user, server, password, alias string allowPLAIN, isRoot bool oxPrivKey *crypto.Key recipients []recipientsType fast xmpp.Fast // There are some errors that we ignore as we do not want to // stop the execution. Failure is used to track those to exit // with a non-success return value. failure error ) log.SetFlags(0) log.SetOutput(new(prettyLogger)) slog.SetLogLoggerLevel(slog.LevelWarn) // Define command line flags. flagHelp := getopt.BoolLong("help", 0, "Show help.") flagHTTPUpload := getopt.ListLong("http-upload", 'h', "Send a file via http-upload. Can be invoked several times to upload multiple files.") flagDebug := getopt.BoolLong("debug", 'd', "Show XMPP stanzas.") flagVerbose := getopt.BoolLong("verbose", 'v', "Show debug information.") flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.") flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.") flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.") flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.") flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.") flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+ "for chatrooms.") flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+ "~/.config/go-sendxmpp/sendxmpprc)") flagMessageFile := getopt.StringLong("message", 'm', "", "Set file including the message.") flagInteractive := getopt.BoolLong("interactive", 'i', "Interactive mode (for use with e.g. 'tail -f').") flagSkipVerify := getopt.BoolLong("no-tls-verify", 'n', "Skip verification of TLS certificates (not recommended).") flagRaw := getopt.BoolLong("raw", 0, "Send raw XML.") flagListen := getopt.BoolLong("listen", 'l', "Listen for messages and print them to stdout.") flagTimeout := getopt.IntLong("timeout", 0, defaultTimeout, "Connection timeout in seconds.") flagTLSMinVersion := getopt.IntLong("tls-version", 0, defaultTLSMinVersion, "Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).") flagVersion := getopt.BoolLong("version", 0, "Show version information.") flagMUCPassword := getopt.StringLong("muc-password", 0, "", "Password for password protected MUCs.") flagOx := getopt.BoolLong("ox", 0, "Use \"OpenPGP for XMPP\" encryption (experimental).") flagOxGenPrivKeyRSA := getopt.BoolLong("ox-genprivkey-rsa", 0, "Generate a private OpenPGP key (RSA 4096 bit) for the given JID and publish the "+ "corresponding public key.") flagOxGenPrivKeyX25519 := getopt.BoolLong("ox-genprivkey-x25519", 0, "Generate a private OpenPGP key (x25519) for the given JID and publish the "+ "corresponding public key.") flagOxPassphrase := getopt.StringLong("ox-passphrase", 0, "", "Passphrase for locking and unlocking the private OpenPGP key.") flagOxImportPrivKey := getopt.StringLong("ox-import-privkey", 0, "", "Import an existing private OpenPGP key.") 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.") flagFastInvalidate := getopt.BoolLong("fast-invalidate", 0, "Invalidate XEP-0484: Fast Authentication Streamlining Tokens.") flagPLAINAllow := getopt.BoolLong("allow-plain", 0, "Allow PLAIN authentication.") flagNoRootWarning := getopt.BoolLong("suppress-root-warning", 0, "Suppress warning when run as root.") flagAnon := getopt.BoolLong("anonymous", 0, "Use anonymous authentication (specify the target anon service as username.") flagNoSASLUpgrade := getopt.BoolLong("no-sasl-upgrade", 0, "Disable XEP-0480: SASL Upgrade Tasks.") flagRecipientsFile := getopt.StringLong("recipients", 'r', "", "Read recipients from file.") flagRetryConnect := getopt.IntLong("retry-connect", 0, 0, "Time to retry to connect after failed connection in seconds. (0 = disabled)") flagRetryConnectMax := getopt.IntLong("retry-connect-max", 0, 0, "Number of maximum retries to perform for '--retry-connect'. (0 = unlimited)") flagLegacyPGP := getopt.BoolLong("legacy-pgp", 0, "Use \"Legacy PGP\" encryption using the Ox key infrastructure (experimental and not encouraged).") // Parse command line flags. getopt.Parse() switch { case *flagHelp: // If requested, show help and quit. getopt.PrintUsage(os.Stdout) os.Exit(0) case *flagVersion: // If requested, show version and quit. fmt.Println("Go-sendxmpp", version) fmt.Println("Go-xmpp library version:", xmpp.Version) fmt.Println("Xmppsrv library version:", xmppsrv.Version) fmt.Println("Gopenpgp library version:", gopenpgpConst.Version) system := runtime.GOOS + "/" + runtime.GOARCH fmt.Println("System:", system, runtime.Version()) fmt.Println("License: BSD-2-clause") os.Exit(0) // Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like // groupchat, http-upload or listening. case *flagOx && len(*flagHTTPUpload) != 0: log.Fatal("No Ox support for http-upload available.") case (*flagOx || *flagLegacyPGP) && *flagChatroom: log.Fatal("No Ox or legacy PGP support for chat rooms available.") case len(*flagHTTPUpload) != 0 && *flagInteractive: log.Fatal("Interactive mode and http upload can't" + " be used at the same time.") case len(*flagHTTPUpload) != 0 && *flagMessageFile != "": log.Fatal("You can't send a message while using" + " http upload.") case *flagOx && *flagLegacyPGP: log.Fatal("Ox and legacy PGP can't be used simultaneously.") case (*flagOx || *flagLegacyPGP) && *flagOOBFile != "": log.Fatal("No encryption possible for OOB data.") case (*flagOx || *flagLegacyPGP) && *flagHeadline: log.Fatal("No Ox or legacy PGP support for headline messages.") case *flagLegacyPGP && *flagSubject != "": log.Fatal("No legacy PGP support for subject.") case *flagHeadline && *flagChatroom: log.Fatal("Can't use message type headline for groupchat messages.") case *flagAnon && *flagUser == "": log.Fatal("Specifying a username is required when using anonymous authentication.") } if *flagVerbose { slog.SetLogLoggerLevel(slog.LevelDebug) } // Print a warning if go-sendxmpp is run by the user root on non-windows systems. if runtime.GOOS != "windows" && !*flagNoRootWarning { // Get the current user. slog.Info("checking for current OS user") currUser, err := osUser.Current() if err != nil { log.Fatal("Failed to get current user: ", err) } slog.Info("checking if OS user is root") slog.Info("user identification", "UID", syscall.Getuid()) slog.Info("user identification", "GID", syscall.Getgid()) if currUser.Username == "root" { fmt.Println("WARNING: It seems you are running go-sendxmpp as root user.\n" + "This 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.") // Logging that we're running as root isRoot = true *flagFastOff = true slog.Info("disabling fast as we're running as root") *flagOx = false slog.Info("disabling OX as we're running as root") } } switch *flagSCRAMPinning { case "", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS", "SCRAM-SHA-512", "SCRAM-SHA-512-PLUS": slog.Info("SCRAM pinning", "mechanism", *flagSCRAMPinning) default: log.Fatal("Unknown SCRAM mechanism: ", *flagSCRAMPinning) } // Read recipients from command line, if no recipients list is specified, 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). var recipientsList []string if *flagRecipientsFile == "" { slog.Info("getting recipients from command line") recipientsList = getopt.Args() } else { slog.Info("getting recipients from recipients file") rl, err := readFileByLine(*flagRecipientsFile) if err != nil { log.Fatal(err) } if strings.Contains(rl, " ") { recipientsList = strings.Split(rl, " ") } else { recipientsList = strings.Split(rl, "\n") } } slog.Info("determining", "recipients", recipientsList) if (len(recipientsList) == 0 && !*flagRaw && !*flagListen && !*flagOxGenPrivKeyX25519 && !*flagOxGenPrivKeyRSA && *flagOxImportPrivKey == "" && !*flagFastInvalidate) && !*flagOxDeleteNodes || (len(recipientsList) == 0 && *flagChatroom) { log.Fatal("No recipient specified.") } // Read configuration file if no user or password is specified and anonymous // authentication is not used. if (*flagUser == "" || *flagPassword == "") && !*flagAnon { // Read configuration from file. slog.Info("reading config from command line", "flag", *flagFile) config, err := parseConfig(*flagFile) if err != nil { log.Fatal("Error parsing ", *flagFile, ": ", err) } // Set connection options according to config. user = config.username slog.Info("config", "user", config.username) server = config.jserver slog.Info("config", "jserver", config.jserver) password = config.password slog.Info("config", "password", "REDACTED") alias = config.alias slog.Info("config", "alias", config.alias) if config.port != "" { slog.Info("config", "port", config.port) server = net.JoinHostPort(server, fmt.Sprint(config.port)) slog.Info("setting", "server", server) } allowPLAIN = config.allowPLAIN slog.Info("config", "allow-plain", config.allowPLAIN) } // Overwrite user if specified via command line flag. if *flagUser != "" { user = *flagUser slog.Info("overwriting config value by command line flag", "user", *flagUser) } // Overwrite server if specified via command line flag. if *flagServer != "" { server = *flagServer slog.Info("overwriting config value by command line flag", "server", *flagServer) } // Overwrite password if specified via command line flag. if *flagPassword != "" && !*flagAnon { password = *flagPassword slog.Info("overwriting config value by command line flag", "password", "REDACTED") } // 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 != "" && !*flagAnon { slog.Info("user seems to be no full JID") // Remove port if server contains it. if strings.Contains(server, ":") { serverpart, _, err = net.SplitHostPort(server) if err != nil { log.Fatal(err) } } else { serverpart = server } slog.Info("creating JID with", "userpart", user, "serverpart", serverpart) user = user + "@" + serverpart } switch { // Use "go-sendxmpp" if no nick is specified via config or command line flag. case *flagChatroom && alias == "" && *flagAlias == "": slog.Info("no alias defined, using go-sendxmpp") alias = "go-sendxmpp" // Overwrite configured alias if a nick is specified via command line flag. case *flagAlias != "": alias = *flagAlias slog.Info("overwriting config value by command line flag", "alias", *flagAlias) } // Timeout timeout := time.Duration(*flagTimeout) * time.Second slog.Info("setting", "timeout", timeout) slog.Info("creating client ID") clientID, err := getClientID(user) if err != nil { fmt.Println(err) } if !*flagFastOff { slog.Info("getting FAST data") fast, err = getFastData(user, password, clientID) if err != nil { slog.Info("no FAST data found") } // Reset FAST token and mechanism if expired or // FastInvalidate is set. if time.Now().After(fast.Expiry) || *flagFastInvalidate { slog.Info("invalidating fast data:", "token expired", time.Now().After(fast.Expiry), "flag FAST invalidate", *flagFastInvalidate) fast.Token = "" fast.Mechanism = "" } } if fast.Mechanism != "" && *flagSCRAMPinning != "" { log.Fatalf("FAST: %s is requested, but we have a token for %s.", *flagSCRAMPinning, fast.Mechanism) } var tlsConfig tls.Config jserver := user[strings.Index(user, "@")+1:] if !*flagAnon { tlsConfig.ServerName, err = idna.ToASCII(jserver) if err != nil { log.Fatal(err) } slog.Info("setting TLS config:", "ServerName", tlsConfig.ServerName) } else { tlsConfig.ServerName, err = idna.ToASCII(*flagUser) if err != nil { log.Fatal(err) } slog.Info("setting TLS config:", "ServerName", tlsConfig.ServerName) *flagSCRAMPinning = "ANONYMOUS" slog.Info("setting SCRAM", "mechanism", *flagSCRAMPinning) } // Use ALPN tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client") slog.Info("setting TLS config:", "NextProtos", tlsConfig.NextProtos) if *flagSkipVerify { // #nosec G402 -- user must explicitly enable it and a warning is logged tlsConfig.InsecureSkipVerify = *flagSkipVerify slog.Warn("TLS config INSECURE:", "InsecureSkipVerify", tlsConfig.InsecureSkipVerify) } else { slog.Info("setting TLS config:", "InsecureSkipVerify", tlsConfig.InsecureSkipVerify) } tlsConfig.Renegotiation = tls.RenegotiateNever slog.Info("setting TLS config:", "Renegotiation", tlsConfig.Renegotiation) switch *flagTLSMinVersion { case defaultTLS10: slog.Info("setting TLS config:", "MinVersion", defaultTLS10) tlsConfig.MinVersion = tls.VersionTLS10 case defaultTLS11: slog.Info("setting TLS config:", "MinVersion", defaultTLS11) tlsConfig.MinVersion = tls.VersionTLS11 case defaultTLS12: slog.Info("setting TLS config:", "MinVersion", defaultTLS12) tlsConfig.MinVersion = tls.VersionTLS12 case defaultTLS13: slog.Info("setting TLS config:", "MinVersion", defaultTLS13) tlsConfig.MinVersion = tls.VersionTLS13 default: fmt.Println("Unknown TLS version.") os.Exit(0) } resource := "go-sendxmpp." + getShortID() // Check whether PLAIN authentication is disabled. pinNoPLAIN, _ := parseAuthPinFile(user) noPLAIN := pinNoPLAIN || (!*flagPLAINAllow && !allowPLAIN) // Periodic server ping settings psp := true pspPeriod := 30000 // Set XMPP connection options. options := xmpp.Options{ Host: server, User: user, DialTimeout: timeout, Resource: resource, Password: password, // NoTLS doesn't mean that no TLS is used at all but that instead // of using an encrypted connection to the server (direct TLS) // an unencrypted connection is established. As StartTLS is // set when NoTLS is set go-sendxmpp won't use unencrypted // client-to-server connections. // See https://pkg.go.dev/github.com/xmppo/go-xmpp#Options NoTLS: !*flagDirectTLS, StartTLS: !*flagDirectTLS, Debug: *flagDebug, DebugWriter: os.Stdout, TLSConfig: &tlsConfig, Mechanism: *flagSCRAMPinning, SSDP: !*flagSSDPOff, UserAgentSW: resource, UserAgentID: clientID, Fast: !*flagFastOff, FastToken: fast.Token, FastMechanism: fast.Mechanism, FastInvalidate: *flagFastInvalidate, NoPLAIN: noPLAIN, NoSASLUpgrade: *flagNoSASLUpgrade, PeriodicServerPings: psp, PeriodicServerPingsPeriod: pspPeriod, ReportSoftwareVersion: true, SoftwareName: "go-sendxmpp", SoftwareVersion: version, ReportSoftwareOS: true, } slog.Info("setting xmpp connection option:", "Host", server) slog.Info("setting xmpp connection option:", "User", user) slog.Info("setting xmpp connection option:", "DialTimeout", timeout) slog.Info("setting xmpp connection option:", "Resource", resource) slog.Info("setting xmpp connection option:", "Password", "REDACTED") slog.Info("setting xmpp connection option:", "NoTLS", !*flagDirectTLS) slog.Info("setting xmpp connection option:", "StartTLS", !*flagDirectTLS) slog.Info("setting xmpp connection option:", "Debug", *flagDebug) slog.Info("setting xmpp connection option:", "Mechanism", *flagSCRAMPinning) slog.Info("setting xmpp connection option:", "SSDP", !*flagSSDPOff) slog.Info("setting xmpp connection option:", "UserAgentSW", resource) slog.Info("setting xmpp connection option:", "UserAgentID", clientID) slog.Info("setting xmpp connection option:", "Fast", !*flagFastOff) slog.Info("setting xmpp connection option:", "FastToken", "REDACTED") slog.Info("setting xmpp connection option:", "FastMechanism", fast.Mechanism) slog.Info("setting xmpp connection option:", "FastInvalidate", *flagFastInvalidate) slog.Info("setting xmpp connection option:", "NoSASLUpgrade", *flagNoSASLUpgrade) slog.Info("setting xmpp connection option:", "NoPLAIN", noPLAIN) slog.Info("setting xmpp connection option:", "PeriodicServerPings", psp) slog.Info("setting xmpp connection option:", "PeriodicServerPingsPeriod", pspPeriod) // Read message from file. if *flagMessageFile != "" { slog.Info("reading message from message file") message, err = readFileByLine(*flagMessageFile) if err != nil { log.Fatal(err) } } // Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'. // Also for listening mode and Ox key handling. if !*flagInteractive && !*flagListen && len(*flagHTTPUpload) == 0 && !*flagOxDeleteNodes && *flagOxImportPrivKey == "" && !*flagOxGenPrivKeyX25519 && !*flagOxGenPrivKeyRSA && *flagOOBFile == "" && !*flagFastInvalidate && message == "" { slog.Info("reading message from stdin") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if message == "" { message = scanner.Text() slog.Info("reading message", "line", scanner.Text()) } else { message = message + "\n" + scanner.Text() slog.Info("reading message", "line", scanner.Text()) } } if err := scanner.Err(); err != nil { if err != io.EOF { log.Fatal(err) } } } // Remove invalid code points. slog.Info("removing invalid code points from message") message = validUTF8(message) // Exit if message is empty. if message == "" && !*flagInteractive && !*flagListen && !*flagOxGenPrivKeyRSA && !*flagOxGenPrivKeyX25519 && *flagOxImportPrivKey == "" && !*flagOxDeleteNodes && len(*flagHTTPUpload) == 0 && *flagOOBFile == "" && !*flagFastInvalidate { slog.Info("exiting due to empty message") os.Exit(0) } if isRoot { _ = dropPrivs() } // Connect to server. client, err := connect(options, *flagDirectTLS, *flagAnon, *flagRetryConnect, *flagRetryConnectMax) if err != nil { if fast.Token != "" { slog.Info("failed connecting using fast", "mechanism", options.FastMechanism, "token", options.FastToken) options.FastToken = "" options.FastMechanism = "" client, err = connect(options, *flagDirectTLS, *flagAnon, *flagRetryConnect, *flagRetryConnectMax) if err != nil { log.Fatal(err) } } else { log.Fatal(err) } } slog.Info("connection successful") // 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 slog.Info("updating FAST:", "token", "REDACTED") fast.Mechanism = client.Fast.Mechanism slog.Info("updating FAST:", "mechanism", fast.Mechanism) fast.Expiry = client.Fast.Expiry slog.Info("updating FAST:", "expiry", fast.Expiry) slog.Info("writing FAST data to disk") err := writeFastData(user, password, clientID, fast) if err != nil { fmt.Println(err) } } // Delete stored fast data when FastInvalidate is set. if *flagFastInvalidate && *flagFastOff { slog.Info("deleting FAST data") err := deleteFastData(user) if err != nil { fmt.Println(err) } } // If authentication is not yet pinned to not use PLAIN and a SCRAM mechanism is // used, write the auth pin file. if !pinNoPLAIN && (strings.HasPrefix(client.Mechanism, "SCRAM") || strings.HasPrefix(client.Mechanism, "HT")) { slog.Info("writing auth pin file") err = writeAuthPinFile(user) if err != nil { log.Println("could not write authentication mechanism pin file:", err) } } iqc := make(chan xmpp.IQ, defaultBufferSize) msgc := make(chan xmpp.Chat, defaultBufferSize) prsc := make(chan xmpp.Presence, defaultBufferSize) disc := make(chan xmpp.DiscoItems, defaultBufferSize) drc := make(chan xmpp.DiscoResult, defaultBufferSize) slc := make(chan xmpp.Slot, defaultBufferSize) ctx, cancel := context.WithCancel(context.Background()) slog.Info("starting to receive stanzas") go rcvStanzas(client, ctx, iqc, msgc, prsc, disc, drc, slc) for _, r := range recipientsList { var re recipientsType re.Jid = r if *flagOx || *flagLegacyPGP { slog.Info("requesting OX key for", "JID", re.Jid) re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r) if err != nil { re.OxKeyRing = nil fmt.Printf("ox: error fetching key for %s: %v\n", r, err) failure = err } } recipients = append(recipients, re) } // Check that all recipient JIDs are valid. for i, recipient := range recipients { slog.Info("checking validity", "JID", recipient.Jid) validatedJid, err := MarshalJID(recipient.Jid) if err != nil { cancel() closeAndExit(client, err) } recipients[i].Jid = validatedJid } switch { case *flagOxGenPrivKeyX25519: slog.Info("checking validity", "JID", user) validatedOwnJid, err := MarshalJID(user) if err != nil { cancel() closeAndExit(client, err) } slog.Info("generating X25519 private OX key") err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, crypto.KeyGenerationCurve25519Legacy) // crypto.KeyGenerationCurve25519) // TODO: Clarify whether RFC9580 can also be used instead of RFC4880bis // for OX keys. if err != nil { cancel() closeAndExit(client, err) } os.Exit(0) case *flagOxGenPrivKeyRSA: slog.Info("checking validity", "JID", user) validatedOwnJid, err := MarshalJID(user) if err != nil { cancel() closeAndExit(client, err) } slog.Info("generating RSA4096 private OX key") err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, crypto.KeyGenerationRSA4096) if err != nil { cancel() closeAndExit(client, err) } os.Exit(0) case *flagOxImportPrivKey != "": slog.Info("checking validity", "JID", user) validatedOwnJid, err := MarshalJID(user) if err != nil { cancel() closeAndExit(client, err) } slog.Info("importing private OX key:", "file", *flagOxImportPrivKey) err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey, client, iqc) if err != nil { cancel() closeAndExit(client, err) } os.Exit(0) case *flagOxDeleteNodes: slog.Info("checking validity", "JID", user) validatedOwnJid, err := MarshalJID(user) if err != nil { cancel() closeAndExit(client, err) } slog.Info("delete OX pep nodes") err = oxDeleteNodes(validatedOwnJid, client, iqc) if err != nil { cancel() closeAndExit(client, err) } os.Exit(0) case *flagOx, *flagLegacyPGP: slog.Info("checking validity", "JID", user) validatedOwnJid, err := MarshalJID(user) if err != nil { cancel() closeAndExit(client, err) } slog.Info("reading private OX key") oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase) if err != nil { cancel() closeAndExit(client, err) } } var uploadMessages []string if len(*flagHTTPUpload) != 0 && !*flagLegacyPGP { slog.Info("http-upload:", "file", *flagHTTPUpload) uploadMessages, err = httpUpload(client, iqc, disc, drc, slc, jserver, *flagHTTPUpload, timeout, nil, nil) if err != nil { cancel() closeAndExit(client, err) } } if *flagOOBFile != "" { // Remove invalid UTF8 code points. message = validUTF8(*flagOOBFile) // Check if the URI is valid. uri, err := validURI(message) if err != nil { cancel() closeAndExit(client, err) } message = uri.String() slog.Info("send OOB:", "uri", message) } var msgType string switch { case *flagHeadline: msgType = strHeadline case *flagChatroom: msgType = strGroupchat default: msgType = strChat } slog.Info("setting message", "type", msgType) if *flagChatroom { // Join the MUCs. for _, recipient := range recipients { var presence xmpp.Presence pc := make(chan xmpp.Presence, defaultBufferSize) go getPresence(recipient.Jid, pc, prsc) if *flagMUCPassword != "" { slog.Info("joining MUC using password", "MUC", recipient.Jid, "password", "REDACTED") dummyTime := time.Now() _, err = client.JoinProtectedMUC(recipient.Jid, alias, *flagMUCPassword, xmpp.NoHistory, 0, &dummyTime) } else { slog.Info("joining", "MUC", recipient.Jid) _, err = client.JoinMUCNoHistory(recipient.Jid, alias) } if err != nil { fmt.Printf("Could not join MUC %s: %v\n", recipient.Jid, err) failure = err } select { case presence = <-pc: if presence.Type == "error" { fmt.Printf("Could not join MUC %s\n", recipient.Jid) failure = fmt.Errorf("failed to join one or more MUCs") } case <-time.After(60 * time.Second): fmt.Printf("Could not join muc: didn't receive a presence from %s\n", recipient.Jid) } } } switch { case *flagRaw: if message == "" { slog.Info("not sending empty message") break } // Send raw XML slog.Info("sending raw message") _, err = client.SendOrg(message) if err != nil { cancel() closeAndExit(client, err) } case *flagInteractive: // Send in endless loop (for usage with e.g. "tail -f"). slog.Info("reading in interactive mode from stdin") reader := bufio.NewReader(os.Stdin) // Quit if ^C is pressed. c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { for range c { slog.Info("stopping interactive mode") cancel() if failure != nil { closeAndExit(client, failure) } 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") slog.Info("reading message", "line", message) // Remove invalid code points. slog.Info("removing invalid code points from message") message = validUTF8(message) if message == "" { continue } for _, recipient := range recipients { switch { case *flagOx, *flagLegacyPGP: var cryptMessage string if recipient.OxKeyRing == nil { slog.Info("skipping sending an OX encrypted message due to missing public key for", "JID", recipient.Jid) continue } switch { case *flagOx: slog.Info("encrypting message with OX for", "JID", recipient.Jid) cryptMessage, err = oxEncrypt(client, oxPrivKey, recipient.Jid, *recipient.OxKeyRing, message, *flagSubject) if err != nil { fmt.Printf("ox: couldn't encrypt to %s: %v\n", recipient.Jid, err) failure = err continue } case *flagLegacyPGP: slog.Info("encrypting message with legacy PGP for", "JID", recipient.Jid) cryptMessage, err = legacyPGPEncrypt(client, oxPrivKey, recipient.Jid, *recipient.OxKeyRing, message) if err != nil { fmt.Printf("legacy PGP: couldn't encrypt to %s: %v\n", recipient.Jid, err) failure = err continue } } slog.Info("sending OX encrypted message to", "JID", recipient.Jid) _, err = client.SendOrg(cryptMessage) if err != nil { cancel() closeAndExit(client, err) } default: slog.Info("sending message to", "JID", recipient.Jid) _, err = client.Send(xmpp.Chat{ Remote: recipient.Jid, Type: msgType, Text: message, Subject: *flagSubject, }) if err != nil { cancel() closeAndExit(client, err) } } } } case *flagListen: slog.Info("listening for incoming messages") 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 { slog.Info("stopping listening for incoming messages") cancel() if failure != nil { closeAndExit(client, failure) } closeAndExit(client, nil) } }() for { v := <-msgc slog.Info("received message") isOx := isOxMsg(v) isLegacyPGP := isLegacyPGPMsg(v) switch { case isOx, isLegacyPGP: var msg, prefix string var t time.Time if isOx { slog.Info("message is OX encrypted") msg, t, err = oxDecrypt(v, client, iqc, user, oxPrivKey) if err != nil { log.Println(err) continue } if msg == "" { slog.Info("message is empty") continue } prefix = "[OX]" } else { msg, t, err = legacyPGPDecrypt(v, client, iqc, user, oxPrivKey) if err != nil { log.Println(err) continue } if msg == "" { slog.Info("message is empty") continue } prefix = "[LPGP]" } var bareFrom string switch v.Type { case strChat: bareFrom = strings.Split(v.Remote, "/")[0] case strGroupchat: bareFrom = v.Remote default: bareFrom = strings.Split(v.Remote, "/")[0] } // Print any messages if no recipients are specified if len(recipients) == 0 { fmt.Println(t.In(tz).Format(time.RFC3339), prefix, bareFrom+":", msg) } else { for _, recipient := range recipients { if strings.Split(v.Remote, "/")[0] == strings.ToLower(recipient.Jid) { fmt.Println(t.In(tz).Format(time.RFC3339), prefix, bareFrom+":", msg) } } } default: var t time.Time if v.Text == "" { continue } if v.Stamp.IsZero() { t = time.Now() } else { t = v.Stamp } var bareFrom string switch v.Type { case strChat: bareFrom = strings.Split(v.Remote, "/")[0] case strGroupchat: bareFrom = v.Remote default: bareFrom = strings.Split(v.Remote, "/")[0] } // Print any messages if no recipients are specified if len(recipients) == 0 { fmt.Println(t.In(tz).Format(time.RFC3339), bareFrom+":", v.Text) } else { for _, recipient := range recipients { if strings.Split(v.Remote, "/")[0] == strings.ToLower(recipient.Jid) { fmt.Println(t.In(tz).Format(time.RFC3339), bareFrom+":", v.Text) } } } } } default: for _, recipient := range recipients { if message == "" && len(*flagHTTPUpload) == 0 { slog.Info("skipping sending of empty message to", "JID", recipient.Jid) break } switch { case len(*flagHTTPUpload) != 0 && !*flagLegacyPGP: for _, message = range uploadMessages { slog.Info("sending http-upload message:", "file", message, "JID", recipient.Jid) _, err = client.SendOOB(xmpp.Chat{ Remote: recipient.Jid, Type: msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject, }) if err != nil { fmt.Println("Couldn't send message to", recipient.Jid) } } case len(*flagHTTPUpload) != 0 && *flagLegacyPGP: uploadMessages, err = httpUpload(client, iqc, disc, drc, slc, jserver, *flagHTTPUpload, timeout, oxPrivKey, recipient.OxKeyRing) if err != nil { cancel() closeAndExit(client, err) } for _, message = range uploadMessages { slog.Info("sending legacy pgp http-upload message:", "file", message, "JID", recipient.Jid) _, err = client.SendOOB(xmpp.Chat{ Remote: recipient.Jid, Type: msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject, }) if 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 != "": _, err = client.SendOOB(xmpp.Chat{ Remote: recipient.Jid, Type: msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject, }) if err != nil { fmt.Println("Couldn't send message to", recipient.Jid) } case *flagOx, *flagLegacyPGP: if recipient.OxKeyRing == nil { slog.Info("skipping sending an OX encrypted message due to missing public key for", "JID", recipient.Jid) continue } var cryptMessage string switch { case *flagOx: slog.Info("encrypting message with OX for", "JID", recipient.Jid) cryptMessage, err = oxEncrypt(client, oxPrivKey, recipient.Jid, *recipient.OxKeyRing, message, *flagSubject) if err != nil { fmt.Printf("ox: couldn't encrypt to %s: %v\n", recipient.Jid, err) failure = err continue } case *flagLegacyPGP: slog.Info("encrypting message with legacy PGP for", "JID", recipient.Jid) cryptMessage, err = legacyPGPEncrypt(client, oxPrivKey, recipient.Jid, *recipient.OxKeyRing, message) if err != nil { fmt.Printf("legacy PGP: couldn't encrypt to %s: %v\n", recipient.Jid, err) failure = err continue } } _, err = client.SendOrg(cryptMessage) if err != nil { cancel() closeAndExit(client, err) } default: slog.Info("sending message to", "JID", recipient.Jid) _, err = client.Send(xmpp.Chat{ Remote: recipient.Jid, Type: msgType, Text: message, Subject: *flagSubject, }) if err != nil { cancel() closeAndExit(client, err) } } } } cancel() if failure != nil { closeAndExit(client, failure) } closeAndExit(client, nil) } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/000077500000000000000000000000001520066516100220455ustar00rootroot00000000000000go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.1000066400000000000000000000266101520066516100243750ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 .TH "GO\-SENDXMPP" "1" "May 2026" "" .SH "NAME" \fBgo\-sendxmpp\fR \- A tool to send messages to an XMPP contact or MUC\. .SH "SYNOPSIS" \fBgo\-sendxmpp [\-cdilntv] [\-a value] [\-\-allow\-plain] [\-\-anonymous] [\-\-fast\-invalidate] [\-\-fast\-off] [\-f value] [\-\-headline] [\-\-help] [\-h value] [\-j value] [\-\-legacy\-pgp] [\-m value] [\-\-muc\-password value] [\-\-no\-sasl\-upgrade] [\-\-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] [\-\-retry\-connect value] [\-\-retry\-connect\-max value] [\-\-scram\-mech\-pinning value] [\-\-ssdp\-off] [\-s value] [\-\-suppress\-root\-warning] [\-\-timeout value] [\-\-tls\-version value] [\-u value] [\-\-version] [recipients…]\fR .SH "DESCRIPTION" A tool to send messages to an XMPP contact or MUC inspired by \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" .TP \fB\-a\fR, \fB\-\-alias\fR=[\fIvalue\fR] Set alias/nickname for chatrooms\. .TP \fB\-\-allow\-plain\fR Allow PLAIN authentication\. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before\. In that case PLAIN is not allowed to prevent downgrades by a man\-in\-the\-middle attack\. .TP \fB\-\-anonymous\fR Use anonymous authentication\. Specify the the target server by using the flag \fB\-u\fR or \fB\-\-username\fR\. Username is used to specify the server to be able to override the server by using \fB\-j\fR or \fB\-\-jserver\fR in cases where the service (e\.g\. anon\.example\.org) is served by another server (e\.g\. xmpp\.example\.org) but no SRV records are set up\. .TP \fB\-c\fR, \fB\-\-chatroom\fR=[\fIvalue\fR] Send message to a chatroom\. .TP \fB\-d\fR, \fB\-\-debug\fR Show XMPP stanzas\. These can include sensitive data\. .TP \fB\-\-fast\-invalidate\fR Invalidate XEP\-0484: Fast Authentication Streamlining Tokens\. The current FAST token will be invalidated and a new one will be requested\. If used together with \fB\-\-fast\-off\fR the FAST token will be invalidated and the stored FAST token deleted\. .TP \fB\-\-fast\-off\fR Disable XEP\-0484: Fast Authentication Streamlining Tokens\. Deletes the stored FAST token when used together with \fB\-\-fast\-invalidate\fR\. .TP \fB\-f\fR, \fB\-\-file\fR=[\fIvalue\fR] Set configuration file\. (Default: ~/\.config/go\-sendxmpp/config) .TP \fB\-\-headline\fR Send message as type headline\. .TP \fB\-\-help\fR Show help\. .TP \fB\-h\fR, \fB\-\-http\-upload=\fR[\fIvalue\fR] Send a file via http\-upload\. Can be invoked several times to upload multiple files\. .TP \fB\-i\fR, \fB\-\-interactive\fR Interactive mode (for use with e\.g\. \fBtail \-f\fR)\. .TP \fB\-j\fR, \fB\-\-jserver\fR=[\fIvalue\fR] XMPP server address\. .TP \fB\-\-legacy\-pgp\fR "Legacy PGP" encryption using the Ox key infrastructure\. This means the recipient must have an Ox key published and use the same key for legacy PGP\. The use of legacy PGP is actually discouraged and only provided as a workaround for conversations users\. If the existing key has no ID in the form of \fBxmpp:user@example\.org\fR, it can be added by using the tool \fIsq\fR: .IP "" 4 .nf $ sq key userid add \-u xmpp:user@example\.org \-Bo newkey\.asc oldkey\.asc` .fi .IP "" 0 .IP This key (\fBnewkey\.asc\fR in the \fIsq\fR example) can than be imported by using \fB\-\-ox\-import\-privkey\fR\. Please note that this will store the secret key at \fB~/\.local/share/go\-sendxmpp/user_at_example_org/oxprivkey\fR\. If you don't want to use that account with go\-sendxmpp and only imported the key to upload the public key to pubsub you might consider deleting the file\. .TP \fB\-l\fR, \fB\-\-listen\fR Listen for messages and print them to stdout\. If JIDs are specified only messages from those contacts are shown\. If no JIDs are specified all received messages will be shown\. .TP \fB\-m\fR, \fB\-\-message\fR=[\fIvalue\fR] Set file including the message\. .TP \fB\-\-muc\-password\fR=[\fIvalue\fR] Password for password protected MUCs\. .TP \fB\-\-no\-sasl\-upgrade\fR Disable XEP\-0480: SASL Upgrade Tasks\. .TP \fB\-n\fR, \fB\-\-no\-tls\-verify\fR Skip verification of TLS certificates (not recommended)\. .TP \fB\-\-oob\-file\fR=[\fIvalue\fR] URL to send a file as out of band data\. .TP \fB\-\-ox\fR Use "OpenPGP for XMPP" encryption (experimental)\. .br Ox in go\-sendxmpp only supports sending encrypted 1\-1 messages\. Sending to groupchats and sending encrypted files is not supported\. There is no check whether the recipients key is trusted as there is no local keyring used\. Go\-sendxmpp just uses the most recent key that is provided via pubsub and checks that it is not expired\. As a user facing client a notification would be shown that a new key is available and ask the user whether to use the new key or stick to the old one\. As go\-sendxmpp is usually used in scripts it just accepts the new key to prevent the user from missing a new notification due to changed keys\. .TP \fB\-\-ox\-delete\-nodes\fR Delete existing OpenPGP nodes on the server\. .TP \fB\-\-ox\-genprivkey\-rsa\fR Generate a private OpenPGP key (RSA 4096 bit) for the configured account (via config file or \fB\-u\fR and \fB\-p\fR) and publish the corresponding public key\. Go\-sendxmpp will save the key in \fB$XDG_DATA_HOME/go\-sendxmpp/oxprivkeys\fR or \fB$HOME/\.local/share/go\-sendxmpp/oxprivkeys\fR\. To protect the key a passphrase might be set using \fB\-\-ox\-passphrase\fR while generating the key\. .br If there is an existing private key for "OpenPGP for XMPP" created by another client (e\.g\. profanity) it might be imported using \fB\-\-ox\-import\-privkey\fR\. .TP \fB\-\-ox\-genprivkey\-x25519\fR Generate a private OpenPGP key (X25519) for the configured account (via config file or \fB\-u\fR and \fB\-p\fR) and publish the corresponding public key\. Go\-sendxmpp will save the key in \fB$XDG_DATA_HOME/go\-sendxmpp/oxprivkeys\fR or \fB$HOME/\.local/share/go\-sendxmpp/oxprivkeys\fR\. To protect the key a passphrase might be set using \fB\-\-ox\-passphrase\fR while generating the key\. .br If there is an existing private key for "OpenPGP for XMPP" created by another client (e\.g\. profanity) it might be imported using \fB\-\-ox\-import\-privkey\fR\. .TP \fB\-\-ox\-import\-privkey\fR=[\fIvalue\fR] Import an existing private OpenPGP key\. .TP \fB\-\-ox\-passphrase\fR=[\fIvalue\fR] Passphrase for locking and unlocking the private OpenPGP key\. .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\-r\fR, \fB\-\-recipients\fR=[\fIvalue\fR] Read recipients from file, one JID per line\. .TP \fB\-\-retry\-connect\fR=[\fIvalue\fR] Specify a connection retry waiting time in seconds\. This specifies the waiting time until go\-sendxmpp will try to reconnect if connecting fails\. This setting only affects the initial connection, it does not reconnect after losing the connection\. It is also ignored if the connection fails due to an authentication error as then it does not make sense to try again using the same credentials\. Setting a time of 0 seconds disables this functionality\. (Default: 0) .TP \fB\-\-retry\-connect\-max\fR=[\fIvalue\fR] Number of maximum retries to perform for '\-\-retry\-connect'\. A value of 0 means unlimited retries\. (Default: 0) .TP \fB\-\-scram\-mech\-pinning=[]\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 and requires a SCRAM authentication mechanism)\. .TP \fB\-\-ssdp\-off\fR Disable XEP\-0474: SASL SCRAM Downgrade Protection\. .TP \fB\-s\fR, \fB\-\-subject\fR=[\fIvalue\fR] Set message subject\. .TP \fB\-\-suppress\-root\-warning\fR Suppress warning when run as root\. .TP \fB\-\-timeout=\fR[\fIvalue\fR] Connection timeout in seconds\. (Default: 10) .TP \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: 13) .TP \fB\-u\fR, \fB\-\-username\fR=[\fIvalue\fR] Username for XMPP account (JID)\. .TP \fB\-v\fR, \fB\-\-verbose\fR Show debug information\. These can include sensitive data\. .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\. .IP "" 4 .nf HTTP_PROXY="socks5://127\.0\.0\.1:9050" go\-sendxmpp \-\-http\-upload file\.txt user@example\.org .fi .IP "" 0 .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: .IP "" 4 .nf # command\-that\-requires\-root|sudo \-H \-u sendxmpp go\-sendxmpp me@example\.org .fi .IP "" 0 .P If go\-sendxmpp is run as root it will drop the root privileges before connecting to the server\. This means \fBFAST\fR and \fBOX\fR will be disabled as go\-sendxmpp will not have the necessary rights to store the necessary data (e\.g\. FAST tokes or OX key material) after dropping the privileges\. This is just to avoid network connections as root and doesn't mean that running as root as a supported use case, in fact it is highly recommended to never run go\-sendxmpp as root\. .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\. .P Just place the following in your \fB~/\.zshrc\fR: .IP "" 4 .nf compdef _gnu_generic go\-sendxmpp .fi .IP "" 0 .SS "FISH" There are no shell completions yet, but FISH can generate them from the man page with following command: .IP "" 4 .nf fish_update_completions .fi .IP "" 0 .SH "CHAT" Feel free to join \fIhttps://join\.jabber\.network/#go\-sendxmpp@chat\.mdosch\.de?join\fR\. .SH "AUTHOR" Written by Martin Dosch\. .SH "REPORTING BUGS" Report bugs at \fIhttps://salsa\.debian\.org/mdosch/go\-sendxmpp/\-/work_items\fR\. .SH "COPYRIGHT" Copyright (c) Martin Dosch License: BSD 2\-clause License .SH "SEE ALSO" go\-sendxmpp(5), xmppc(1), sendxmpp(1) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.1.html000066400000000000000000000404111520066516100253330ustar00rootroot00000000000000 go-sendxmpp(1) - A tool to send messages to an XMPP contact or MUC.
  1. go-sendxmpp(1)
  2. go-sendxmpp(1)

NAME

go-sendxmpp - A tool to send messages to an XMPP contact or MUC.

SYNOPSIS

go-sendxmpp [-cdilntv] [-a value] [--allow-plain] [--anonymous] [--fast-invalidate] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [--legacy-pgp] [-m value] [--muc-password value] [--no-sasl-upgrade] [--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] [--retry-connect value] [--retry-connect-max value] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--suppress-root-warning] [--timeout value] [--tls-version value] [-u value] [--version] [recipients…]

DESCRIPTION

A tool to send messages to an XMPP contact or MUC inspired by 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 (deprecated) ~/.sendxmpprc (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with -f or --file.

OPTIONS

-a, --alias=[value]
Set alias/nickname for chatrooms.
--allow-plain
Allow PLAIN authentication. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before. In that case PLAIN is not allowed to prevent downgrades by a man-in-the-middle attack.
--anonymous
Use anonymous authentication. Specify the the target server by using the flag -u or --username. Username is used to specify the server to be able to override the server by using -j or --jserver in cases where the service (e.g. anon.example.org) is served by another server (e.g. xmpp.example.org) but no SRV records are set up.
-c, --chatroom=[value]
Send message to a chatroom.
-d, --debug
Show XMPP stanzas. These can include sensitive data.
--fast-invalidate
Invalidate XEP-0484: Fast Authentication Streamlining Tokens. The current FAST token will be invalidated and a new one will be requested. If used together with --fast-off the FAST token will be invalidated and the stored FAST token deleted.
--fast-off
Disable XEP-0484: Fast Authentication Streamlining Tokens. Deletes the stored FAST token when used together with --fast-invalidate.
-f, --file=[value]
Set configuration file. (Default: ~/.config/go-sendxmpp/config)
--headline
Send message as type headline.
--help
Show help.
-h, --http-upload=[value]
Send a file via http-upload. Can be invoked several times to upload multiple files.
-i, --interactive
Interactive mode (for use with e.g. tail -f).
-j, --jserver=[value]
XMPP server address.
--legacy-pgp
"Legacy PGP" encryption using the Ox key infrastructure. This means the recipient must have an Ox key published and use the same key for legacy PGP. The use of legacy PGP is actually discouraged and only provided as a workaround for conversations users. If the existing key has no ID in the form of xmpp:user@example.org, it can be added by using the tool sq:
  $ sq key userid add -u xmpp:user@example.org -Bo newkey.asc oldkey.asc`

This key (newkey.asc in the sq example) can than be imported by using --ox-import-privkey. Please note that this will store the secret key at ~/.local/share/go-sendxmpp/user_at_example_org/oxprivkey. If you don't want to use that account with go-sendxmpp and only imported the key to upload the public key to pubsub you might consider deleting the file.

-l, --listen
Listen for messages and print them to stdout. If JIDs are specified only messages from those contacts are shown. If no JIDs are specified all received messages will be shown.
-m, --message=[value]
Set file including the message.
--muc-password=[value]
Password for password protected MUCs.
--no-sasl-upgrade
Disable XEP-0480: SASL Upgrade Tasks.
-n, --no-tls-verify
Skip verification of TLS certificates (not recommended).
--oob-file=[value]
URL to send a file as out of band data.
--ox
Use "OpenPGP for XMPP" encryption (experimental).
Ox in go-sendxmpp only supports sending encrypted 1-1 messages. Sending to groupchats and sending encrypted files is not supported. There is no check whether the recipients key is trusted as there is no local keyring used. Go-sendxmpp just uses the most recent key that is provided via pubsub and checks that it is not expired. As a user facing client a notification would be shown that a new key is available and ask the user whether to use the new key or stick to the old one. As go-sendxmpp is usually used in scripts it just accepts the new key to prevent the user from missing a new notification due to changed keys.
--ox-delete-nodes
Delete existing OpenPGP nodes on the server.
--ox-genprivkey-rsa
Generate a private OpenPGP key (RSA 4096 bit) for the configured account (via config file or -u and -p) and publish the corresponding public key. Go-sendxmpp will save the key in $XDG_DATA_HOME/go-sendxmpp/oxprivkeys or $HOME/.local/share/go-sendxmpp/oxprivkeys. To protect the key a passphrase might be set using --ox-passphrase while generating the key.
If there is an existing private key for "OpenPGP for XMPP" created by another client (e.g. profanity) it might be imported using --ox-import-privkey.
--ox-genprivkey-x25519
Generate a private OpenPGP key (X25519) for the configured account (via config file or -u and -p) and publish the corresponding public key. Go-sendxmpp will save the key in $XDG_DATA_HOME/go-sendxmpp/oxprivkeys or $HOME/.local/share/go-sendxmpp/oxprivkeys. To protect the key a passphrase might be set using --ox-passphrase while generating the key.
If there is an existing private key for "OpenPGP for XMPP" created by another client (e.g. profanity) it might be imported using --ox-import-privkey.
--ox-import-privkey=[value]
Import an existing private OpenPGP key.
--ox-passphrase=[value]
Passphrase for locking and unlocking the private OpenPGP key.
-p, --password=[value]
Password for XMPP account.
--raw
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.
-r, --recipients=[value]
Read recipients from file, one JID per line.
--retry-connect=[value]
Specify a connection retry waiting time in seconds. This specifies the waiting time until go-sendxmpp will try to reconnect if connecting fails. This setting only affects the initial connection, it does not reconnect after losing the connection. It is also ignored if the connection fails due to an authentication error as then it does not make sense to try again using the same credentials. Setting a time of 0 seconds disables this functionality. (Default: 0)
--retry-connect-max=[value]
Number of maximum retries to perform for '--retry-connect'. A value of 0 means unlimited retries. (Default: 0)
--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 and requires a SCRAM authentication mechanism).
--ssdp-off
Disable XEP-0474: SASL SCRAM Downgrade Protection.
-s, --subject=[value]
Set message subject.
--suppress-root-warning
Suppress warning when run as root.
--timeout=[value]
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: 13)
-u, --username=[value]
Username for XMPP account (JID).
-v, --verbose
Show debug information. These can include sensitive data.
--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

If go-sendxmpp is run as root it will drop the root privileges before connecting to the server. This means FAST and OX will be disabled as go-sendxmpp will not have the necessary rights to store the necessary data (e.g. FAST tokes or OX key material) after dropping the privileges. This is just to avoid network connections as root and doesn't mean that running as root as a supported use case, in fact it is highly recommended to never run go-sendxmpp as root.

SHELL COMPLETIONS

ZSH

There are no shell completions yet (contributions welcome) but for zsh it is possible to automatically create completions from --help which might work good enough.

Just place the following in your ~/.zshrc:

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.

AUTHOR

Written by Martin Dosch.

REPORTING BUGS

Report bugs at https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items.

Copyright (c) Martin Dosch License: BSD 2-clause License

SEE ALSO

go-sendxmpp(5), xmppc(1), sendxmpp(1)

  1. May 2026
  2. go-sendxmpp(1)
go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.1.ronn000066400000000000000000000245341520066516100253530ustar00rootroot00000000000000go-sendxmpp(1) -- A tool to send messages to an XMPP contact or MUC. ==== ## SYNOPSIS `go-sendxmpp [-cdilntv] [-a value] [--allow-plain] [--anonymous] [--fast-invalidate] [--fast-off] [-f value] [--headline] [--help] [-h value] [-j value] [--legacy-pgp] [-m value] [--muc-password value] [--no-sasl-upgrade] [--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] [--retry-connect value] [--retry-connect-max value] [--scram-mech-pinning value] [--ssdp-off] [-s value] [--suppress-root-warning] [--timeout value] [--tls-version value] [-u value] [--version] [recipients…]` ## DESCRIPTION A tool to send messages to an XMPP contact or MUC inspired by `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` (deprecated) `~/.sendxmpprc` (for compatibility with the original perl sendxmpp) if no other configuration file location is specified with `-f` or `--file`. ## OPTIONS * `-a`, `--alias`=[]: Set alias/nickname for chatrooms. * `--allow-plain`: Allow PLAIN authentication. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before. In that case PLAIN is not allowed to prevent downgrades by a man-in-the-middle attack. * `--anonymous`: Use anonymous authentication. Specify the the target server by using the flag `-u` or `--username`. Username is used to specify the server to be able to override the server by using `-j` or `--jserver` in cases where the service (e.g. anon.example.org) is served by another server (e.g. xmpp.example.org) but no SRV records are set up. * `-c`, `--chatroom`=[]: Send message to a chatroom. * `-d`, `--debug`: Show XMPP stanzas. These can include sensitive data. * `--fast-invalidate`: Invalidate XEP-0484: Fast Authentication Streamlining Tokens. The current FAST token will be invalidated and a new one will be requested. If used together with `--fast-off` the FAST token will be invalidated and the stored FAST token deleted. * `--fast-off`: Disable XEP-0484: Fast Authentication Streamlining Tokens. Deletes the stored FAST token when used together with `--fast-invalidate`. * `-f`, `--file`=[]: Set configuration file. (Default: ~/.config/go-sendxmpp/config) * `--headline`: Send message as type headline. * `--help`: Show help. * `-h`, `--http-upload=`[]: Send a file via http-upload. Can be invoked several times to upload multiple files. * `-i`, `--interactive`: Interactive mode (for use with e.g. `tail -f`). * `-j`, `--jserver`=[]: XMPP server address. * `--legacy-pgp`: "Legacy PGP" encryption using the Ox key infrastructure. This means the recipient must have an Ox key published and use the same key for legacy PGP. The use of legacy PGP is actually discouraged and only provided as a workaround for conversations users. If the existing key has no ID in the form of `xmpp:user@example.org`, it can be added by using the tool *sq*: ``` $ sq key userid add -u xmpp:user@example.org -Bo newkey.asc oldkey.asc` ``` This key (`newkey.asc` in the *sq* example) can than be imported by using `--ox-import-privkey`. Please note that this will store the secret key at `~/.local/share/go-sendxmpp/user_at_example_org/oxprivkey`. If you don't want to use that account with go-sendxmpp and only imported the key to upload the public key to pubsub you might consider deleting the file. * `-l`, `--listen`: Listen for messages and print them to stdout. If JIDs are specified only messages from those contacts are shown. If no JIDs are specified all received messages will be shown. * `-m`, `--message`=[]: Set file including the message. * `--muc-password`=[]: Password for password protected MUCs. * `--no-sasl-upgrade`: Disable XEP-0480: SASL Upgrade Tasks. * `-n`, `--no-tls-verify`: Skip verification of TLS certificates (not recommended). * `--oob-file`=[]: URL to send a file as out of band data. * `--ox`: Use "OpenPGP for XMPP" encryption (experimental). Ox in go-sendxmpp only supports sending encrypted 1-1 messages. Sending to groupchats and sending encrypted files is not supported. There is no check whether the recipients key is trusted as there is no local keyring used. Go-sendxmpp just uses the most recent key that is provided via pubsub and checks that it is not expired. As a user facing client a notification would be shown that a new key is available and ask the user whether to use the new key or stick to the old one. As go-sendxmpp is usually used in scripts it just accepts the new key to prevent the user from missing a new notification due to changed keys. * `--ox-delete-nodes`: Delete existing OpenPGP nodes on the server. * `--ox-genprivkey-rsa`: Generate a private OpenPGP key (RSA 4096 bit) for the configured account (via config file or `-u` and `-p`) and publish the corresponding public key. Go-sendxmpp will save the key in `$XDG_DATA_HOME/go-sendxmpp/oxprivkeys` or `$HOME/.local/share/go-sendxmpp/oxprivkeys`. To protect the key a passphrase might be set using `--ox-passphrase` while generating the key. If there is an existing private key for "OpenPGP for XMPP" created by another client (e.g. profanity) it might be imported using `--ox-import-privkey`. * `--ox-genprivkey-x25519`: Generate a private OpenPGP key (X25519) for the configured account (via config file or `-u` and `-p`) and publish the corresponding public key. Go-sendxmpp will save the key in `$XDG_DATA_HOME/go-sendxmpp/oxprivkeys` or `$HOME/.local/share/go-sendxmpp/oxprivkeys`. To protect the key a passphrase might be set using `--ox-passphrase` while generating the key. If there is an existing private key for "OpenPGP for XMPP" created by another client (e.g. profanity) it might be imported using `--ox-import-privkey`. * `--ox-import-privkey`=[]: Import an existing private OpenPGP key. * `--ox-passphrase`=[]: Passphrase for locking and unlocking the private OpenPGP key. * `-p`, `--password`=[]: Password for XMPP account. * `--raw`: 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. * `-r`, `--recipients`=[]: Read recipients from file, one JID per line. * `--retry-connect`=[]: Specify a connection retry waiting time in seconds. This specifies the waiting time until go-sendxmpp will try to reconnect if connecting fails. This setting only affects the initial connection, it does not reconnect after losing the connection. It is also ignored if the connection fails due to an authentication error as then it does not make sense to try again using the same credentials. Setting a time of 0 seconds disables this functionality. (Default: 0) * `--retry-connect-max`=[]: Number of maximum retries to perform for '--retry-connect'. A value of 0 means unlimited retries. (Default: 0) * `--scram-mech-pinning=[]`: 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 and requires a SCRAM authentication mechanism). * `--ssdp-off`: Disable XEP-0474: SASL SCRAM Downgrade Protection. * `-s`, `--subject`=[]: Set message subject. * `--suppress-root-warning`: Suppress warning when run as root. * `--timeout=`[]: Connection timeout in seconds. (Default: 10) * `-t`, `--tls`: Use direct TLS. * `--tls-version`=[]: Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2), 13 (TLSv1.3) (Default: 13) * `-u`, `--username`=[]: Username for XMPP account (JID). * `-v`, `--verbose`: Show debug information. These can include sensitive data. * `--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 ``` If go-sendxmpp is run as root it will drop the root privileges before connecting to the server. This means **FAST** and **OX** will be disabled as go-sendxmpp will not have the necessary rights to store the necessary data (e.g. FAST tokes or OX key material) after dropping the privileges. This is just to avoid network connections as root and doesn't mean that running as root as a supported use case, in fact it is highly recommended to never run go-sendxmpp as root. ## SHELL COMPLETIONS ### ZSH There are no shell completions yet (contributions welcome) but for zsh it is possible to automatically create completions from `--help` which might work good enough. Just place the following in your `~/.zshrc`: ``` 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). ## AUTHOR Written by Martin Dosch. ## REPORTING BUGS Report bugs at [https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items](https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items). ## COPYRIGHT Copyright (c) Martin Dosch License: BSD 2-clause License ## SEE ALSO go-sendxmpp(5), xmppc(1), sendxmpp(1) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.5000066400000000000000000000052131520066516100243750ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 .TH "GO\-SENDXMPP" "5" "May 2026" "" .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: .SH "FORMAT" username: [\fIyour_jid\fR] .br jserver: [\fIjabber_server\fR] .br port: [\fIjabber_port\fR] .br password: [\fIyour_jabber_password\fR] .br eval_password: [\fIcommand_to_unlock_your_password\fR] .br alias: [\fIyour_alias\fR] .br allow_plain: [\fIbool\fR] .SH "REQUIRED SETTINGS" If all necessary settings are supplied as command line arguments no config file is needed at all\. Setting \fBjserver\fR and \fBport\fR might not be necessary depending on the used server\. .br You should either use a password manager and the setting \fBeval_password\fR or add your password in plaintext to the config file with the setting \fBpassword\fR\. Please be careful with \fBeval_password\fR as any command put there will be executed\. .SH "OPTIONS" .TP \fBusername\fR Your JID, e\.g\. \fBuser@example\.com\fR\. .TP \fBjserver\fR Your server, e\.g\. \fBxmpp\.example\.com\fR\. Should not be necessary if the server is set up properly\. .TP \fBport\fR The client\-to\-server port of the XMPP server, e\.g\. \fB5222\fR\. Should not be necessary if the server is set up properly\. .TP \fBpassword\fR Your accounts password, e\.g\. \fBhunter2\fR (please, do not use \fBhunter2\fR)\. Use \fB"\fR if your password contains spaces, e\.g\. \fB"hunter 2"\fR (please, also do nut use \fBhunter 2\fR)\. .TP \fBeval_password\fR A command to retrieve the password from a password manager, e\.g\. \fBpass xmpp/user@example\.com\fR\. .TP \fBalias\fR The alias/nickname for chatrooms, e\.g\. \fBMartin\fR or \fB"My nick contains spaces"\fR\. .TP \fBallow_plain\fR Boolean to allow PLAIN authentication\. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before\. In that case PLAIN is not allowed to prevent downgrades by a man\-in\-the\-middle attack\. .br Allowed values: \fBtrue\fR/\fBfalse\fR (Default: false) .SH "AUTHOR" Written by Martin Dosch\. .SH "REPORTING BUGS" Report bugs at \fIhttps://salsa\.debian\.org/mdosch/go\-sendxmpp/\-/work_items\fR\. .SH "COPYRIGHT" Copyright (c) Martin Dosch License: BSD 2\-clause License .SH "SEE ALSO" go\-sendxmpp(1), sendxmpp(1) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.5.html000066400000000000000000000146071520066516100253470ustar00rootroot00000000000000 go-sendxmpp(5) - A tool to send messages to an XMPP contact or MUC.
  1. go-sendxmpp(5)
  2. go-sendxmpp(5)

NAME

go-sendxmpp - 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 location is specified with -f or --file. The configuration file is expected to be in the following format:

FORMAT

username: [your_jid]
jserver: [jabber_server]
port: [jabber_port]
password: [your_jabber_password]
eval_password: [command_to_unlock_your_password]
alias: [your_alias]
allow_plain: [bool]

REQUIRED SETTINGS

If all necessary settings are supplied as command line arguments no config file is needed at all. Setting jserver and port might not be necessary depending on the used server.
You should either use a password manager and the setting eval_password or add your password in plaintext to the config file with the setting password. Please be careful with eval_password as any command put there will be executed.

OPTIONS

username
Your JID, e.g. user@example.com.
jserver
Your server, e.g. xmpp.example.com. Should not be necessary if the server is set up properly.
port
The client-to-server port of the XMPP server, e.g. 5222. Should not be necessary if the server is set up properly.
password
Your accounts password, e.g. hunter2 (please, do not use hunter2). Use " if your password contains spaces, e.g. "hunter 2" (please, also do nut use hunter 2).
eval_password
A command to retrieve the password from a password manager, e.g. pass xmpp/user@example.com.
alias
The alias/nickname for chatrooms, e.g. Martin or "My nick contains spaces".
allow_plain
Boolean to allow PLAIN authentication. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before. In that case PLAIN is not allowed to prevent downgrades by a man-in-the-middle attack.
Allowed values: true/false (Default: false)

AUTHOR

Written by Martin Dosch.

REPORTING BUGS

Report bugs at https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items.

Copyright (c) Martin Dosch License: BSD 2-clause License

SEE ALSO

go-sendxmpp(1), sendxmpp(1)

  1. May 2026
  2. go-sendxmpp(5)
go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/man/go-sendxmpp.5.ronn000066400000000000000000000046131520066516100253530ustar00rootroot00000000000000go-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 location is specified with -f or --file. The configuration file is expected to be in the following format: ## FORMAT username: [] jserver: [] port: [] password: [] eval_password: [] alias: [] allow_plain: [] ## REQUIRED SETTINGS If all necessary settings are supplied as command line arguments no config file is needed at all. Setting `jserver` and `port` might not be necessary depending on the used server. You should either use a password manager and the setting `eval_password` or add your password in plaintext to the config file with the setting `password`. Please be careful with `eval_password` as any command put there will be executed. ## OPTIONS * `username`: Your JID, e.g. **user@example.com**. * `jserver`: Your server, e.g. **xmpp.example.com**. Should not be necessary if the server is set up properly. * `port`: The client-to-server port of the XMPP server, e.g. **5222**. Should not be necessary if the server is set up properly. * `password`: Your accounts password, e.g. **hunter2** (please, do not use **hunter2**). Use `"` if your password contains spaces, e.g. **"hunter 2"** (please, also do nut use **hunter 2**). * `eval_password`: A command to retrieve the password from a password manager, e.g. **pass xmpp/user@example.com**. * `alias`: The alias/nickname for chatrooms, e.g. **Martin** or **"My nick contains spaces"**. * `allow_plain`: Boolean to allow PLAIN authentication. Note that this setting has no effect if there was a successful connection using a SCRAM authentication mechanism before. In that case PLAIN is not allowed to prevent downgrades by a man-in-the-middle attack. Allowed values: **true**/**false** (Default: false) ## AUTHOR Written by Martin Dosch. ## REPORTING BUGS Report bugs at [https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items](https://salsa.debian.org/mdosch/go-sendxmpp/-/work_items). ## COPYRIGHT Copyright (c) Martin Dosch License: BSD 2-clause License ## SEE ALSO go-sendxmpp(1), sendxmpp(1) go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/ox.go000066400000000000000000000762161520066516100222630ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. // TODO: Add more logging package main import ( "encoding/base64" "fmt" "io" "log" "log/slog" "os" "runtime" "strings" "time" "github.com/ProtonMail/gopenpgp/v3/crypto" // MIT License "github.com/beevik/etree" // BSD-2-clause "github.com/xmppo/go-xmpp" // BSD-3-Clause ) func oxDeleteNodes(jid string, client *xmpp.Client, iqc chan xmpp.IQ) error { nodeListRequest := etree.NewDocument() nodeListRequest.WriteSettings.AttrSingleQuote = true query := nodeListRequest.CreateElement("query") query.CreateAttr("xmlns", nsDiscoItems) nlr, err := nodeListRequest.WriteToString() if err != nil { return fmt.Errorf("ox: delete nodes: failed to create node list request %w", err) } slog.Info("ox: sending IQ to list pep nodes") iqReply, err := sendIQ(client, iqc, jid, "get", nlr) if err != nil { return fmt.Errorf("ox: delete nodes: failure with node list request iq: %w", err) } slog.Info("ox: parsing reply to IQ") nodeListReply := etree.NewDocument() err = nodeListReply.ReadFromBytes(iqReply.Query) if err != nil { return fmt.Errorf("ox: delete nodes: failed to read node list reply iq: %w", err) } query = nodeListReply.SelectElement("query") if query == nil { return fmt.Errorf("ox: error parsing iq reply") } items := query.SelectElements("item") if items == nil { return fmt.Errorf("ox: error parsing iq reply") } for _, item := range items { node := item.SelectAttr("node") if node == nil { continue } if !strings.Contains(node.Value, nsOx) { continue } deleteNodeRequest := etree.NewDocument() deleteNodeRequest.WriteSettings.AttrSingleQuote = true pubsub := deleteNodeRequest.CreateElement("pubsub") pubsub.CreateAttr("xmlns", nsPubsubOwner) del := pubsub.CreateElement("delete") del.CreateAttr("node", node.Value) dnr, err := deleteNodeRequest.WriteToString() if err != nil { continue } slog.Info("ox: sending IQ to list pep nodes", "node", node.Value) _, err = sendIQ(client, iqc, jid, "set", dnr) if err != nil { continue } } return nil } func oxDecrypt(m xmpp.Chat, client *xmpp.Client, iqc chan xmpp.IQ, user string, oxPrivKey *crypto.Key) (string, time.Time, error) { var cryptMsgByte []byte var err error sender := strings.Split(m.Remote, "/")[0] for _, r := range m.OtherElem { if r.XMLName.Space == nsOx { cryptMsgByte, err = base64.StdEncoding.DecodeString(r.InnerXML) if err != nil { return strError, time.Now(), err } break } } oxMsg := crypto.NewPGPMessage(cryptMsgByte) keyRing, err := crypto.NewKeyRing(oxPrivKey) if err != nil { return strError, time.Now(), err } slog.Info("ox: decrypting: getting senders key", "sender", sender) senderKeyRing, err := oxGetPublicKeyRing(client, iqc, sender) if err != nil { return strError, time.Now(), err } slog.Info("ox: decrypting message") pgpDecrypt, err := crypto.PGP().Decryption(). DecryptionKeys(keyRing). VerificationKeys(senderKeyRing). New() if err != nil { return strError, time.Now(), err } decryptMsg, err := pgpDecrypt.Decrypt(oxMsg.Bytes(), crypto.Bytes) if err != nil { return strError, time.Now(), err } // Remove invalid code points. slog.Info("ox: removing invalid code points from decrypted message") message := validUTF8(decryptMsg.String()) doc := etree.NewDocument() err = doc.ReadFromString(message) if err != nil { return strError, time.Now(), err } slog.Info("ox: decrypting: checking signcrypt element") signcrypt := doc.SelectElement("signcrypt") if signcrypt == nil { return strError, time.Now(), fmt.Errorf("ox: no signcrypt element") } slog.Info("ox: decrypting: checking to element") to := signcrypt.SelectElement("to") if to == nil { return strError, time.Now(), fmt.Errorf("ox: no to element") } slog.Info("ox: decrypting: checking jid attribute") jid := to.SelectAttr("jid") if jid == nil { return strError, time.Now(), fmt.Errorf("ox: no jid attribute") } jidAttrib := strings.Split(jid.Value, "/")[0] slog.Info("ox: decrypting: comparing jid attribute with local user", "jid-attribute", jidAttrib, "user", user) if jidAttrib != user { return strError, time.Now(), fmt.Errorf("ox: encrypted for wrong user") } slog.Info("ox: decrypting: checking time element") timestamp := signcrypt.SelectElement("time") if timestamp == nil { return strError, time.Now(), fmt.Errorf("ox: no time element") } slog.Info("ox: decrypting: checking stamp attribute") stamp := timestamp.SelectAttr("stamp") if stamp == nil { return strError, time.Now(), fmt.Errorf("ox: no stamp attribute") } slog.Info("ox: decrypting: parsing time stamp") msgStamp, err := time.Parse("2006-01-02T15:04:05Z0700", stamp.Value) if err != nil { return strError, time.Now(), err } slog.Info("ox: decrypting: checking payload element") payload := signcrypt.SelectElement("payload") if payload == nil { return strError, time.Now(), fmt.Errorf("ox: no payload element") } slog.Info("ox: decrypting: checking body element") body := payload.SelectElement("body") if body == nil { return "", time.Now(), nil } slog.Info("ox: decrypting: returning body") return body.Text(), msgStamp, nil } func isOxMsg(m xmpp.Chat) bool { slog.Info("ox: checking for ox message") for _, r := range m.OtherElem { slog.Info("ox: checking for ox message: comparing", "XMLNameSpace", r.XMLName.Space, "OXNameSpace", nsOx) if r.XMLName.Space == nsOx { slog.Info("ox: checking for ox message:", "result", true) return true } } slog.Info("ox: checking for ox message:", "result", false) return false } func oxImportPrivKey(jid string, privKeyLocation string, client *xmpp.Client, iqc chan xmpp.IQ) error { xmppURI := "xmpp:" + jid slog.Info("ox: importing private key:", "xmpp-uri", xmppURI) slog.Info("ox: importing private key:", "location", privKeyLocation) buffer, err := readFile(privKeyLocation) if err != nil { return err } slog.Info("ox: importing private key: reading key") key, err := crypto.NewKey(buffer.Bytes()) if err != nil { slog.Info("ox: importing private key: reading key failed: checking if armored") key, err = crypto.NewKeyFromArmored(buffer.String()) if err != nil { keyDecoded, err := base64.StdEncoding.DecodeString(buffer.String()) slog.Info("ox: importing private key: reading key failed: checking if base64 encoded") if err != nil { return fmt.Errorf("ox: import privkey: failed to import private key: %w", err) } slog.Info("ox: importing private key: reading key") key, err = crypto.NewKey(keyDecoded) if err != nil { return fmt.Errorf("ox: import privkey: failed to import private key: %w", err) } } } slog.Info("ox: importing private key: reading key: success") entity := key.GetEntity() slog.Info("ox: importing private key:", "entity", entity) if entity.Identities[xmppURI] == nil { return fmt.Errorf("ox: key identity is not %s", xmppURI) } slog.Info("ox: importing private key: deriving public key") pk, err := key.GetPublicKey() if err != nil { return fmt.Errorf("ox: import privkey: failed to get public key associated to private key: %w", err) } pubKey, err := crypto.NewKey(pk) if err != nil { return fmt.Errorf("ox: import privkey: failed to get public key associated to private key: %w", err) } fingerprint := strings.ToUpper(pubKey.GetFingerprint()) slog.Info("ox: importing private key:", "fingerprint", fingerprint) slog.Info("ox: importing private key: checking if public key is already published") _, err = oxRecvPublicKeys(client, iqc, jid, fingerprint) if err != nil { slog.Info("ox: importing private key: publishing public key") err = oxPublishPubKey(jid, client, iqc, pubKey) if err != nil { return fmt.Errorf("ox: import privkey: failed to publish public key: %w", err) } } location, err := oxGetPrivKeyLoc(jid) if err != nil { return fmt.Errorf("ox: import privkey: failed to determine private key location: %w", err) } slog.Info("ox: importing private key: getting private key location", "jid", jid, "location", location) slog.Info("ox: importing private key: serializing key") keySerialized, err := key.Serialize() if err != nil { return fmt.Errorf("ox: import privkey: failed to serialize private key: %w", err) } slog.Info("ox: importing private key: storing key:", "location", location) err = oxStoreKey(location, base64.StdEncoding.EncodeToString(keySerialized)) if err != nil { log.Fatal(err) } slog.Info("ox: importing private key: requesting public key ring") pubKeyRing, err := oxGetPublicKeyRing(client, iqc, jid) if err == nil { slog.Info("ox: importing private key: checking if public key ring contains", "fingerprint", fingerprint) pubKeys := pubKeyRing.GetKeys() for _, r := range pubKeys { fp := strings.ToUpper(r.GetFingerprint()) slog.Info("ox: importing private key: checking", "fingerprint", fp) if fp == fingerprint { slog.Info("ox: importing private key: checking if public key ring contains fingerprint: success") return nil } } } slog.Info("ox: importing private key: publishing public key") err = oxPublishPubKey(jid, client, iqc, pubKey) if err != nil { return fmt.Errorf("ox: import privkey: failed to publish public key: %w", err) } return nil } func oxPublishPubKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, pubKey *crypto.Key) error { keyCreated := time.Now().UTC().Format("2006-01-02T15:04:05Z") fingerprint := strings.ToUpper(pubKey.GetFingerprint()) slog.Info("ox: publishing public key:", "fingerprint", fingerprint, "created", keyCreated) slog.Info("ox: publishing public key: serializing public key") keySerialized, err := pubKey.Serialize() if err != nil { return fmt.Errorf("ox: publish pubkey: failed to serialize pubkey: %w", err) } slog.Info("ox: publishing public key: base64 encoding public key") pubKeyBase64 := base64.StdEncoding.EncodeToString(keySerialized) root := etree.NewDocument() root.WriteSettings.AttrSingleQuote = true pubsub := root.CreateElement("pubsub") pubsub.CreateAttr("xmlns", nsPubsub) publish := pubsub.CreateElement("publish") publish.CreateAttr("node", nsOxPubKeys+":"+fingerprint) item := publish.CreateElement("item") item.CreateAttr("id", keyCreated) pubkey := item.CreateElement("pubkey") pubkey.CreateAttr("xmlns", nsOx) data := pubkey.CreateElement("data") data.CreateText(pubKeyBase64) publishoptions := pubsub.CreateElement("publish-options") x := publishoptions.CreateElement("x") x.CreateAttr("xmlns", nsJabberData) x.CreateAttr("type", "submit") field := x.CreateElement("field") field.CreateAttr("var", "FORM_TYPE") field.CreateAttr("type", "hidden") value := field.CreateElement("value") value.CreateText(pubsubPubOptions) field = x.CreateElement("field") field.CreateAttr("var", "pubsub#access_model") value = field.CreateElement("value") value.CreateText("open") xmlstring, err := root.WriteToString() if err != nil { return fmt.Errorf("ox: publish pubkey: failed to create publish public key iq xml: %w", err) } slog.Info("ox: publishing public key: sending IQ") iqReply, err := sendIQ(client, iqc, jid, "set", xmlstring) if err != nil { return fmt.Errorf("ox: publish pubkey: iq failure publishing public key: %w", err) } if iqReply.Type != strResult { return fmt.Errorf("ox: error while publishing public key") } slog.Info("ox: publishing public key: received result") slog.Info("ox: publishing public key: receiving own public key from pubsub") ownPubKeyRingFromPubsub, err := oxRecvPublicKeys(client, iqc, jid, fingerprint) if err != nil { return fmt.Errorf("ox: couldn't successfully verify public key upload") } ownPubKeyFromPubsub := ownPubKeyRingFromPubsub.GetKeys()[0] slog.Info("ox: publishing public key: serializing own public key received from pubsub") ownPubKeyFromPubsubSerialized, err := ownPubKeyFromPubsub.Serialize() if err != nil { return fmt.Errorf("ox: couldn't successfully verify public key upload") } slog.Info("ox: publishing public key: base64 encoding own public key received from pubsub") psPubKeyBase64 := base64.StdEncoding.EncodeToString(ownPubKeyFromPubsubSerialized) slog.Info("ox: publishing public key: comparing local and pubsub public key:", "local", pubKeyBase64, "pubsub", psPubKeyBase64) if pubKeyBase64 != psPubKeyBase64 { return fmt.Errorf("ox: couldn't successfully verify public key upload") } slog.Info("ox: publishing public key: comparing local and pubsub public key: success") root = etree.NewDocument() root.WriteSettings.AttrSingleQuote = true pubsub = root.CreateElement("pubsub") pubsub.CreateAttr("xmlns", nsPubsub) publish = pubsub.CreateElement("publish") publish.CreateAttr("node", nsOxPubKeys) item = publish.CreateElement("item") pubkeyslist := item.CreateElement("public-keys-list") pubkeyslist.CreateAttr("xmlns", nsOx) pubkeymeta := pubkeyslist.CreateElement("pubkey-metadata") pubkeymeta.CreateAttr("v4-fingerprint", fingerprint) pubkeymeta.CreateAttr("date", keyCreated) publishoptions = pubsub.CreateElement("publish-options") x = publishoptions.CreateElement("x") x.CreateAttr("xmlns", nsJabberData) x.CreateAttr("type", "submit") field = x.CreateElement("field") field.CreateAttr("var", "FORM_TYPE") field.CreateAttr("type", "hidden") value = field.CreateElement("value") value.CreateText(pubsubPubOptions) field = x.CreateElement("field") field.CreateAttr("var", "pubsub#access_model") value = field.CreateElement("value") value.CreateText("open") xmlstring, err = root.WriteToString() if err != nil { return fmt.Errorf("ox: publish pubkey: failed to create xml for iq to publish public key list: %w", err) } slog.Info("ox: publishing public key: sending IQ to publish key list to pubsub") iqReply, err = sendIQ(client, iqc, jid, "set", xmlstring) if err != nil { return fmt.Errorf("ox: publish pubkey: iq failure publishing public key list: %w", err) } if iqReply.Type != strResult { return fmt.Errorf("ox: couldn't publish public key list") } slog.Info("ox: publishing public key: received result") return nil } func oxGetPrivKeyLoc(jid string) (string, error) { dataDir, err := getDataPath(fsFriendlyJid(jid), true) slog.Info("ox: get private key location: trying", "location", dataDir) if err != nil { return strError, fmt.Errorf("ox: get privkey location: %w", err) } oldDataDir, err := getDataPath("oxprivkeys/", false) slog.Info("ox: get private key location: trying", "location", oldDataDir) if err != nil { return strError, fmt.Errorf("ox: get privkey location: %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.ReplaceAll(jid, "@", "_at_") oldDataFile3 := oldDataDir + fsFriendlyJid(jid) dataFile := dataDir + "oxprivkey" if _, err := os.Stat(oldDataFile); err == nil { err := os.Rename(oldDataFile, dataFile) slog.Info("ox: get private key location: moving file to new location", "old", oldDataFile, "new", dataFile) if err != nil { return dataFile, err } } if _, err := os.Stat(oldDataFile2); err == nil { slog.Info("ox: get private key location: moving file to new location", "old", oldDataFile2, "new", dataFile) err := os.Rename(oldDataFile2, dataFile) if err != nil { return dataFile, err } } if _, err := os.Stat(oldDataFile3); err == nil { slog.Info("ox: get private key location: moving file to new location", "old", oldDataFile3, "new", dataFile) err := os.Rename(oldDataFile3, dataFile) if err != nil { return dataFile, err } } // #nosec G304 -- old data dir comes from the local system, it's no untrusted input from the network dir, err := os.Open(oldDataDir) if err == nil { slog.Info("ox: get private key location: entering old data", "folder", oldDataDir) slog.Info("ox: get private key location: checking if directory is empty") _, err = dir.ReadDir(1) // Delete old data dir if empty. if err == io.EOF { slog.Info("ox: get private key location: trying to delete empty", "folder", oldDataDir) err = os.Remove(oldDataDir) if err != nil { fmt.Printf("oxGetPrivKeyLoc: failed to delete old private key location: %v\n", err) } } } return dataFile, nil } func oxGetPubKeyLoc(fingerprint string) (string, error) { dataDir, err := getDataPath("oxpubkeys/", true) slog.Info("ox: getting public key", "location", dataDir) if err != nil { return strError, fmt.Errorf("ox: get pubkey location: %w", err) } dataFile := dataDir + fingerprint slog.Info("ox: getting public key location", "file", dataFile) return dataFile, nil } func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) { dataFile, err := oxGetPrivKeyLoc(jid) if err != nil { log.Fatal(err) } slog.Info("ox: getting private key:", "file", dataFile) keyBuffer, err := readFile(dataFile) if err != nil { log.Fatal(err) } keyString := keyBuffer.String() slog.Info("ox: getting private key: base64 decoding key") decodedPrivKey, err := base64.StdEncoding.DecodeString(keyString) if err != nil { return nil, fmt.Errorf("ox: get privkey: failed to decode private key: %w", err) } slog.Info("ox: getting private key: decoding key") key, err := crypto.NewKey(decodedPrivKey) if err != nil { return nil, fmt.Errorf("ox: get privkey: failed to decode private key: %w", err) } if passphrase != "" { slog.Info("ox: getting private key: unlocking key", "passphrase", "REDACTED") key, err = key.Unlock([]byte(passphrase)) if err != nil { log.Fatal("ox: couldn't unlock private key") } } isLocked, err := key.IsLocked() if err != nil { return nil, fmt.Errorf("ox: get privkey: failed to check whether private key is locked: %w", err) } if isLocked { log.Fatal("ox: private key is locked") } slog.Info("ox: getting private checking if key is expired") if key.IsExpired(time.Now().Unix()) { return nil, fmt.Errorf("ox: private key is expired: %s", key.GetFingerprint()) } return key, nil } func oxStoreKey(location string, key string) error { var file *os.File slog.Info("ox: storing key:", "location", location) // #nosec G304 -- location comes from the local system, it's no untrusted input from the network file, err := os.Create(location) if err != nil { return fmt.Errorf("ox: store key: failed to create key location: %w", err) } if runtime.GOOS != "windows" { _ = file.Chmod(os.FileMode(defaultFileRights)) } else { _ = file.Chmod(os.FileMode(defaultFileRightsWin)) } slog.Info("ox: storing key: writing key to file") _, err = file.Write([]byte(key)) if err != nil { return fmt.Errorf("ox: store key: failed to write key to file: %w", err) } err = file.Close() if err != nil { fmt.Println("error while closing file:", err) } return nil } func oxGenPrivKey(jid string, client *xmpp.Client, iqc chan xmpp.IQ, passphrase string, keyType int, ) error { xmppURI := "xmpp:" + jid slog.Info("ox: generating privkey:", "jid", xmppURI, "keytype", keyType) pgp := crypto.PGP() privKey, err := pgp.KeyGeneration(). AddUserId(xmppURI, ""). OverrideProfileAlgorithm(keyType). New().GenerateKey() if err != nil { return fmt.Errorf("ox: generate privkey: failed to generate private key: %w", err) } key := privKey if passphrase != "" { slog.Info("ox: generating privkey: setting", "passphrase", "REDACTED") key, err = pgp.LockKey(privKey, []byte(passphrase)) if err != nil { return fmt.Errorf("ox: generate privkey: failed to lock private key with passphrase: %w", err) } } slog.Info("ox: generating privkey: serializing key") keySerialized, err := key.Serialize() if err != nil { return fmt.Errorf("ox: generate privkey: failed to serialize private key: %w", err) } location, err := oxGetPrivKeyLoc(jid) if err != nil { return fmt.Errorf("ox: generate privkey: failed to get private key location: %w", err) } slog.Info("ox: generating privkey: storing base64 encoded key:", "location", location) err = oxStoreKey(location, base64.StdEncoding.EncodeToString(keySerialized)) if err != nil { log.Fatal(err) } slog.Info("ox: generating privkey: deriving public key") decodedPubKey, err := key.GetPublicKey() if err != nil { return fmt.Errorf("ox: generate privkey: failed to decode public key: %w", err) } pubKey, err := crypto.NewKey(decodedPubKey) if err != nil { return fmt.Errorf("ox: generate privkey: failed to decode public key: %w", err) } err = oxPublishPubKey(jid, client, iqc, pubKey) if err != nil { return fmt.Errorf("ox: generate privkey: failed to publish public key: %w", err) } return nil } func oxRecvPublicKeys(client *xmpp.Client, iqc chan xmpp.IQ, recipient string, fingerprint string) (*crypto.KeyRing, error) { opkr := etree.NewDocument() opkr.WriteSettings.AttrSingleQuote = true opkrPs := opkr.CreateElement("pubsub") opkrPs.CreateAttr("xmlns", nsPubsub) opkrPsItems := opkrPs.CreateElement("items") opkrPsItems.CreateAttr("node", nsOxPubKeys+":"+fingerprint) opkrPsItems.CreateAttr("max_items", "1") opkrString, err := opkr.WriteToString() if err != nil { return nil, fmt.Errorf("ox: receive public keys: failed to generate xml for public key request: %w", err) } slog.Info("ox: receiving public keys:", "recipient", recipient) oxPublicKey, err := sendIQ(client, iqc, recipient, "get", opkrString) if err != nil { return nil, fmt.Errorf("ox: receive public keys: iq error requesting public keys: %w", err) } if oxPublicKey.Type != strResult { return nil, fmt.Errorf("ox: error while requesting public key for %s", recipient) } oxPublicKeyXML := etree.NewDocument() err = oxPublicKeyXML.ReadFromBytes(oxPublicKey.Query) if err != nil { return nil, fmt.Errorf("ox: receive public keys: failed parsing iq reply to public key request: %w", err) } keyring, err := crypto.NewKeyRing(nil) if err != nil { return nil, fmt.Errorf("ox: receive public keys: 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") } oxPublicKeyXMLItems := oxPublicKeyXMLPubsub.SelectElement("items") if oxPublicKeyXMLItems == nil { return nil, fmt.Errorf("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") } slog.Info("ox: receiving public keys: received public keys") oxPublicKeyXMLPubkeys := oxPublicKeyXMLItem.SelectElements("pubkey") for _, r := range oxPublicKeyXMLPubkeys { slog.Info("ox: receiving public keys: decoding public", "key", r) data := r.SelectElement("data") if data == nil { continue } decodedPubKey, err := base64.StdEncoding.DecodeString(data.Text()) if err != nil { return nil, fmt.Errorf("ox: receive public keys: failed to decode public key: %w", err) } key, err := crypto.NewKey(decodedPubKey) if err != nil { return nil, fmt.Errorf("ox: receive public keys: failed to decode public key: %w", err) } slog.Info("ox: receiving public keys: checking for expiry:", "fingerprint", fingerprint) if key.IsExpired(time.Now().Unix()) { return nil, fmt.Errorf("ox: key is expired: %s", fingerprint) } err = keyring.AddKey(key) if err != nil { return nil, fmt.Errorf("ox: receive public keys: failed adding public key to keyring: %w", err) } } return keyring, nil } func oxGetPublicKeyRing(client *xmpp.Client, iqc chan xmpp.IQ, recipient string) (*crypto.KeyRing, error) { publicKeyRing, err := crypto.NewKeyRing(nil) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to create a new keyring: %w", err) } oxPubKeyListReq := etree.NewDocument() oxPubKeyListReq.WriteSettings.AttrSingleQuote = true oxPubKeyListReqPs := oxPubKeyListReq.CreateElement("pubsub") oxPubKeyListReqPs.CreateAttr("xmlns", nsPubsub) oxPubKeyListReqPsItems := oxPubKeyListReqPs.CreateElement("items") oxPubKeyListReqPsItems.CreateAttr("node", nsOxPubKeys) oxPubKeyListReqPsItems.CreateAttr("max_items", "1") opkl, err := oxPubKeyListReq.WriteToString() if err != nil { log.Fatal(err) } oxPublicKeyList, err := sendIQ(client, iqc, recipient, "get", opkl) if err != nil { log.Fatal(err) } if oxPublicKeyList.Type != strResult { return nil, fmt.Errorf("ox: error while requesting public openpgp keys for %s", recipient) } oxPubKeyListXML := etree.NewDocument() err = oxPubKeyListXML.ReadFromBytes(oxPublicKeyList.Query) if err != nil { return nil, fmt.Errorf("ox: get public keyring: 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 { return nil, fmt.Errorf("ox: get public keyring: 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") } oxPubKeyListXMLPubsubItems := oxPubKeyListXMLPubsub.SelectElement("items") if oxPubKeyListXMLPubsubItems == nil { return nil, fmt.Errorf("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") } oxPubKeyListXMLPubsubItemsItemPkl := oxPubKeyListXMLPubsubItemsItem.SelectElement("public-keys-list") if oxPubKeyListXMLPubsubItemsItemPkl == nil { return nil, fmt.Errorf("ox: no public-keys-list element") } oxPubKeyListXMLPubsubItemsItemPklPm := oxPubKeyListXMLPubsubItemsItemPkl.SelectElements("pubkey-metadata") for _, r := range oxPubKeyListXMLPubsubItemsItemPklPm { date := r.SelectAttr("date") if date == nil { continue } fingerprint := r.SelectAttr("v4-fingerprint") if fingerprint == nil { continue } keyDate, err := time.Parse(time.RFC3339, date.Value) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to parse time stamp for key: %w", err) } if keyDate.After(newestKey) { newestKey = keyDate pubKeyRingID = fingerprint.Value } } if pubKeyRingID == "none" { return nil, fmt.Errorf("ox: server didn't provide public key fingerprints for %s", recipient) } pubKeyRingLocation, err := oxGetPubKeyLoc(pubKeyRingID) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to get public key ring location: %w", err) } pubKeyReadXML := etree.NewDocument() err = pubKeyReadXML.ReadFromFile(pubKeyRingLocation) if err == nil { date := pubKeyReadXML.SelectElement("date") if date != nil { savedKeysDate, err := time.Parse(time.RFC3339, date.Text()) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to parse time for saved key: %w", err) } if !savedKeysDate.Before(newestKey) { pubKeys := pubKeyReadXML.SelectElements("pubkey") if pubKeys == nil { return nil, fmt.Errorf("ox: couldn't read public keys from cache") } for _, r := range pubKeys { keyByte, err := base64.StdEncoding.DecodeString(r.Text()) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to decode saved key: %w", err) } key, err := crypto.NewKey(keyByte) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to parse saved key: %w", err) } if !key.IsExpired(time.Now().Unix()) { err = publicKeyRing.AddKey(key) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to add key to public keyring: %w", err) } } } if publicKeyRing.CanEncrypt(time.Now().Unix()) { return publicKeyRing, nil } } } } pubKeyRing, err := oxRecvPublicKeys(client, iqc, recipient, pubKeyRingID) if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to get public keyring: %w", err) } pubKeySaveXML := etree.NewDocument() date := pubKeySaveXML.CreateElement("date") date.SetText(newestKey.Format(time.RFC3339)) for _, key := range pubKeyRing.GetKeys() { keySerialized, err := key.Serialize() if err != nil { return nil, fmt.Errorf("ox: get public keyring: failed to serialize key: %w", err) } saveKey := pubKeySaveXML.CreateElement("pubkey") saveKey.SetText(base64.StdEncoding.EncodeToString(keySerialized)) } err = pubKeySaveXML.WriteToFile(pubKeyRingLocation) if err != nil { return nil, fmt.Errorf("ox: get public keyring: 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) { if message == "" { return "", nil } ownJid := strings.Split(client.JID(), "/")[0] if recipient != ownJid { opk, err := oxPrivKey.GetPublicKey() if err == nil { ownKey, _ := crypto.NewKey(opk) _ = keyRing.AddKey(ownKey) } } oxCryptMessage := etree.NewDocument() oxCryptMessage.WriteSettings.AttrSingleQuote = true oxCryptMessageSc := oxCryptMessage.CreateElement("signcrypt") oxCryptMessageSc.CreateAttr("xmlns", nsOx) oxCryptMessageScTo := oxCryptMessageSc.CreateElement("to") oxCryptMessageScTo.CreateAttr("jid", recipient) oxCryptMessageScTime := oxCryptMessageSc.CreateElement("time") oxCryptMessageScTime.CreateAttr("stamp", time.Now().UTC().Format("2006-01-02T15:04:05Z")) 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 { return strError, fmt.Errorf("ox: encrypt: failed to create xml for ox crypt message: %w", err) } pgpEncrypt, err := crypto.PGP().Encryption(). Recipients(&keyRing). SigningKey(oxPrivKey).New() if err != nil { return strError, fmt.Errorf("oxEncrypt: failed to create pgp encryption interface: %w", err) } pgpMessage, err := pgpEncrypt.Encrypt([]byte(ocm)) if err != nil { return strError, fmt.Errorf("ox: encrypt: failed to create pgp message: %w", err) } id := getID() om := etree.NewDocument() om.WriteSettings.AttrSingleQuote = true omMessage := om.CreateElement("message") omMessage.CreateAttr("to", recipient) omMessage.CreateAttr("id", id) omMessageStore := omMessage.CreateElement("store") omMessageStore.CreateAttr("xmlns", nsHints) omMessageEme := omMessage.CreateElement("encryption") omMessageEme.CreateAttr("xmlns", nsEme) omMessageEme.CreateAttr("namespace", nsOx) omMessageOpgp := omMessage.CreateElement("openpgp") omMessageOpgp.CreateAttr("xmlns", nsOx) omMessageOpgp.CreateText(base64.StdEncoding.EncodeToString(pgpMessage.Bytes())) omMessageBody := omMessage.CreateElement("body") omMessageBody.CreateText(oxAltBody) omMessageOrigID := omMessage.CreateElement("origin-id") omMessageOrigID.CreateAttr("xmlns", nsSid) omMessageOrigID.CreateAttr("id", id) oms, err := om.WriteToString() if err != nil { return strError, fmt.Errorf("ox: encrypt: failed to create xml for ox message: %w", err) } return oms, nil } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/parseconfig.go000066400000000000000000000115671520066516100241330ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. // TODO: Add logging package main import ( "bufio" "fmt" "log" "log/slog" "os" "os/exec" "os/user" "runtime" "strconv" "strings" ) func findConfig() (string, error) { // Get the current user. curUser, err := user.Current() if err != nil { return "", fmt.Errorf("find config: failed to get current user: %w", err) } // Get home directory. home := curUser.HomeDir if home == "" { return "", fmt.Errorf("find config: no home directory found") } osConfigDir := os.Getenv("XDG_CONFIG_HOME") if osConfigDir == "" { osConfigDir = home + "/.config" } configFiles := [3]string{ osConfigDir + "/go-sendxmpp/config", osConfigDir + "/go-sendxmpp/sendxmpprc", home + "/.sendxmpprc", } for _, r := range configFiles { // Check that the config file is existing. _, err := os.Stat(r) if err == nil { return r, nil } } return "", fmt.Errorf("find config: no configuration file found") } // Opens the config file and returns the specified values // for username, server and port. func parseConfig(configPath string) (output configuration, err error) { // Use $XDG_CONFIG_HOME/.config/go-sendxmpp/config, // $XDG_CONFIG_HOME/.config/go-sendxmpp/sendxmpprc or // ~/.sendxmpprc if no config path is specified. // Get systems user config path. if configPath == "" { configPath, err = findConfig() if err != nil { log.Fatal(err) } } // Only check file permissions if we are not running on windows. if runtime.GOOS != "windows" { info, err := os.Stat(configPath) if 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("parse config: wrong permissions for %s: %s instead of 400 (recommended), 440, 600 or 640", configPath, permissions) } } // Open config file. // #nosec G304 -- config path comes from the local system, it's no untrusted input from the network file, err := os.Open(configPath) if err != nil { return output, fmt.Errorf("parse config: failed to open config file: %w", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) // Read config file per line. for scanner.Scan() { if strings.HasPrefix(scanner.Text(), "#") { continue } column := strings.SplitN(scanner.Text(), " ", defaultConfigColumnSep) if len(column) > 1 { switch column[0] { case "username:": output.username = column[1] case "jserver:": output.jserver = column[1] case "password:": output.password = column[1] case "eval_password:": shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } // #nosec G204,G702 -- command to unlock password manager is supplied by the user // in the config and no untrusted data out, err := exec.Command(shell, "-c", column[1]).Output() if err != nil { err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err) } log.Fatal(err) } output.password = string(out) if output.password[len(output.password)-1] == '\n' { output.password = output.password[:len(output.password)-1] } case "port:": output.port = column[1] case "alias:": output.alias = column[1] case "allow_plain:": if strings.ToLower(column[1]) == "true" { output.allowPLAIN = true } else { output.allowPLAIN = false } default: // Try to parse legacy sendxmpp config files. if len(column) >= defaultConfigColumnSep && strings.Contains(column[0], "@") { if strings.Contains(scanner.Text(), ";") { output.username = strings.Split(column[0], ";")[0] output.jserver = strings.Split(column[0], ";")[1] output.password = column[1] } else { output.username = strings.Split(column[0], ":")[0] if strings.Contains(output.username, "@") { jserver := strings.SplitAfter(output.username, "@")[1] if len(jserver) < defaultLenServerConf { log.Fatal("Couldn't parse config: ", column[0]) } output.jserver = jserver } output.password = column[1] } } } } } // Check if the username is a valid JID output.username, err = MarshalJID(output.username) if err != nil { // Check whether only the local part was used by appending an @ and the // server part. output.username = output.username + "@" + output.jserver // Check if the username is a valid JID now output.username, err = MarshalJID(output.username) if err != nil { return output, fmt.Errorf("parse config: invalid username/JID: %s", output.username) } } err2 := file.Close() if err2 != nil { slog.Warn("closing file failed", "error", err) } return output, err } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/parseconfig_test.go000066400000000000000000000047161520066516100251700ustar00rootroot00000000000000package main import ( "os" "runtime" "strings" "testing" ) func TestParseConfig(t *testing.T) { // Set permissions. testfiles := []string{ "testdata/config001", "testdata/config002", "testdata/config003", "testdata/config004", } for i, file := range testfiles { switch i { case 1: err := os.Chmod(file, 0o444) if err != nil { t.Errorf("Failed to set permissions for %s", file) } default: err := os.Chmod(file, 0o400) if err != nil { t.Errorf("Failed to set permissions for %s", file) } } } // Test good config. cfg, err := parseConfig("testdata/config001") if err != nil { t.Fatalf("Failed to parse config: %v", err) } if cfg.username != "user@example.com" { t.Errorf("Expected username 'user@example.com', got '%s'", cfg.username) } if cfg.password != "hunter2" { t.Errorf("Expected password 'hunter2', got '%s'", cfg.password) } if cfg.jserver != "xmpp.example.com" { t.Errorf("Expected jserver 'xmpp.example.com', got '%s'", cfg.jserver) } if cfg.port != "1234" { t.Errorf("Expected port '1234', got '%s'", cfg.port) } if cfg.alias != "bla" { t.Errorf("Expected alias 'bla', got '%s'", cfg.alias) } if runtime.GOOS != "windows" { // Bad permissions. cfg, err = parseConfig("testdata/config002") if err == nil { t.Fatalf("Expected an error, got nil") } if !strings.Contains(err.Error(), "parse config: wrong permissions") { t.Errorf("unexpected error message: got %v", err) } } // Legacy sendxmpp formats. cfg, err = parseConfig("testdata/config003") if err != nil { t.Fatalf("Failed to parse config: %v", err) } if cfg.username != "user@example.com" { t.Errorf("Expected username 'user@example.com', got '%s'", cfg.username) } if cfg.password != "hunter2" { t.Errorf("Expected password 'hunter2', got '%s'", cfg.password) } if cfg.jserver != "example.com" { t.Errorf("Expected jserver 'example.com', got '%s'", cfg.jserver) } cfg, err = parseConfig("testdata/config004") if err != nil { t.Fatalf("Failed to parse config: %v", err) } if cfg.username != "user@example.com" { t.Errorf("Expected username 'user@example.com', got '%s'", cfg.username) } if cfg.jserver != "xmpp.example.com" { t.Errorf("Expected jserver 'xmpp.example.com', got '%s'", cfg.jserver) } if cfg.password != "hunter2" { t.Errorf("Expected password 'hunter2', got '%s'", cfg.password) } if cfg.jserver != "xmpp.example.com" { t.Errorf("Expected jserver 'xmpp.example.com', got '%s'", cfg.jserver) } } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/stanzahandling.go000066400000000000000000000171041520066516100246310ustar00rootroot00000000000000// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. // TODO: Add logging. package main import ( "context" "encoding/xml" "fmt" "io" "log" "log/slog" "strings" "time" "github.com/beevik/etree" // BSD-2-clause "github.com/xmppo/go-xmpp" // BSD-3-Clause ) func getDiscoInfo(client *xmpp.Client, drc chan xmpp.DiscoResult, target string) (dr xmpp.DiscoResult, err error) { id := getID() c := make(chan xmpp.DiscoResult, defaultBufferSize) go getDr(c, drc) slog.Info("stanzahandling: requesting disco info") _, err = client.RawInformation(client.JID(), target, id, "get", fmt.Sprintf("", nsDiscoInfo)) if err != nil { return dr, err } select { case dr = <-c: slog.Info("stanzahandling: requesting disco info: received reply") case <-time.After(60 * time.Second): slog.Info("stanzahandling: requesting disco info: timeout") return dr, fmt.Errorf("get disco info: %s didn't reply to disco info request", target) } return dr, err } func getDr(c chan xmpp.DiscoResult, drc chan xmpp.DiscoResult) { for { dr := <-drc c <- dr return } } func getSlot(client *xmpp.Client, slc chan xmpp.Slot, target, request string) (sl xmpp.Slot, err error) { id := getID() c := make(chan xmpp.Slot, defaultBufferSize) go getSl(c, slc) slog.Info("stanzahandling: requesting upload slot") _, err = client.RawInformation(client.JID(), target, id, "get", request) if err != nil { return sl, fmt.Errorf("get slot: failed to send iq: %w", err) } select { case sl = <-c: slog.Info("stanzahandling: requesting upload slot: received reply") case <-time.After(60 * time.Second): slog.Info("stanzahandling: requesting upload slot: timeout") return sl, fmt.Errorf("get upload slot: %s didn't reply to upload slot request", target) } return sl, err } func getSl(c chan xmpp.Slot, slc chan xmpp.Slot) { for { sl := <-slc c <- sl return } } func getDiscoItems(client *xmpp.Client, disc chan xmpp.DiscoItems, target string) (dis xmpp.DiscoItems, err error) { id := getID() c := make(chan xmpp.DiscoItems, defaultBufferSize) go getDis(target, c, disc) slog.Info("stanzahandling: requesting disco items") _, err = client.RawInformation(client.JID(), target, id, "get", fmt.Sprintf("", nsDiscoItems)) if err != nil { return dis, err } select { case dis = <-c: slog.Info("stanzahandling: requesting disco items: received reply") case <-time.After(60 * time.Second): slog.Info("stanzahandling: requesting disco items: timeout") return dis, fmt.Errorf("get disco items: %s didn't reply to disco items request", target) } return dis, err } func getDis(jid string, c chan xmpp.DiscoItems, disc chan xmpp.DiscoItems) { for { dis := <-disc if dis.Jid == jid { c <- dis return } } } 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) go getIQ(id, c, iqc) _, err := client.RawInformation(client.JID(), target, id, iQtype, content) if err != nil { return iq, fmt.Errorf("send iq: failed to send iq: %w", err) } select { case iq = <-c: case <-time.After(60 * time.Second): return iq, fmt.Errorf("send iq: server didn't reply to IQ: %s", content) } return iq, nil } func getIQ(id string, c chan xmpp.IQ, iqc chan xmpp.IQ) { for { iq := <-iqc if iq.ID == id { c <- iq return } } } func getPresence(from string, c chan xmpp.Presence, prsc chan xmpp.Presence) { for { p := <-prsc if strings.HasPrefix(p.From, from) { c <- p return } } } func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc chan xmpp.Chat, prsc chan xmpp.Presence, disc chan xmpp.DiscoItems, drc chan xmpp.DiscoResult, slc chan xmpp.Slot) { type messageError struct { XMLName xml.Name `xml:"message-error"` Text string `xml:"text"` } var me messageError var received interface{} r := make(chan interface{}, defaultBufferSize) e := make(chan error, defaultBufferSize) go func() { for { select { case <-ctx.Done(): return default: } rcv, err := client.Recv() if err != nil { e <- err } else { r <- rcv } } }() for { select { case <-ctx.Done(): return case err := <-e: switch err { case io.EOF, io.ErrUnexpectedEOF: return case nil: continue default: closeAndExit(client, fmt.Errorf("receive stanzas: %w", err)) return } case received = <-r: } switch v := received.(type) { case xmpp.Chat: if v.Type == "error" { for _, oe := range v.OtherElem { err := xml.Unmarshal([]byte(""+ oe.InnerXML+""), &me) if err == nil { fmt.Printf("%s: %s\n", v.Remote, me.Text) } } } msgc <- v case xmpp.Presence: prsc <- v case xmpp.DiscoItems: disc <- v case xmpp.DiscoResult: drc <- v case xmpp.Slot: slc <- v case xmpp.IQ: switch v.Type { case "get": var xmlns *etree.Attr iq := etree.NewDocument() err := iq.ReadFromBytes(v.Query) if err != nil { log.Println("Couldn't parse IQ:", string(v.Query), err) } query := iq.SelectElement("query") if query != nil { xmlns = query.SelectAttr("xmlns") } if xmlns == nil { break } switch xmlns.Value { case nsDiscoInfo: 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", nsDiscoInfo) identity := replyQuery.CreateElement("identity") identity.CreateAttr("category", "client") identity.CreateAttr("type", "bot") 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) } } default: root := etree.NewDocument() root.WriteSettings.AttrSingleQuote = true reply := root.CreateElement("iq") reply.CreateAttr("type", strError) reply.CreateAttr("from", client.JID()) reply.CreateAttr("to", v.From) reply.CreateAttr("id", v.ID) errorReply := reply.CreateElement(strError) errorReply.CreateAttr("type", "cancel") su := errorReply.CreateElement("service-unavailable") su.CreateAttr("xmlns", nsXMPPStanzas) xmlString, err := root.WriteToString() if err == nil { _, err = client.SendOrg(xmlString) if err != nil { log.Println(err) } } } case "set": root := etree.NewDocument() root.WriteSettings.AttrSingleQuote = true reply := root.CreateElement("iq") reply.CreateAttr("type", strError) reply.CreateAttr("from", client.JID()) reply.CreateAttr("to", v.From) reply.CreateAttr("id", v.ID) errorReply := reply.CreateElement(strError) errorReply.CreateAttr("type", "cancel") su := errorReply.CreateElement("service-unavailable") su.CreateAttr("xmlns", nsXMPPStanzas) xmlString, err := root.WriteToString() if err == nil { _, err = client.SendOrg(xmlString) if err != nil { log.Println(err) } } case "reply", strError: iqc <- v default: iqc <- v } } } } go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/testdata/000077500000000000000000000000001520066516100231035ustar00rootroot00000000000000go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/testdata/config001000066400000000000000000000002311520066516100245100ustar00rootroot00000000000000# comment username: user@example.com # comment password: hunter2 # comment jserver: xmpp.example.com # comment port: 1234 # comment alias: bla # comment go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/testdata/config002000066400000000000000000000001351520066516100245140ustar00rootroot00000000000000username: user@example.com password: hunter2 jserver: xmpp.example.com port: 1234 alias: bla go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/testdata/config003000066400000000000000000000000621520066516100245140ustar00rootroot00000000000000# legacy sendxmpp format user@example.com hunter2 go-sendxmpp-v0.15.8-55bea0799389b0608929724e4993916ba737bc17/testdata/config004000066400000000000000000000001031520066516100245110ustar00rootroot00000000000000# legacy sendxmpp format user@example.com;xmpp.example.com hunter2