From fbb07ea55c9996830f4731b80913d625fa4df654 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Thu, 6 Jan 2022 00:18:33 +0000 Subject: [PATCH] Initial commit --- .gitignore | 4 + LICENSE | 177 +++++++++++++ README.md | 490 ++++++++++++++++++++++++++++++++++++ assets/firefox-settings.png | Bin 0 -> 72009 bytes lint.sh | 4 + ssh-uuid.sh | 398 +++++++++++++++++++++++++++++ tests/run_tests.sh | 388 ++++++++++++++++++++++++++++ tests/scp-uuid | 1 + tests/ssh-uuid | 1 + 9 files changed, 1463 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/firefox-settings.png create mode 100755 lint.sh create mode 100755 ssh-uuid.sh create mode 100755 tests/run_tests.sh create mode 120000 tests/scp-uuid create mode 120000 tests/ssh-uuid diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ec23d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +./ssh-uuid +./scp-uuid +tests/test-config.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..430d42b --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..14e64e1 --- /dev/null +++ b/README.md @@ -0,0 +1,490 @@ +Table of Contents + +- [ssh-uuid, scp-uuid](#ssh-uuid-scp-uuid) +- [Usage examples](#usage-examples) + - [Remote command at the end of the command line, like standard `ssh`](#remote-command-at-the-end-of-the-command-line-like-standard-ssh) + - [Remote command execution with `--service`](#remote-command-execution-with---service) + - [Remote command execution preserves the command's exit status code](#remote-command-execution-preserves-the-commands-exit-status-code) + - [Piping stdin/stdout/stderr works just like standard `ssh`](#piping-stdinstdoutstderr-works-just-like-standard-ssh) + - [File transfer with `scp`](#file-transfer-with-scp) + - [File transfer with `cat`](#file-transfer-with-cat) + - [File transfer with `tar`](#file-transfer-with-tar) + - [File transfer with `rsync`](#file-transfer-with-rsync) + - [Port fowarding and SOCKS proxy with ssh's `-D`, `-L` and `-R` options](#port-fowarding-and-socks-proxy-with-sshs--d--l-and--r-options) + - [Web (http) server running on the device: local port 8000, remote port 80](#web-http-server-running-on-the-device-local-port-8000-remote-port-80) + - [Local port 80 may be forwarded using `sudo`](#local-port-80-may-be-forwarded-using-sudo) + - ["Hostvia" - Access a remote device through another remote device](#hostvia---access-a-remote-device-through-another-remote-device) + - [Expose a local server to a remote device](#expose-a-local-server-to-a-remote-device) + - [Use the remote device as an Internet point of presence for web browsing](#use-the-remote-device-as-an-internet-point-of-presence-for-web-browsing) +- [Installation](#installation) + - [Dependencies](#dependencies) + - [macOS](#macos) + - [Linux (Debian, Ubuntu, others)](#linux-debian-ubuntu-others) + - [Windows](#windows) +- [Authentication](#authentication) + - [More about authentication](#more-about-authentication) +- [Troubleshooting](#troubleshooting) +- [Why?](#why) + +# ssh-uuid, scp-uuid + +This project is a proof of concept implementation of the "thinnest useful wrapper" around +the standard `ssh` and `scp` tools to allow them to connect to the `ssh` server of a +remote balenaOS device, using the balenaCloud backend as a "raw bytestream pipe" between +the ssh client and the ssh server. + +The thin wrapper does a light-touch editing of the `ssh` or `scp` command line options +provided by the user, before passing them to the actual `ssh` or `scp` tools. It +automatically adds a `'-o ProxyCommand=…'` option that tunnels the connection through the +balenaCloud backend. It also adds a `--service` command line option that allows specifying +the name of a balena fleet service (application container running in balenaOS) in order to +target a service instead of the host OS. + +With this approach, all standard `ssh` and `scp` command line options, standard +environment variables and standard configuration files are supported, plus remote command +execution as provided by standard `ssh` -- preserving the remote command exit status code +and allowing the plumbing of stdin/stdout/stderr. + +This project is also an alternative vision of what the `balena ssh` command could be. +For more background information, check the **[Why](#why)** section. + +# Usage examples + +The device's ssh/scp hostname has the format `'.balena'`, using the device's +full UUID (not a shortened form). + +The `--service` option is used to target a balena fleet service name. When `--service` is +used, behind the scenes a `balena-engine exec` command takes care of executing the remote +command in the service container (similar to `balena ssh`), but the command syntax and +argument escaping are identical to standard `ssh`. + +## Remote command at the end of the command line, like standard `ssh` + +```sh +$ ssh-uuid a123456abcdef123456abcdef1234567.balena cat /etc/issue +balenaOS 2.85.2 \n \l +``` + +## Remote command execution with `--service` + +```sh +$ ssh-uuid --service my-service a123456abcdef123456abcdef1234567.balena cat /etc/issue +Debian GNU/Linux 11 \n \l +``` + +## Remote command execution preserves the command's exit status code + +```sh +$ ssh-uuid --service my-service a123456abcdef123456abcdef1234567.balena true; echo $? +0 +$ ssh-uuid --service my-service a123456abcdef123456abcdef1234567.balena false; echo $? +1 +``` + +## Piping stdin/stdout/stderr works just like standard `ssh` + +... because it ***is*** standard `ssh`! + +```sh +$ cat local.txt | ssh-uuid --service my-service a123456abcdef123456abcdef1234567.balena cat '>' /tmp/local-copy.txt + +$ ssh-uuid --service my-service a123456abcdef123456abcdef1234567.balena cat /tmp/remote.txt > remote-copy.txt +``` + +Note how `'>'` was quoted in the first command line above, so that the stdout redirection +is interpreted by the remote shell rather than the local shell (standard `ssh` syntax). In +the second command line, `'>'` is not quoted because we actually want the local shell to +interpret it as stdout redirection. + +## File transfer with `scp` + +```sh +# local -> remote +$ scp-uuid local.txt a123456abcdef123456abcdef1234567.balena:/mnt/data/ +local.txt 100% 6 0.0KB/s 00:00 + +# remote -> local +$ scp-uuid a123456abcdef123456abcdef1234567.balena:/tmp/remote.txt . +remote.txt 100% 17 0.0KB/s 00:01 +``` + +All `scp` command line options are supported. The `-r` option is used to copy a whole +folder: + +```sh +# local -> remote +$ scp-uuid -r local-folder a123456abcdef123456abcdef1234567.balena:/mnt/data/ +local.txt 100% 6 0.0KB/s 00:00 + +# remote -> local +$ scp-uuid -r a123456abcdef123456abcdef1234567.balena:/mnt/data/remote-folder . +remote.txt 100% 17 0.0KB/s 00:01 +``` + +To copy files or folders to a service container, check the following sections for file +copy with `cat`, `tar` or `rsync`. When the `--service` option is used with `scp-uuid`, +help output is printed with some great or even superior alternatives using `tar` or +`rsync`. + +If you are transferring files to/from a service's named volume (often at the '/data' mount +point in service containers), note that named volumes are also exposed on the host OS +under folder `'/mnt/data/docker/volumes/_data/_data/'`. As such, it is also +possible to scp to/from named volumes without using `--service`: + +```sh +# local -> remote +$ scp-uuid -r local-folder UUID.balena:/mnt/data/docker/volumes/_data/_data/ + +# remote -> local +$ scp-uuid -r UUID.balena:/mnt/data/docker/volumes/_data/_data/remote-folder . +``` + +## File transfer with `cat` + +```sh +# local -> remote +$ cat local.txt | ssh-uuid --service my-service UUID.balena cat \> /data/remote.txt + +# remote -> local +$ ssh-uuid --service my-service UUID.balena cat /data/remote.txt > local.txt +``` + +## File transfer with `tar` + +```sh +# local -> remote +$ tar cz local-folder | ssh-uuid --service my-service UUID.balena tar xzvC /data/ + +# remote -> local +$ ssh-uuid --service my-service UUID.balena tar czC /data remote-folder | tar xvz +``` + +`tar` must be installed both on the local workstation and on the remote service container: + +```sh +$ apt-get install -y tar # Debian, Ubuntu, etc +$ apk add tar # Alpine +``` + +## File transfer with `rsync` + +```sh +# local -> remote +$ rsync -av -e 'ssh-uuid --service my-service' local-folder UUID.balena:/data/ + +# remote -> local +$ rsync -av -e 'ssh-uuid --service my-service' UUID.balena:/data/remote-folder . +``` + +`rsync` must be installed both on the local workstation and on the remote service +container: + +```sh +$ apt-get install -y rsync # Debian, Ubuntu, etc +$ apk add rsync # Alpine +``` + +## Port fowarding and SOCKS proxy with ssh's `-D`, `-L` and `-R` options + +The wrapper does not interfere, and the full power of standard ssh is made available. This +is a full replacement for the `balena tunnel` command, with additional capabilities such a +dynamic port forwarding (SOCKS proxy) and the benefit of the full range of ssh +configuration options and files. + +The `--service` option does not apply because port forwarding terminates in the scope of +the `ssh` server, which is the host OS. To access service container ports, expose them to +the host OS through the `docker-compose.yml` file. + +### Web (http) server running on the device: local port 8000, remote port 80 + +```sh +$ ssh-uuid -NL 8000:127.0.0.1:80 a123456abcdef123456abcdef1234567.balena +``` + +Point the web browser at http://127.0.0.1:8000 + +### Local port 80 may be forwarded using `sudo` + +Any port numbers lower than 1024 normally require administrator privileges (`sudo`). + +```sh +$ sudo ssh-uuid -NL 80:127.0.0.1:80 a123456abcdef123456abcdef1234567.balena +``` + +Point the web browser at http://127.0.0.1 + +### "Hostvia" - Access a remote device through another remote device + +This is a rudimentary replacement for the balena-proxy "hostvia" functionality. It may be +useful as a diagnostics operation if one of the remote devices gets in trouble and loses +access to balena's VPN service, but can still access its local network. Both remote +devices should be on the same local network, e.g. connected to the same WiFi access point. +For example, assuming two devices on the `192.168.1.xxx` subnet, run both the following +commands on two command prompt windows on your workstation: + +* `ssh-uuid -NL 22222:192.168.1.86:22222 2eb94bd6ea9a3b9b4c0442aebf7bdb18.balena` + where the UUID is for the good/online/gateway device, and `192.168.1.86` is the IP + address of the device in trouble (reportedly offline). +* `ssh -p 22222 username@127.0.0.1` + where the username is your balenaCloud account username ('root' may also work if the + second device is running a development image of balenaOS). + +### Expose a local server to a remote device + +Use the `-R` option to expose a server running on your workstation or on the workstation's +local network to a remote device. For example, a local netcat chat server may be setup as +follows: + +* Run a netcat _server_ on your workstation (syntax may vary, check the `nc` manual page): + `$ nc -l 1300` +* Forward remote port 1300 to local port 1300: + `ssh-uuid -tR 1300:127.0.0.1:1300 2eb94bd6ea9a3b9b4c0442aebf7bdb18.balena` +* Run a netcat client on the remote device, balenaOS host OS prompt: + `$ nc 127.0.0.1 1300` +* Type words and hit Enter on each shell (local and remote). + +### Use the remote device as an Internet point of presence for web browsing + +Use the `-D` option to run a SOCKS proxy server on the local workstation (standard `ssh` +functionality - dynamic port forwarding) and then configure the Firefox web browser (on +the workstation) to use it, thus using the remote device as an Internet "point of +presence". For example, if you are in Europe and the device is in the USA and you open +'whatsmyip.com' on the Firefox browser, you will be reported as being in the USA. + +```sh +$ ssh-uuid -vND 8888 a123456abcdef123456abcdef1234567.balena +``` + +Configure Firefox (Network Settings, Connection Settings) as per screenshot +below. + +> NOTE: This screenshot is just an advanced usage example for the `-D` option, which you +> do not have to use! You do not need to use Firefox or change any proxy settings in order +> to use the `ssh-uuid` or `scp-uuid` tools. + +![Firefox Connection Settings](assets/firefox-settings.png) + +# Installation + +Clone this repo and create a couple of soft links as follows: + +```sh +$ git clone https://github.com/pdcastro/ssh-uuid.git +$ cd ssh-uuid +$ sudo ln -sf "${PWD}/ssh-uuid.sh" /usr/local/bin/ssh-uuid +$ sudo ln -sf "${PWD}/ssh-uuid.sh" /usr/local/bin/scp-uuid +$ which scp-uuid ssh-uuid +/usr/local/bin/scp-uuid +/usr/local/bin/ssh-uuid +``` + +The soft links (`ln -s`) are important. Both `ssh-uuid` and `scp-uuid` are soft links +to the same `ssh-uuid.sh` script. The script inspects how it was invoked at runtime +in order to decide what functionality to provide. + +You will also need to install the dependencies below. + +## Dependencies + +Follow the steps in your system-specific section below to install: + +* `bash` v4.4 or later +* `socat` v1.7.4 or later +* `jq`, `sed`, `ssh`, `scp` + +The balena CLI is not strictly required, but it can make authentication easier. +See [Authentication](#authentication) section. + +### macOS + +Install Homebrew (https://brew.sh/), and then: + +```sh +brew update && brew install bash git jq socat ssh +``` + +### Linux (Debian, Ubuntu, others) + +At the time of this writing, the latest stable distros of Debian and Ubuntu provide an +outdated version of `socat` with `apt-get install` (they provide socat v1.7.3, but we need +socat v1.7.4 or later). Here's how to install `socat` v1.7.4 (tested with Debian 9, Debian +10 and Ubuntu 20.04): + +```sh +$ sudo apt-get update +$ sudo apt-get install -y curl git jq ssh build-essential libreadline-dev libssl-dev libwrap0-dev +$ curl -LO http://www.dest-unreach.org/socat/download/socat-1.7.4.2.tar.gz +$ tar xzf socat-1.7.4.2.tar.gz +$ cd socat-1.7.4.2 +$ ./configure +$ make +$ sudo make install +$ which socat +/usr/local/bin/socat +``` + +If needed, more details about the compilation of socat can be found at: +http://www.dest-unreach.org/socat/doc/README + +### Windows + +Native PowerShell or cmd.exe are not supported by this proof-of-concept implemntation, +but you can use Microsoft's [WSL - Windows Subsystem for +Linux](https://docs.microsoft.com/en-us/windows/wsl/install) (e.g. Ubuntu), and then +follow the instructions for Linux. + +# Authentication + +Like `balena ssh`, authentication involves ***both*** ssh public key authentication (for +the ssh server) and balenaCloud authentication (balenaCloud username and session token or +API key). See [SSH Access +documentation](https://www.balena.io/docs/learn/manage/ssh-access/). + +Using the balena CLI is ***NOT*** a requirement for using `ssh-uuid` or `scp-uuid`. +However, if you happen to have the balena CLI installed, then all you need to do for +authentication is to log in with the balena CLI by running the following commands: + +* `balena login` +* `balena whoami` + +Running ***both*** commands will ensure that file `~/.balena/cachedUsername` exists and +contains a valid (not expired) session token. The file stores both a balenaCloud username +is the respective session token. The `ssh-uuid` or `scp-uuid` commands will check whether +that file exists (optional), to use the details in there for convenience. + +> Note: The `ssh-uuid` or `scp-uuid` script does not check whether a session token has +> expired. Expired tokens will cause authentication errors. If the `'balena whoami'` +> command succeeds, the token is good. + +**Alternatively,** if you would rather not use the balena CLI for balenaCloud +authentication, perhaps for non-interactive use, you can set the following two environment +variables instead: + +* `BALENA_USERNAME`: your balenaCloud username +* `BALENA_TOKEN`: your balenaCloud session token or API key + +Both the username and the session token (or API key) can be found in the balenaCloud +web dashboard, Preferences page. + +The authentication instructions above should be sufficient to get you started. The +following section gets into more details, in case you are interested or for +troubleshooting. + +## More about authentication + +Two levels of authentication are involved: + +* balenaCloud username and session token (or API key): These are sent to the balenaCloud + proxy backend (over HTTPS), which uses the details to ensure that the tunneling + service is only provided to registered users, and for activity logging. + +* SSH username and keys: Used by the balenaOS ssh server (on the device) to authenticate + the user against their public ssh key. The username may be `'root'` or a balenaCloud + username, with different behaviors depending on whether the remote device is running a + [development or production + image](https://www.balena.io/docs/reference/OS/overview/2.x/#development-vs-production-images) + of balenaOS, as per table below. + +Username | Production Image | Development Image +-------- | ---------------- | ----------------- +root | Requires adding a ssh public key to the [sshKeys section of config.json](https://www.balena.io/docs/reference/OS/configuration/#sshkeys) | ⚠ Allows unauthenticated access +balenaCloud username | ssh server authenticates against user's public SSH key stored in balenaCloud (requires balenaOS v2.44.0 or later) | ssh server authenticates against user's public SSH key stored in balenaCloud (requires balenaOS v2.44.0 or later) + +Reminder: public key authentication involves a pair of private and public keys: + +* Both the private and public keys are stored in the user's workstation, typically in the + `~/.ssh/` folder in the user's home directory on the machine where `ssh` (or `ssh-uuid`) + is executed. +* A copy of the public key is additionally stored remotely, either on the machine where + the ssh server is running (e.g. the `config.json` file of a balenaOS device), or in the + cloud (balenaCloud dashboard / API datatabase). + +# Troubleshooting + +* Set the DEBUG=1 environment variable to enable some debugging output produced by + the `ssh-uuid.sh` script itself. +* Add the `-v`, `-vv` or `-vvv` command line option, e.g.: + `DEBUG=1 ssh-uuid -v a123456abcdef123456abcdef1234567.balena cat /etc/issue` + +Common errors: + +* `socat[1355] E parseopts(): unknown option "proxy-authorization-file"` + This error means that your system is using an outdate version of 'socat'. + Update `socat` to version 1.7.4 or later as per Dependencies section. + +* `socat[25336] E CONNECT a123456abcdef123456abcdef1234567.balena:22222: Proxy Authorization Required` + Double check that the authentication session or API token is correct and has not expired. + See [Authentication](#authentication) section. + +* `socat[25072] E CONNECT a123456abcdef123456abcdef1234567.balena:22222: Internal Server Error` + Double check that the UUID is correct. Ensure you're using the full long UUID, not the + 7-char short form. + +# Why? + +Why are `ssh-uuid` and `scp-uuid` needed? What's wrong with the existing `balena ssh` and +`balena tunnel` implementations? + +Currently, `balena ssh` does not aim at command line compatibility with `ssh` or `scp` and +offers only a small subset of the "Swiss army knife" functionality provided by standard +`ssh` and `scp`. The [manual page of ssh](https://linux.die.net/man/1/ssh) lists more than +40 command line options and tens of further [configuration +options](https://linux.die.net/man/5/ssh_config), most of which consist of functionality +not matched by `balena ssh`. + +The most commonly reported issues/requests are probably the lack of file copy +functionality (no `balena scp` command exists), and the limited support for running remote +commands with `balena ssh` _while preserving command exit code and allowing plumbing of +stdin / stdout / stderr,_ as in the [Usage Examples](#usage-examples) section. + +`balena ssh` and the balenaCloud proxy service offer their own "custom interface" +(incompatible with standard ssh) for remote command execution: + +```sh +$ echo "uptime; exit;" | balena ssh 8dc8a1b +``` + +This custom interface takes a command line via stdin. It is problematic because: + +* It does not allow plumbing stdin/stderr/stdout, as stdin is used to specify the remote + commands. +* It does not preserve the remote command exit code (not event explicit exit codes such as + `exit 3`). +* Unlike standard `ssh`, it requires explicitly running `exit` in order to end the remote + session, which is prone to "hanging session" bugs. +* Being named `balena ssh`, it suggests the provision of `ssh`'s functionality, while + being incompatible with basic `ssh` command line usage. + +These limitations are fundamentally related to how the balenaCloud proxy currently sits as +a "man in the middle" ssh server that provides its own API to clients and makes its own +connections to the ssh server on devices. + +The `balena tunnel` command was created in part to work around these limitations, by +tunneling a TCP connection (raw bytestream) between a standard `ssh` client and the `ssh` +server on a balenaOS device. However, to that end, `balena tunnel` adds complexity and a +point of failure by requiring users to explicity run two different processes in different +shell prompts: `balena tunnel` in a prompt, and standard `ssh` in another. It also falls +on the user to choose a free TCP port number to provide as argument to both processes. +Also, `balena tunnel` offers only a small subset of features/options of standard `ssh`, +and indeed `balena tunnel` could be completely replaced with the standard port forwarding +options of standard `ssh` (`-L`, `-R` and `-D` options). + +While `balena ssh` offers a custom, incompatible interface to `ssh` functionality, it +still uses the standard `ssh` tool behind the scenes, and having the `ssh` tool +pre-installed is already a requirement of the balena CLI. In the past, there was +discussion about removing the need of pre-installing `ssh` by using a Javascript +implementation of `ssh` such as the `ssh2` npm module, but the motivation for that effort +was weakened when Microsoft Windows 10 started shipping with the `ssh` tool pre-installed. +Nowadays, Apple, Microsoft and all Linux distributions offer a standard `ssh` tool in +their operating systems. + +The `ssh-uuid` implementation provided in this project is able to replace both the `balena +ssh` and the `balena tunnel` commands (and potentially, eventually, part of the +balena-proxy backend) with a single revamped `balena ssh` command implemented as a thin +wrapper around `ssh`. To end users, it offers a lot more functionality, full compatibility +with the `ssh` and `scp` command lines that many users are familiar with, and removes the +complexity of managing separate processes (`balena tunnel` + `ssh`) in common usage +scenarios. To balena developers, `ssh-uuid` promises fewer lines of code and a simpler +architecture by minimally wrapping the standard `ssh` tool and avoiding the "man in the +middle" backend ssh server that terminates and initiates `ssh` sessions on behalf of the +balena CLI. diff --git a/assets/firefox-settings.png b/assets/firefox-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..c744e54728d096fe6b7ac4197a381736334ab803 GIT binary patch literal 72009 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�) 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 literal 0 HcmV?d00001 diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..8b4f2d6 --- /dev/null +++ b/lint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# https://github.com/koalaman/shellcheck +shellcheck -- *.sh tests/*.sh diff --git a/ssh-uuid.sh b/ssh-uuid.sh new file mode 100755 index 0000000..b52e93f --- /dev/null +++ b/ssh-uuid.sh @@ -0,0 +1,398 @@ +#!/usr/bin/env bash + +# Copyright 2022 Balena Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Exit immediately on unhandled errors + +BALENARC_DATA_DIRECTORY="${BALENARC_DATA_DIRECTORY:-"${HOME}/.balena"}" + +function quit { + echo "ERROR: $1" >/dev/stderr + exit 1 +} + +# check bash version +function check_bash_version { + local major="${BASH_VERSINFO[0]:-0}" + local minor="${BASH_VERSINFO[1]:-0}" + if (( "${major}" < 4 || ("${major}" == 4 && "${minor}" < 4) )); then + quit "\ +bash v${major}.${minor} detected, but this script requires bash v4.4 or later. +On macOS, it can be updated with 'brew install bash' ( https://brew.sh/ ) +Sorry for the inconvenience! +" + fi +} + +check_bash_version + +# Escape the arguments in a way compatible with the POSIX 'sh'. Alternative to +# bash's printf '%q' (because bash's printf '%q' may produce bash-specific +# escaping, for example using the $'a\nb' syntax that interprets ASCII control +# characters, which is not supported by POSIX 'sh'). Useful with the '--service' +# flag as we do not assume that balenaOS application containers will have 'bash' +# installed. Based on: https://stackoverflow.com/a/29824428 +function escape_sh { + case $# in 0) return 0; esac + while : + do + printf "'" + printf %s "$1" | sed "s/'/'\\\\''/g" + shift + case $# in 0) break; esac + printf "' " + done + printf "'\n" +} + +function print_help_and_quit { + echo "For ssh or scp options, check the manual pages: 'man ssh' or 'man scp'. +For ssh-uuid or scp-uuid usage, see README at: +https://github.com/pdcastro/ssh-uuid/blob/master/README.md +" >/dev/stderr + exit +} + +# Check whether the BALENA_USERNAME and BALENA_TOKEN environment variables are set, for +# the purpose of balenaCloud proxy authentication, and as the username to use for ssh +# public key authentication. If either variable is not set, attempt to obtain the missing +# value from the balena CLI's ~/.balena/cachedUsername JSON file. That file is created +# by running the `balena login` command followed by the `balena whoami` command. If you +# would rather not depend on the balena CLI, set the environment variables manually with +# the values found in the balenaCloud web dashboard, Preferences page, Account Details and +# Access Tokens tabs. +function get_user_and_token { + if [[ -n "${BALENA_USERNAME}" && -n "${BALENA_TOKEN}" ]]; then + return + fi + local cached_usr_file="${BALENARC_DATA_DIRECTORY}/cachedUsername" + if [[ ! -r "${cached_usr_file}" ]]; then + quit "\ +'BALENA_USERNAME' or 'BALENA_TOKEN' env vars not defined, and file +'${cached_usr_file}' not found or not readable. +Set the env vars as per README, or use the balena CLI 'login' and 'whoami' +commands to ensure that file is created. +" + fi + check_tool jq + local cached_username + local cached_token + cached_username="$(jq -r .username "${cached_usr_file}")" + cached_token="$(jq -r .token "${cached_usr_file}")" + BALENA_USERNAME="${BALENA_USERNAME:-"${cached_username}"}" + BALENA_TOKEN="${BALENA_TOKEN:-"${cached_token}"}" +} + +# This function will execute on the balenaOS host OS +function remote__get_container_name { + local service="$1" + local len="${#service}" + local -a names + read -ra names <<< "$(balena-engine ps --format '{{.Names}}')" + local result='' + local name + for name in "${names[@]}"; do + if [ "${name:0:len+1}" = "${service}_" ]; then + result="${name}" + break + fi + done + echo "${result}" +} + +# This function will execute on the balenaOS host OS with bash v5 +function remote__main { + local service="$1" + shift + local container_name + container_name="$(remote__get_container_name "${service}")" + if [ -z "${container_name}" ]; then + echo "ERROR: Cannot find a running container for service '${service}'" >/dev/stderr + exit 1 + fi + local -a args + if [ "$#" = 0 ]; then + args=('sh') + else + IFS=' ' args=('sh' '-c' "$*") + fi + local tty_flags='-i' # without '-i', STDIN is closed + [[ "$#" = 0 || -t 0 ]] && tty_flags='-it' + [ -n "${SSUU_DEBUG}" ] && set -x + balena-engine exec ${tty_flags} "${container_name}" "${args[@]}" + { local status="$?"; [ -n "${SSUU_DEBUG}" ] && set +x; } 2>/dev/null + return "${status}" +} + +# Parse a short flag specification like '-N' or '-CNL', the latter being the +# abbreviated form for '-C' '-N' '-L'. For each short flag, set a global +# variable in the format "SSUU_FLAG_${flag}". Examples: +# parse_short_flag '-N' +# -> SSUU_FLAG_N='-N' +# +# parse_short_flag '-CNL' +# -> SSUU_FLAG_C='-CNL' +# SSUU_FLAG_N='-CNL' +# SSUU_FLAG_L='-CNL' +function parse_short_flag { + local spec="$1" # flag spec like '-N' or '-CNL' + local i + for (( i=1; i<${#spec}; i++ )); do + local flag="${spec:i:1}" + if [[ "${flag}" =~ [a-zA-Z] ]]; then + declare -g SSUU_FLAG_"${flag}"="${spec}" + else + break + fi + done +} + +function split_ssh_positional_args { + local args=("$@") + local nargs=${#args[@]} + local i + SSUU_OPT_ARGS=() + SSUU_POS_ARGS=() + for (( i=0; i/dev/null + set -e + return "${status}" +} + +function print_scp_service_msg { + local service="$1" + local uuid="$2" + echo "\ +scp-uuid does not support the '--service' flag. However, files and folders +can be copied to a service container with 'ssh-uuid', for example: + +# local -> remote +$ cat local.txt | ssh-uuid --service ${service} ${uuid}.balena cat \\> /data/remote.txt + +# remote -> local +$ ssh-uuid --service ${service} ${uuid}.balena cat /data/remote.txt > local.txt + +Or multiple files and folders with 'tar' on the fly: + +# local -> remote +$ tar cz local-folder | ssh-uuid --service ${service} ${uuid}.balena tar xzvC /data/ + +# remote -> local +$ ssh-uuid --service ${service} ${uuid}.balena tar czC /data remote-folder | tar xvz + +Or multiple files and folders with the powerful 'rsync' tool: + +# local -> remote +$ rsync -av -e 'ssh-uuid --service ${service}' local-folder ${uuid}.balena:/data/ + +# remote -> local +$ rsync -av -e 'ssh-uuid --service ${service}' ${uuid}.balena:/data/remote-folder . + +In these examples respectively, 'cat' or 'tar' or 'rsync' must be installed both +on the local workstation and on the remote service container: +$ apt-get install -y rsync tar # Debian, Ubuntu, etc +$ apk add rsync tar # Alpine + +Finally, if you are transferring files to/from a service's named volume (often +named 'data'), note that named volumes are also exposed on the host OS under +folder: '/mnt/data/docker/volumes/_data/_data/' +As such, it is also possible to scp to/from named volumes without '--service': + +# local -> remote +$ scp-uuid -r local-folder ${uuid}.balena:/mnt/data/docker/volumes/_data/_data/ + +# remote -> local +$ scp-uuid -r ${uuid}.balena:/mnt/data/docker/volumes/_data/_data/remote-folder . +" +} + +function run_scp { + local args=("$@") + local nargs=${#args[@]} + local found_hostname='0' + local user='' + local i + for (( i=0; i/dev/null + set -e + return "${status}" +} + +# Run socat +function do_proxy { + local TARGET_HOST="$1" + local TARGET_PORT="$2" + local PROXY_AUTH_FILE="${BALENARC_DATA_DIRECTORY}/proxy-auth" + local SOCAT_PORT + SOCAT_PORT="$(get_rand_port_num)" + mkdir -p "${BALENARC_DATA_DIRECTORY}" || quit "Cannot write to '${BALENARC_DATA_DIRECTORY}'" + echo -n "${BALENA_USERNAME}:${BALENA_TOKEN}" > "${PROXY_AUTH_FILE}" || quit "Cannot write to '${PROXY_AUTH_FILE}'" + [ -n "${DEBUG}" ] && set -x + socat "TCP-LISTEN:${SOCAT_PORT},bind=127.0.0.1" "OPENSSL:tunnel.balena-cloud.com:443,snihost=tunnel.balena-cloud.com" & + { set +x; } 2>/dev/null + sleep 1 # poor man's wait for the background socat process to be ready + [ -n "${DEBUG}" ] && set -x + socat - "PROXY:127.0.0.1:${TARGET_HOST}:${TARGET_PORT},proxyport=${SOCAT_PORT},proxy-authorization-file=${PROXY_AUTH_FILE}" + { local status="$?"; [ -n "${DEBUG}" ] && set +x; } 2>/dev/null + return "${status}" +} + +# Generate a random, possibly unavailable, TCP port number between +# 10,000 and 65,535 using a shady, questionable algorithm that assumes, +# based on annecodtal evidencce, that port numbers lower than 10,000 +# are less likely to be available. +# If the port number is already in use, socat will produce an error. +function get_rand_port_num { + # In bash, "$RANDOM" produces a random integer between 0 and 32767. + # RANDOM * 2 produces an even number reasonably evenly distributed + # over the range from 0 to 65534, and then RANDOM % 2 adds 0 or 1. + # '% 55536 + 10000' then coerces the range into 10,000 to 65,535. + # (This spoils the even distribution, yes, but the real problem is + # ensuring that the port number is not in use. It does not need + # to be random, it needs to be available.) + echo $(( (RANDOM * 2 + RANDOM % 2) % 55536 + 10000 )) +} + +function check_tool { + local tool + tool=$(which "$1") + if [[ ! -x "${tool}" ]]; then + quit "Unable to execute '${tool}'. Is it installed?" + fi +} + +function main { + get_user_and_token + if [ "$1" = 'do_proxy' ]; then + shift + do_proxy "$@" + else + case "$(basename "$0")" in + 'ssh-uuid.sh') run_ssh "$@";; + 'ssh-uuid') run_ssh "$@";; + 'scp-uuid') run_scp "$@";; + esac + fi +} + +main "$@" diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..3519055 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,388 @@ +#!/usr/bin/env bash + +# Copyright 2022 Balena Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TEST_DIR="$(dirname "${BASH_SOURCE[0]}")" + +if [ -f "${TEST_DIR}/test-config.sh" ]; then + # shellcheck disable=SC1091 + source "${TEST_DIR}/test-config.sh" +fi + +# BEGIN - test configuration variables +# The tests are executed against a real/live balenaOS device on the local network +# (either production or development image). These variables specify the details of +# the test device. The device should be running an application with services. +# One of the services should have 'rsync' installed and the service name should be +# assigned to the TEST_SERVICE variable. The TEST_SERVICE_ISSUE variable should be +# assigned the contents of the '/etc/issue' file for the service. You can avoid +# editing this file directly by creating a a file named 'test-config.sh' on the +# same folder as this file, and setting you device configuration variables there. +TEST_DEVICE_IP="${TEST_DEVICE_IP:-192.168.1.50}" +TEST_DEVICE_UUID="${TEST_DEVICE_UUID:-a123456abcdef123456abcdef1234567}" +TEST_DEVICE_HOST="${TEST_DEVICE_UUID}.balena" +TEST_DEVICE_OS_VERSION="${TEST_DEVICE_OS_VERSION:-2.85.2}" +TEST_SERVICE="${TEST_SERVICE:-my-service}" +TEST_SERVICE_ISSUE="${TEST_SERVICE_ISSUE:-Debian GNU/Linux 11 \n \l}" +# END - test configuration variables + +SCP_UUID="${TEST_DIR}/scp-uuid" +SSH_UUID="${TEST_DIR}/ssh-uuid" + +SSH_WITH_IP_ADDRESS=(ssh -p 22222 "root@${TEST_DEVICE_IP}") +SSH_WITH_UUID=("${SSH_UUID}" "${TEST_DEVICE_HOST}") +SSH_WITH_SERVICE=("${SSH_UUID}" --service "${TEST_SERVICE}" "${TEST_DEVICE_HOST}") + +TEST_COUNTER=0 + +function quit { + echo -e "\nERROR: $1" >/dev/stderr + exit 1 +} + +# escape array +function escape_a { + # local escaped_str + # printf -v escaped_str '%q ' "$@" + # declare -ag ESCAPED_ARRAY="( ${escaped_str} )" + printf '%q ' "$@" +} + +function run_test { + local test_name="$1" + local -a cmd="($2)" # unescaped array + local expected="$3" + local expected_status="$4" + local actual + local actual_status + + set -x + actual="$("${cmd[@]}" 2>&1)" + { actual_status="$?"; set +x; } 2>/dev/null + + # echo "actual: ~${actual}~ (${actual_status})" + # echo "expected: ~${expected}~ (${expected_status})" + + if [ "${actual}" != "${expected}" ]; then + quit "\nTEST '${test_name}': FAIL\n +Mismatched output: +Expected: '${expected}' +Actual: '${actual}'" + fi + if [ "${actual_status}" != "${expected_status}" ]; then + quit "\nTEST '${test_name}': FAIL\n +Mismatched exit status code: +Expected: '${expected_status}' +Actual: '${actual_status}'" + fi + echo -e "\nTEST '${test_name}': PASS\n" + (( TEST_COUNTER++ )) +} + +function test_host_os_status { + local c + local expected='' + local expected_status + for c in true false; do + if [ "$c" = 'true' ]; then expected_status='0'; else expected_status='1'; fi + local cmd1=("${SSH_WITH_IP_ADDRESS[@]}" "$c") + local cmd2=("${SSH_WITH_UUID[@]}" "$c") + + run_test "${FUNCNAME[0]} (IP address)" "$(escape_a "${cmd1[@]}")" "${expected}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID)" "$(escape_a "${cmd2[@]}")" "${expected}" "${expected_status}" + done +} + +function test_host_os_cat { + local cmd1=("${SSH_WITH_IP_ADDRESS[@]}" cat /etc/issue) + local cmd2=("${SSH_WITH_UUID[@]}" cat /etc/issue) + local expected="balenaOS ${TEST_DEVICE_OS_VERSION} \n \l" + local expected_status='0' + + run_test "${FUNCNAME[0]} (IP address)" "$(escape_a "${cmd1[@]}")" "${expected}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID)" "$(escape_a "${cmd2[@]}")" "${expected}" "${expected_status}" +} + +function test_host_os_cat_with_spaces { + local contents='hi there' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces.txt' + local cmd1=("${SSH_WITH_IP_ADDRESS[@]}" echo "${contents}" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_UUID[@]}" cat "${fname}") + local expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (IP address 1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID 1)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # change contents and swap ssh commands + contents='hi there (new contents)' + cmd1=("${SSH_WITH_UUID[@]}" echo "'${contents}'" '>' "${fname}") + expected1='' + cmd2=("${SSH_WITH_IP_ADDRESS[@]}" cat "${fname}") + expected2="${contents}" + + run_test "${FUNCNAME[0]} (IP address 2)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID 2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_host_os_cat_with_encoded_line_breaks { + local contents='hi there\nline2' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces.txt' + local cmd1=("${SSH_WITH_IP_ADDRESS[@]}" echo -e "'${contents}'" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_UUID[@]}" cat "${fname}") + local expected2 + printf -v expected2 '%b' "${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (IP address 1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID 1)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # change contents and swap ssh commands + contents='hi there\nline2 (new contents)' + cmd1=("${SSH_WITH_UUID[@]}" echo -e "'${contents}'" '>' "${fname}") + expected1='' + cmd2=("${SSH_WITH_IP_ADDRESS[@]}" cat "${fname}") + printf -v expected2 '%b' "${contents}" + + run_test "${FUNCNAME[0]} (UUID 2)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (IP address 2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_host_os_cat_with_unencoded_line_breaks { + local contents='hi there +line2 (with unencoded newline characters)' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces.txt' + local cmd1=("${SSH_WITH_IP_ADDRESS[@]}" echo "'${contents}'" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_UUID[@]}" cat "${fname}") + local expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (IP address 1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (UUID 1)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # change contents and swap ssh commands + contents='hi there +line2 (with unencoded newline characters) +(new contents)' + cmd1=("${SSH_WITH_UUID[@]}" echo "'${contents}'" '>' "${fname}") + expected1='' + cmd2=("${SSH_WITH_IP_ADDRESS[@]}" cat "${fname}") + expected2="${contents}" + + run_test "${FUNCNAME[0]} (UUID 2)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (IP address 2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_service_status { + local c + local expected='' + local expected_status + for c in true false; do + if [ "$c" = 'true' ]; then expected_status='0'; else expected_status='1'; fi + local cmd=("${SSH_WITH_SERVICE[@]}" "$c") + + run_test "${FUNCNAME[0]} (UUID)" "$(escape_a "${cmd[@]}")" "${expected}" "${expected_status}" + done +} + +function test_service_cat { + local cmd=("${SSH_WITH_SERVICE[@]}" cat /etc/issue) + local expected="${TEST_SERVICE_ISSUE}" + local expected_status='0' + + run_test "${FUNCNAME[0]}" "$(escape_a "${cmd[@]}")" "${expected}" "${expected_status}" +} + +function test_service_cat_with_spaces { + local contents='hi there' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces\ \(service\).txt' + local cmd1=("${SSH_WITH_SERVICE[@]}" echo "${contents}" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_SERVICE[@]}" cat "${fname}") + local expected2 + expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_service_cat_with_encoded_line_breaks { + local contents='hi there +line2 (service)' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces\ \(service\).txt' + local cmd1=("${SSH_WITH_SERVICE[@]}" echo "'${contents}'" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_SERVICE[@]}" cat "${fname}") + local expected2 + printf -v expected2 '%b' "${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_service_cat_with_unencoded_line_breaks { + local contents='hi there +line2 (service)' + local fname='/tmp/test\ host\ os\ cat\ with\ spaces\ \(service\).txt' + local cmd1=("${SSH_WITH_SERVICE[@]}" echo "'${contents}'" '>' "${fname}") + local expected1='' + local cmd2=("${SSH_WITH_SERVICE[@]}" cat "${fname}") + local expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" +} + +function test_scp_local_to_remote { + local contents="hi there +(local)" + local fname='/tmp/local copy.txt' + local escaped_fname + printf -v escaped_fname '%q' "${fname}" + + # clean up previous runs and create local file + "${SSH_WITH_IP_ADDRESS[@]}" rm -f "${escaped_fname}" + echo "${contents}" > "${fname}" + + local cmd1=("${SCP_UUID}" "${fname}" "${TEST_DEVICE_HOST}:${escaped_fname}") + local expected1='' + local cmd2=("${SSH_WITH_IP_ADDRESS[@]}" cat "${escaped_fname}") + local expected2 + expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # clean up + rm -f "${fname}" +} + +function test_scp_remote_to_local { + local contents="hi there +(remote)" + local fname='/tmp/remote copy.txt' + local escaped_fname + printf -v escaped_fname '%q' "${fname}" + + # clean up previous runs and create remote file + rm -f "${fname}" + "${SSH_WITH_IP_ADDRESS[@]}" echo "'${contents}'" '>' "${escaped_fname}" + + local cmd1=("${SCP_UUID}" "${TEST_DEVICE_HOST}:${escaped_fname}" "${fname}") + local expected1='' + local cmd2=(cat "${fname}") + expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # clean up + rm -f "${fname}" +} + +function test_rsync_local_to_remote { + set -x + local contents="hi there +(local, rsync)" + local fname='/tmp/local copy (rsync).txt' + local escaped_fname + printf -v escaped_fname '%q' "${fname}" + + # clean up previous runs and create local file + "${SSH_WITH_SERVICE[@]}" rm -f "${escaped_fname}" + echo "${contents}" > "${fname}" + + local cmd1=('rsync' '-e' "ssh-uuid --service ${TEST_SERVICE}" "${fname}" "${TEST_DEVICE_HOST}:${escaped_fname}") + local expected1='' + local cmd2=("${SSH_WITH_SERVICE[@]}" cat "${escaped_fname}") + local expected2 + expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # clean up + rm -f "${fname}" + "${SSH_WITH_SERVICE[@]}" rm -f "${escaped_fname}" +} + +function test_rsync_remote_to_local { + set -x + local contents="hi there +(remote, rsync)" + local fname='/tmp/remote copy (rsync).txt' + local escaped_fname + printf -v escaped_fname '%q' "${fname}" + + # clean up previous runs and create local file + rm -f "${fname}" + "${SSH_WITH_SERVICE[@]}" echo "'${contents}'" '>' "${escaped_fname}" + + local cmd1=('rsync' '-e' "ssh-uuid --service ${TEST_SERVICE}" "${TEST_DEVICE_HOST}:${escaped_fname}" "${fname}") + local expected1='' + local cmd2=(cat "${fname}") + local expected2 + expected2="${contents}" + local expected_status='0' + + run_test "${FUNCNAME[0]} (1)" "$(escape_a "${cmd1[@]}")" "${expected1}" "${expected_status}" + run_test "${FUNCNAME[0]} (2)" "$(escape_a "${cmd2[@]}")" "${expected2}" "${expected_status}" + + # clean up + rm -f "${fname}" + "${SSH_WITH_SERVICE[@]}" rm -f "${escaped_fname}" +} + +function test_counter { + local expected_count=35 + if [ "${TEST_COUNTER}" != "${expected_count}" ]; then + quit "\nTEST COUNT FAILED: expected '${expected_count}' tests to run, counted '${TEST_COUNTER}'" + fi + echo -e "\nALL '${TEST_COUNTER}' TESTS COMPLETED SUCCESSFULLY!\n" +} + +function run_tests { + test_host_os_status + test_service_status + + test_host_os_cat + test_host_os_cat_with_spaces + test_host_os_cat_with_encoded_line_breaks + test_host_os_cat_with_unencoded_line_breaks + + test_service_cat + test_service_cat_with_spaces + test_service_cat_with_encoded_line_breaks + test_service_cat_with_unencoded_line_breaks + + test_scp_local_to_remote + test_scp_remote_to_local + + test_rsync_local_to_remote + test_rsync_remote_to_local + + test_counter +} + +run_tests diff --git a/tests/scp-uuid b/tests/scp-uuid new file mode 120000 index 0000000..b96a9d8 --- /dev/null +++ b/tests/scp-uuid @@ -0,0 +1 @@ +../ssh-uuid.sh \ No newline at end of file diff --git a/tests/ssh-uuid b/tests/ssh-uuid new file mode 120000 index 0000000..b96a9d8 --- /dev/null +++ b/tests/ssh-uuid @@ -0,0 +1 @@ +../ssh-uuid.sh \ No newline at end of file