Public Access
Compare commits
5
Commits
ssh-client
...
uname-scp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c4d7fc467 | ||
|
|
18285c3212 | ||
|
|
e707a5f2d4 | ||
|
|
5bec851a1a | ||
|
|
9dfe675db7 |
@@ -252,6 +252,8 @@ The Shell App (docs/milestones/S1.md) runs the commands below on the device's ow
|
||||
| `tls <host> [port]` / `ntp [server]` | A TLS handshake that checks nothing, then the certificate said in words: who it is for, who signed it, until when, its SHA-256, and whether this device's roots and the name asked for accept it (about 52 KB of heap while it runs; refused under 70 KB free). A time server's clock against the device's, with the round trip |
|
||||
| `ifconfig` / `arp` / `netstat` | The interfaces (Wi-Fi and the VPN) with their addresses, MTU, which is the default route, and the DNS servers; the neighbours heard on the Wi-Fi; what listens and what is connected |
|
||||
| `ssh user@host[:port]` / `ssh status` / `ssh stop` | Opens the SSH App and connects (the password is asked there, never on a console); the session's state and this device's public key; end the session |
|
||||
| `scp [-f] [-P port] <file> user@host:path` / `scp [-f] [-P port] user@host:path <file>` | One file between the card and a server, with the device's key, or a password asked (masked) in the Shell, to a server the SSH App already trusts; `-f` replaces a file on the card; `cancel` stops it |
|
||||
| `uname` | The firmware, its version and the chip it is built for (IRC has `/uname`) |
|
||||
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
||||
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
||||
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cctype>
|
||||
|
||||
#include "base64.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
@@ -284,7 +285,7 @@ void IrcSession::onPrivmsg(const IrcMessage& m, int64_t utc, bool notice) {
|
||||
action = true;
|
||||
text = rest;
|
||||
} else {
|
||||
if (verb == "VERSION" && !notice) send(std::string("NOTICE ") + from + " :" + kCtcp + "VERSION roro9stack" + kCtcp);
|
||||
if (verb == "VERSION" && !notice) send(std::string("NOTICE ") + from + " :" + kCtcp + "VERSION " + kProductName + " " + versionString() + kCtcp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -359,10 +360,12 @@ void IrcSession::command(int b, const std::string& text, int64_t utc) {
|
||||
} else if (verb == "quit") {
|
||||
quit_ = true;
|
||||
send(IrcMessage::serialize("QUIT", {rest.empty() ? "roro9stack" : rest}));
|
||||
} else if (verb == "uname") { // shown here, not said to anyone
|
||||
info(b, unameString(), utc);
|
||||
} else if (verb == "raw" || verb == "quote") {
|
||||
if (!rest.empty()) send(rest);
|
||||
} else {
|
||||
info(b, "Unknown command /" + verb + " (try /join or /j, /part /msg /me /nick /topic /names /quit /raw)", utc);
|
||||
info(b, "Unknown command /" + verb + " (try /join or /j, /part /msg /me /nick /topic /names /uname /quit /raw)", utc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "scp_args.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace roro::term {
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> words(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
for (size_t at = 0; at < text.size();) {
|
||||
size_t end = text.find(' ', at);
|
||||
if (end == std::string::npos) end = text.size();
|
||||
if (end > at) out.push_back(text.substr(at, end - at));
|
||||
at = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// user@host:path, as opposed to a path on the card.
|
||||
bool isRemote(const std::string& w) {
|
||||
if (w.empty() || w[0] == '/') return false;
|
||||
size_t at = w.find('@'), colon = w.find(':');
|
||||
return at != std::string::npos && colon != std::string::npos && at < colon;
|
||||
}
|
||||
|
||||
std::string baseName(const std::string& path) {
|
||||
size_t slash = path.rfind('/');
|
||||
return slash == std::string::npos ? path : path.substr(slash + 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string parseScp(const std::string& args, ScpArgs& out) {
|
||||
static const char* kUsage = "scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card>";
|
||||
std::vector<std::string> w = words(args);
|
||||
long port = 0;
|
||||
bool replace = false;
|
||||
while (!w.empty() && w[0][0] == '-') {
|
||||
if (w[0] == "-f") {
|
||||
replace = true;
|
||||
w.erase(w.begin());
|
||||
} else if (w[0] == "-P" && w.size() >= 2) {
|
||||
port = 0;
|
||||
for (char c : w[1]) {
|
||||
if (c < '0' || c > '9' || port > 65535) return "a port from 1 to 65535";
|
||||
port = port * 10 + (c - '0');
|
||||
}
|
||||
if (port < 1 || port > 65535) return "a port from 1 to 65535";
|
||||
w.erase(w.begin(), w.begin() + 2);
|
||||
} else {
|
||||
return kUsage;
|
||||
}
|
||||
}
|
||||
if (w.size() != 2) return kUsage;
|
||||
|
||||
bool firstRemote = isRemote(w[0]), secondRemote = isRemote(w[1]);
|
||||
if (firstRemote == secondRemote) return firstRemote ? "one side has to be on the card, not both on servers" : kUsage;
|
||||
|
||||
ScpArgs a;
|
||||
a.upload = secondRemote;
|
||||
a.replace = replace;
|
||||
const std::string& remote = a.upload ? w[1] : w[0];
|
||||
std::string local = a.upload ? w[0] : w[1];
|
||||
size_t colon = remote.find(':');
|
||||
std::string why = parseSshTarget(remote.substr(0, colon), a.target);
|
||||
if (!why.empty()) return why;
|
||||
if (port) a.target.port = static_cast<uint16_t>(port);
|
||||
a.remote = remote.substr(colon + 1);
|
||||
if (a.remote.empty()) return "say where on " + a.target.host + ": user@host:path";
|
||||
if (local.empty() || local[0] != '/') return "a path on the card starts with a slash";
|
||||
if (local.back() == '/') {
|
||||
if (a.upload) return "that's a folder: scp copies one file";
|
||||
std::string name = baseName(a.remote);
|
||||
if (name.empty()) return "name the file to fetch";
|
||||
local += name;
|
||||
}
|
||||
a.local = local;
|
||||
out = a;
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace roro::term
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ssh_hosts.h"
|
||||
|
||||
namespace roro::term {
|
||||
|
||||
// What `scp` was asked: one file to the card from a server, or from the card to a server.
|
||||
// scp [-f] [-P port] /notes/a.txt user@host:path (upload)
|
||||
// scp [-f] [-P port] user@host:path /notes/a.txt (download; a destination ending in / gets the remote file's name)
|
||||
// The card's paths start with a slash; the server's are the server's own (relative ones start at the home folder).
|
||||
struct ScpArgs {
|
||||
bool upload = false;
|
||||
bool replace = false; // -f: a download may replace a file on the card
|
||||
SshTarget target;
|
||||
std::string local, remote;
|
||||
};
|
||||
|
||||
// "" or what is wrong with it.
|
||||
std::string parseScp(const std::string& args, ScpArgs& out);
|
||||
|
||||
} // namespace roro::term
|
||||
@@ -8,8 +8,26 @@
|
||||
#define RORO_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
#if __has_include("sdkconfig.h")
|
||||
#include "sdkconfig.h"
|
||||
#endif
|
||||
|
||||
namespace roro {
|
||||
|
||||
const char* versionString() { return RORO_VERSION; }
|
||||
|
||||
const char* architectureString() {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
return "xtensa-lx7 esp32s3";
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32)
|
||||
return "xtensa-lx6 esp32";
|
||||
#elif defined(CONFIG_IDF_TARGET)
|
||||
return CONFIG_IDF_TARGET;
|
||||
#else
|
||||
return "host";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string unameString() { return std::string(kProductName) + " " + versionString() + " " + architectureString(); }
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace roro {
|
||||
|
||||
constexpr const char* kProductName = "roro9stack";
|
||||
@@ -9,4 +11,10 @@ constexpr const char* kProductName = "roro9stack";
|
||||
// that one file, and everything else comes from the build cache (issue #74).
|
||||
const char* versionString();
|
||||
|
||||
// The chip this firmware is built for: "xtensa-lx7 esp32s3". "host" in the native tests.
|
||||
const char* architectureString();
|
||||
|
||||
// What `uname` and IRC's /uname report: "roro9stack v0.22.0 xtensa-lx7 esp32s3".
|
||||
std::string unameString();
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -26,6 +26,8 @@ lib_deps =
|
||||
jgromes/RadioLib @ 7.8.1
|
||||
esphome/wireguard @ 0.4.8
|
||||
ewpa/LibSSH-ESP32 @ 5.10.0
|
||||
; WireGuard brings libsodium in; 1.10021.12 defines crypto_sign_ed25519_open as LibSSH does, and the two do not link
|
||||
esphome/libsodium @ 1.10021.11
|
||||
test_ignore = *
|
||||
; Smaller TLS buffers (M2): the framework is rebuilt with these settings (pioarduino "hybrid
|
||||
; compile"). Receive stays 16 KB (servers send full TLS records); send drops to 4 KB (IRC lines are
|
||||
|
||||
@@ -42,6 +42,8 @@ Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings
|
||||
ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time
|
||||
tls <host> [port] | ntp [server] a TLS handshake: who the certificate is for, by whom, until when, and whether this device trusts it; a time server's clock against this one
|
||||
ifconfig | arp | netstat the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard; what listens and what is connected
|
||||
uname the firmware, its version and the chip it is built for
|
||||
scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card> one file over SSH, with this device's key or, in the Shell, a password, to a server the SSH App already trusts; -f replaces a file on the card; `cancel` stops it
|
||||
ssh user@host[:port] | ssh status | ssh stop a terminal on another machine, in the SSH App; the password is asked there, never here
|
||||
vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself
|
||||
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
|
||||
@@ -58,7 +60,7 @@ get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) bina
|
||||
quit close the Debug Console connection
|
||||
```
|
||||
|
||||
In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few run: `help`, `info`, `tasks`, `net`, `reboot`, `boot other`, `wifi status`, and anything starting with `log level`, `crash`, `coredump`, `wifi add`, `debug`. Anything else answers `not available in Safe Mode`.
|
||||
In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few run: `help`, `uname`, `info`, `tasks`, `net`, `reboot`, `boot other`, `wifi status`, and anything starting with `log level`, `crash`, `coredump`, `wifi add`, `debug`. Anything else answers `not available in Safe Mode`.
|
||||
|
||||
## What they do
|
||||
|
||||
@@ -118,6 +120,8 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
|
||||
| `tls <host> [port]` / `ntp [server]` | A TLS handshake that checks nothing, then the certificate said in words: who it is for, who signed it, until when, its SHA-256, and whether this device's roots and the name asked for accept it (about 52 KB of heap while it runs; refused under 70 KB free). A time server's clock against the device's, with the round trip |
|
||||
| `ifconfig` / `arp` / `netstat` | The interfaces (Wi-Fi and the VPN) with their addresses, MTU, which is the default route, and the DNS servers; the neighbours heard on the Wi-Fi; what listens and what is connected |
|
||||
| `ssh user@host[:port]` / `ssh status` / `ssh stop` | Opens the SSH App and connects (the password is asked there, never on a console); the session's state and this device's public key; end the session |
|
||||
| `scp [-f] [-P port] <file> user@host:path` / `scp [-f] [-P port] user@host:path <file>` | One file between the card and a server, with the device's key, or a password asked (masked) in the Shell, to a server the SSH App already trusts; `-f` replaces a file on the card; `cancel` stops it |
|
||||
| `uname` | The firmware, its version and the chip it is built for (IRC has `/uname`) |
|
||||
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
||||
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
||||
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ Yes, WireGuard: one tunnel to one server, set up by copying the client's `.conf`
|
||||
|
||||
## Can it log in to my server?
|
||||
|
||||
Yes, over SSH: the [SSH App](/guide/ssh/) is a terminal on another machine, with a password or with a key the device makes for itself. `vim`, `top` and `less` work. One session at a time, and not at the same time as IRC: there isn't the memory for both.
|
||||
Yes, over SSH: the [SSH App](/guide/ssh/) is a terminal on another machine, with a password or with a key the device makes for itself. `vim`, `top` and `less` work. One session at a time, and not at the same time as IRC: there isn't the memory for both. `scp` in the [Shell](/guide/shell/) copies a file between the card and a server.
|
||||
|
||||
## How do I copy files to and from my phone?
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ You type at the bottom; <kbd>Enter</kbd> sends. A line starting with `/` is a co
|
||||
| `/nick newnick` | Changes your nick |
|
||||
| `/topic [text]` | Shows or sets the channel topic |
|
||||
| `/names [#channel]` | Lists who is there |
|
||||
| `/uname` | Shows the firmware, its version and the chip, in this buffer only: nothing is sent |
|
||||
| `/quit [message]` | Disconnects and **stays disconnected** until you type something again |
|
||||
| `/raw …` (or `/quote`) | Sends a line to the server as it is |
|
||||
| `/settings` | The settings page |
|
||||
|
||||
Anyone who asks the device for its CTCP `VERSION` (`/ctcp roro VERSION` in most clients) is told `roro9stack` and the firmware version.
|
||||
|
||||
Each server, channel and private chat is a **buffer**. <kbd>Tab</kbd> moves to the next one, each shows how many messages are unread, and your own lines are in the accent colour. Scroll back with <kbd>Alt</kbd> + <kbd>;</kbd> (older) and <kbd>Alt</kbd> + <kbd>.</kbd> (newer). The up and down arrows (<kbd>Fn</kbd> + <kbd>;</kbd> and <kbd>.</kbd>) recall lines you sent.
|
||||
|
||||
## Mentions and the unread count
|
||||
|
||||
@@ -21,6 +21,7 @@ Type a command and press <kbd>Enter</kbd>. `help` lists them all; the [command r
|
||||
| `wifi status` | The network, the address, and where the DNS and time servers came from |
|
||||
| `ls /notes` | A folder of the SD card, with sizes and dates |
|
||||
| `crash` | The last crash, if there was one |
|
||||
| `uname` | The firmware, its version and the chip it is built for: `roro9stack v0.22.0 xtensa-lx7 esp32s3` |
|
||||
| `update check` | Whether a newer release exists |
|
||||
| `lora status` | What the radio is set to and what it has heard |
|
||||
|
||||
@@ -45,7 +46,7 @@ The capital is the difference: every command is in small letters, every App star
|
||||
|
||||
## The network
|
||||
|
||||
For the day the network doesn't do what it should. Each of the first six takes a moment and prints as it goes; one runs at a time, and `cancel` stops it.
|
||||
For the day the network doesn't do what it should. Each of the first six takes a moment and prints as it goes, and so does `scp`; one runs at a time, and `cancel` stops it.
|
||||
|
||||
| Command | Tells you |
|
||||
|---|---|
|
||||
@@ -56,6 +57,7 @@ For the day the network doesn't do what it should. Each of the first six takes a
|
||||
| `tls git.twis.la` | A secure connection's certificate: who it is for, who signed it, until when, and **whether this device trusts it**. A port may follow (443 if not) |
|
||||
| `ntp` | A time server's clock against this device's, and how far the server is. Another server may follow |
|
||||
| `ssh user@host` | Opens the [SSH App](/guide/ssh/) and connects; `ssh status` and `ssh stop` for the session |
|
||||
| `scp /notes/a.txt user@host:docs/` | One file between the card and a server, over [SSH](/guide/ssh/#copying-a-file-scp): with the device's key, or a password asked in the Shell, masked. `cancel` stops it |
|
||||
| `ifconfig` | The interfaces (Wi-Fi, and the [VPN](/guide/vpn/) when it is up), their addresses, **which one is the default route**, and the DNS servers |
|
||||
| `arp` | The neighbours heard on the Wi-Fi: is the gateway there at all |
|
||||
| `netstat` | What the device **listens** on (updates, the Debug Console, sharing) and what is connected to it now |
|
||||
|
||||
@@ -9,7 +9,7 @@ screens = ["ssh.png", "ssh-trust.png", "ssh-terminal.png", "ssh-key.png", "ssh-c
|
||||
|
||||
The SSH App opens a **terminal on another machine**: a server, a Raspberry Pi, a router. What you type goes there, and what its programs print is drawn here: a shell, `less`, `top`, `nano`, `vim`.
|
||||
|
||||
It is a client and nothing more: one session at a time, to a shell. No file transfer, no port forwarding, no jump hosts.
|
||||
It is a client and nothing more: one session at a time, to a shell. No port forwarding, no jump hosts. Files go one at a time with [`scp` from the Shell](#copying-a-file-scp).
|
||||
|
||||
## Connecting
|
||||
|
||||
@@ -89,6 +89,26 @@ A session takes about **50 KB** of the device's 100 KB while it is open, and giv
|
||||
|
||||
See [When a connection says "not enough memory"](/howto/not-enough-memory/).
|
||||
|
||||
## Copying a file: scp
|
||||
|
||||
From the [Shell](/guide/shell/), `scp` copies **one file** between the SD card and a server, over the same SSH:
|
||||
|
||||
```
|
||||
scp /notes/a.txt user@host:docs/a.txt the card to the server
|
||||
scp user@host:/var/log/syslog /logs/ the server to the card, keeping its name
|
||||
scp -P 2222 /notes/a.txt user@host: another port (-P first); a path is needed after the colon
|
||||
scp -f user@host:x.txt /notes/x.txt replace a file that is on the card already
|
||||
```
|
||||
|
||||
Paths on the card start with a slash. A path on the server is the server's own, and starts in the user's home folder when it has no slash. Only a file: no folders, no `*`.
|
||||
|
||||
- **The server has to be one you have already trusted** in this App (the fingerprint question above). `scp` can't ask that question, and refuses a server it doesn't know, or one whose key changed.
|
||||
- **It logs in with the device's key, and asks for the password** if the server doesn't accept the key: the Shell shows the question and what you type is masked (<kbd>Back</kbd> cancels). The password is asked only in the Shell, not from a PC on the USB port or the Debug Console.
|
||||
- **A download arrives under a temporary name** and takes its real one when it is all there, so a cut connection never leaves half a file. A file already there is not replaced without `-f`.
|
||||
- It prints how long it took. `cancel` stops it. It needs the same 75 KB of free memory as a session, and **not while a session is open**.
|
||||
|
||||
See [Copy a file to or from a server](/howto/scp/).
|
||||
|
||||
## From the Shell
|
||||
|
||||
```
|
||||
@@ -96,6 +116,7 @@ ssh user@host connect, in the SSH App
|
||||
ssh user@host:2222 on another port
|
||||
ssh status the session, and this device's public key
|
||||
ssh stop end the session
|
||||
scp ... copy one file, see above
|
||||
```
|
||||
|
||||
## Keys
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "How-tos"
|
||||
description = "Short recipes for things you will want to do: move files with your phone, set up the VPN, log in to a server with the device's SSH key, take a screenshot, find out why the network doesn't work, record a track, capture radio packets, and what to try when something does not work."
|
||||
description = "Short recipes for things you will want to do: move files with your phone, set up the VPN, log in to a server with the device's SSH key, copy a file to or from a server, take a screenshot, find out why the network doesn't work, record a track, capture radio packets, and what to try when something does not work."
|
||||
template = "guide-index.html"
|
||||
page_template = "guide-page.html"
|
||||
sort_by = "weight"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
+++
|
||||
title = "Copy a file to or from a server"
|
||||
description = "Send a note from the SD card to a server, or fetch a log from it, with scp in the Shell."
|
||||
weight = 14
|
||||
[extra]
|
||||
tag = "SSH"
|
||||
+++
|
||||
|
||||
One file at a time, between the SD card and a server you can log in to over SSH.
|
||||
|
||||
1. **Connect to the server once in the SSH App** (**SSH → New connection**, `user@host`) and trust its fingerprint. `scp` can't ask that question, so it only goes to servers it has met there.
|
||||
2. For no password to type, put the device's key on the server: [Log in to a server without a password](/howto/ssh-key/). Without it, `scp` asks for the password.
|
||||
3. Open the **Shell** and type, for a file going **to** the server:
|
||||
|
||||
```
|
||||
scp /notes/a.txt user@host:docs/a.txt
|
||||
```
|
||||
|
||||
or one coming **from** it, into a folder on the card (it keeps its name):
|
||||
|
||||
```
|
||||
scp user@host:/var/log/syslog /logs/
|
||||
```
|
||||
|
||||
You should see `scp: ... (1234 bytes) to ...`, then `scp: sent 1234 bytes in 310 ms` (or `received`).
|
||||
4. If it says `password for host`, type it and press <kbd>Enter</kbd>: it shows as stars and is kept nowhere. <kbd>Back</kbd> cancels.
|
||||
|
||||
## If it doesn't work
|
||||
|
||||
| It says | Do |
|
||||
|---|---|
|
||||
| `is not a server this device trusts yet` | Step 1: connect once in the SSH App |
|
||||
| `showed a different key than the one remembered` | The server was reinstalled, or something answers in its place. Check in the SSH App, which asks what to do |
|
||||
| `exists already` | The file is on the card: add `-f` (`scp -f user@host:x /notes/x`) to replace it |
|
||||
| `An SSH session is open` or `Not enough memory` | End the session (`ssh stop`), or close IRC or a Gemini page: [not enough memory](/howto/not-enough-memory/) |
|
||||
| `not logged in`, `wrong password` | Three tries, then it stops. Check the user, or the key on the server |
|
||||
| `a password is only asked in the Shell` | You typed it from a PC on the USB port or the Debug Console: use the Shell, or put the key on the server |
|
||||
| `that isn't a plain file` | `scp` copies one file, not a folder |
|
||||
|
||||
Another port goes first: `scp -P 2222 /notes/a.txt user@host:a.txt`. `cancel` stops a copy that is running.
|
||||
@@ -17,7 +17,7 @@ Typing a password on a small keyboard, every time, gets old. With a key, the ser
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
4. On the Cardputer, connect: **SSH → New connection**, `user@host`. It logs in without asking for anything.
|
||||
4. On the Cardputer, connect: **SSH → New connection**, `user@host`. It logs in without asking for anything. The same key is what [`scp`](/howto/scp/) uses.
|
||||
|
||||
## If it still asks for a password
|
||||
|
||||
|
||||
+32
-1
@@ -33,6 +33,9 @@ void ShellApp::onEnter() {
|
||||
|
||||
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
|
||||
void ShellApp::onExit() {
|
||||
if (password_ && abort) abort();
|
||||
password_ = false;
|
||||
input_.setText("");
|
||||
console.closeShellRing();
|
||||
console.shellShowsAll(false);
|
||||
open_ = false;
|
||||
@@ -49,6 +52,11 @@ void ShellApp::update(uint32_t) {
|
||||
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
|
||||
log_.feed(reinterpret_cast<const char*>(buf), n);
|
||||
}
|
||||
if (!password_ && asking && asking()) { // after the question has been printed above
|
||||
password_ = true;
|
||||
input_.setText("");
|
||||
requestRedraw();
|
||||
}
|
||||
if (log_.revision() != seenRevision_) {
|
||||
seenRevision_ = log_.revision();
|
||||
requestRedraw();
|
||||
@@ -114,6 +122,23 @@ bool ShellApp::onKey(const KeyEvent& e) {
|
||||
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
|
||||
return true;
|
||||
}
|
||||
if (password_) { // masked, kept nowhere: not in the history, not in the log
|
||||
switch (e.key) {
|
||||
case Key::Char: input_.insert(e.ch); break;
|
||||
case Key::Delete: input_.backspace(); break;
|
||||
case Key::Select: {
|
||||
std::string typed = input_.text();
|
||||
input_.setText("");
|
||||
password_ = false;
|
||||
log_.add("> " + std::string(typed.size(), '*'));
|
||||
if (answer) answer(typed);
|
||||
typed.assign(typed.size(), '\0');
|
||||
break;
|
||||
}
|
||||
default: return false; // Back leaves the Shell, which cancels the copy
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::string recalled;
|
||||
switch (e.key) {
|
||||
case Key::Char: input_.insert(e.ch); break;
|
||||
@@ -197,7 +222,13 @@ void ShellApp::draw(Canvas& c) {
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
|
||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
if (password_) {
|
||||
LineEditor stars(240);
|
||||
stars.setText(std::string(input_.text().size(), '*'));
|
||||
widgets::lineEditor(c, stars, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
} else {
|
||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
}
|
||||
if (confirm_) widgets::dialog(c, "Delete?", question_, *confirm_);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ class ShellApp : public App {
|
||||
ShellApp(Run run, Probe probe, List list, Count count, const char* helpText, AppManager& apps)
|
||||
: run_(std::move(run)), probe_(std::move(probe)), list_(std::move(list)), count_(std::move(count)), helpText_(helpText), apps_(apps) {}
|
||||
|
||||
// `scp` asking for a password: while asking() is true, the next line typed is masked and goes to
|
||||
// answer() instead of the console; leaving the Shell calls abort().
|
||||
std::function<bool()> asking;
|
||||
std::function<void(const std::string&)> answer;
|
||||
std::function<void()> abort;
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
@@ -63,6 +69,7 @@ class ShellApp : public App {
|
||||
uint32_t ringPos_ = 0, seenRevision_ = 0;
|
||||
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
||||
bool open_ = false;
|
||||
bool password_ = false; // the line being typed is a password
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
+26
-2
@@ -14,6 +14,7 @@
|
||||
#include "apps/shell_app.h"
|
||||
#include "apps/ssh_app.h"
|
||||
#include "services/net_tools.h"
|
||||
#include "services/scp_service.h"
|
||||
#include "services/ssh_service.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
@@ -63,6 +64,7 @@
|
||||
#include "storage_paths.h"
|
||||
#include "ui/notifier.h"
|
||||
#include "ui/screen.h"
|
||||
#include "scp_args.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace roro;
|
||||
@@ -95,6 +97,8 @@ static VpnService* vpnService; // not in Safe Mode
|
||||
static NetTools netTools; // ping, nslookup and the rest (issue #90)
|
||||
static SshService* sshService; // not in Safe Mode (issue #2)
|
||||
static SshApp* sshApp;
|
||||
static ShellApp* shellApp;
|
||||
static ScpService* scpService; // not in Safe Mode
|
||||
static Notifier* notifier;
|
||||
static LauncherApp launcher;
|
||||
static AppManager* apps;
|
||||
@@ -235,8 +239,13 @@ void setup() {
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
scpService = new ScpService(settings, *storageService, *fileOps);
|
||||
apps->registerApp({"ssh", "SSH", false, sshApp});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps)});
|
||||
shellApp = new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps);
|
||||
apps->registerApp({"shell", "Shell", false, shellApp});
|
||||
shellApp->asking = [] { return scpService->asking(); };
|
||||
shellApp->answer = [](const std::string& password) { scpService->answer(password); };
|
||||
shellApp->abort = [] { scpService->cancel(); };
|
||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
netTools.ntpServer = []() { return settings.getString(Setting::Ntp1); };
|
||||
@@ -688,6 +697,8 @@ static const char* const kHelp =
|
||||
"ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time\n"
|
||||
"tls <host> [port] | ntp [server] a TLS handshake: who the certificate is for, by whom, until when, and whether this device trusts it; a time server's clock against this one\n"
|
||||
"ifconfig | arp | netstat the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard; what listens and what is connected\n"
|
||||
"uname the firmware, its version and the chip it is built for\n"
|
||||
"scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card> one file over SSH, with this device's key or, in the Shell, a password, to a server the SSH App already trusts; -f replaces a file on the card; `cancel` stops it\n"
|
||||
"ssh user@host[:port] | ssh status | ssh stop a terminal on another machine, in the SSH App; the password is asked there, never here\n"
|
||||
"vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself\n"
|
||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
|
||||
@@ -771,6 +782,16 @@ static void sshCommand(const String& arg) {
|
||||
}
|
||||
}
|
||||
|
||||
// `scp ...`: see ScpService. It needs the key and the trusted server the SSH App sets up.
|
||||
static void scpCommand(const String& arg) {
|
||||
if (!scpService) return (void)console.println("scp: not available in Safe Mode");
|
||||
term::ScpArgs args;
|
||||
std::string why = term::parseScp(arg.c_str(), args);
|
||||
if (!why.empty()) return (void)console.printf("scp: %s\n", why.c_str());
|
||||
why = scpService->start(args, sshService && sshService->state() != SshService::State::Idle && sshService->state() != SshService::State::Ended);
|
||||
if (!why.empty()) console.printf("scp: error %s\n", why.c_str());
|
||||
}
|
||||
|
||||
// `vpn ...` (issue #8). Nothing here prints a key.
|
||||
static void vpnCommand(const String& arg) {
|
||||
if (!vpnService) return (void)console.println("vpn: not available in Safe Mode");
|
||||
@@ -840,7 +861,7 @@ static void screenshotStep() {
|
||||
|
||||
// Commands that only touch what Safe Mode starts.
|
||||
static bool safeModeCommand(const String& line) {
|
||||
return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
|
||||
return line == "help" || line == "uname" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
|
||||
line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") ||
|
||||
line == "wifi status" || line.startsWith("wifi add ") || line.startsWith("debug ");
|
||||
}
|
||||
@@ -905,6 +926,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
}
|
||||
if (netTools.command(line.c_str())) return;
|
||||
if (line == "cancel") netTools.cancel(); // and a copy or a delete, below
|
||||
if (line == "uname" || line == "uname -a") return (void)console.println(unameString().c_str());
|
||||
if (line.startsWith("scp ")) return scpCommand(line.substring(4));
|
||||
if (line == "cancel" && scpService) scpService->cancel();
|
||||
if (line == "ssh" || line.startsWith("ssh ")) return sshCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line == "vpn" || line.startsWith("vpn ")) return vpnCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
#include <libssh_esp32.h>
|
||||
|
||||
namespace roro::sshutil {
|
||||
|
||||
std::string fingerprintOf(ssh_session s) {
|
||||
ssh_key key = nullptr;
|
||||
unsigned char* hash = nullptr;
|
||||
size_t len = 0;
|
||||
std::string out;
|
||||
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
|
||||
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
|
||||
out = text; // "SHA256:..."
|
||||
ssh_string_free_char(text);
|
||||
}
|
||||
ssh_clean_pubkey_hash(&hash);
|
||||
}
|
||||
if (key) ssh_key_free(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
void startLibrary() {
|
||||
static bool started = false;
|
||||
if (!started) libssh_begin();
|
||||
started = true;
|
||||
}
|
||||
|
||||
} // namespace roro::sshutil
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
// What the SSH Service and the Scp Service both do with libssh.
|
||||
namespace roro::sshutil {
|
||||
|
||||
// libssh_begin(), once.
|
||||
void startLibrary();
|
||||
// "SHA256:..." of the key the server showed, or "".
|
||||
std::string fingerprintOf(ssh_session s);
|
||||
|
||||
} // namespace roro::sshutil
|
||||
@@ -0,0 +1,269 @@
|
||||
#include "services/scp_service.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kStack = 20480; // as the SSH Service's
|
||||
constexpr size_t kPiece = 4096;
|
||||
|
||||
std::string baseName(const std::string& path) {
|
||||
size_t slash = path.rfind('/');
|
||||
return slash == std::string::npos ? path : path.substr(slash + 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct ScpService::Job {
|
||||
ScpService* owner;
|
||||
Console::Origin from;
|
||||
term::ScpArgs args;
|
||||
std::string privateKey, known;
|
||||
|
||||
bool stopped() const { return owner->stop_; }
|
||||
bool onCard(const std::function<void()>& work) { return owner->storage_.runAndWait(work); }
|
||||
void run();
|
||||
bool login(ssh_session s);
|
||||
void upload(ssh_session s);
|
||||
void download(ssh_session s);
|
||||
};
|
||||
|
||||
std::string ScpService::start(const term::ScpArgs& args, bool sshOpen) {
|
||||
if (busy_) return "A copy is running: `cancel` stops it";
|
||||
if (sshOpen) return "An SSH session is open: close it first, there isn't memory for both";
|
||||
if (ESP.getFreeHeap() < kNeedFree) return "Not enough memory: close IRC or a Gemini page";
|
||||
std::string key = settings_.getString(Setting::SshKey);
|
||||
if (!args.upload) {
|
||||
if (std::string why = files_.whyReadOnly(args.local, false); !why.empty()) return why;
|
||||
bool exists = false;
|
||||
if (!storage_.runAndWait([&]() { exists = SD.exists(args.local.c_str()); })) return "The card isn't answering";
|
||||
if (exists && !args.replace) return args.local + " exists already: scp -f replaces it";
|
||||
}
|
||||
auto* job = new Job{this, console.origin(), args, key, term::SshKnownHosts(settings_.getString(Setting::SshKnown)).fingerprintOf(args.target.hostPort())};
|
||||
stop_ = asking_ = answered_ = false;
|
||||
busy_ = true;
|
||||
if (xTaskCreate(task, "scp", kStack, job, 1, nullptr) != pdPASS) {
|
||||
busy_ = false;
|
||||
job->privateKey.assign(job->privateKey.size(), '\0');
|
||||
delete job;
|
||||
return "Not enough memory for it";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ScpService::task(void* arg) {
|
||||
Job* job = static_cast<Job*>(arg);
|
||||
{
|
||||
Console::As as(job->from);
|
||||
job->run();
|
||||
}
|
||||
job->privateKey.assign(job->privateKey.size(), '\0');
|
||||
ScpService* owner = job->owner;
|
||||
delete job;
|
||||
owner->busy_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void ScpService::answer(const std::string& password) {
|
||||
if (!asking_) return;
|
||||
password_ = password;
|
||||
asking_ = false;
|
||||
answered_ = true;
|
||||
}
|
||||
|
||||
// This device's key first, then the password, three tries: asked in the Shell that typed the command.
|
||||
bool ScpService::Job::login(ssh_session s) {
|
||||
int rc = ssh_userauth_none(s, nullptr);
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
int methods = ssh_userauth_list(s, nullptr);
|
||||
if (!privateKey.empty() && (methods & SSH_AUTH_METHOD_PUBLICKEY)) {
|
||||
ssh_key key = nullptr;
|
||||
if (ssh_pki_import_privkey_base64(privateKey.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) {
|
||||
rc = ssh_userauth_publickey(s, nullptr, key);
|
||||
ssh_key_free(key);
|
||||
}
|
||||
}
|
||||
privateKey.assign(privateKey.size(), '\0');
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
if (!(methods & (SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE))) {
|
||||
console.println("scp: error the server takes neither this device's key nor a password");
|
||||
return false;
|
||||
}
|
||||
if (from != Console::Origin::Shell) {
|
||||
console.println("scp: error the server doesn't know this device's key, and a password is only asked in the Shell");
|
||||
return false;
|
||||
}
|
||||
for (int tries = 0; tries < 3 && !stopped(); tries++) {
|
||||
console.printf("scp: %s's password for %s (Back cancels):\n", args.target.user.c_str(), args.target.host.c_str());
|
||||
owner->answered_ = false;
|
||||
owner->asking_ = true;
|
||||
while (!owner->answered_ && !stopped()) vTaskDelay(pdMS_TO_TICKS(50));
|
||||
owner->asking_ = false;
|
||||
if (stopped()) break;
|
||||
std::string password;
|
||||
password.swap(owner->password_);
|
||||
if (methods & SSH_AUTH_METHOD_PASSWORD) rc = ssh_userauth_password(s, nullptr, password.c_str());
|
||||
else { // asked as questions: every one gets the password
|
||||
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
|
||||
for (int round = 0; rc == SSH_AUTH_INFO && round < 4; round++) {
|
||||
int n = ssh_userauth_kbdint_getnprompts(s);
|
||||
for (int i = 0; i < n; i++) ssh_userauth_kbdint_setanswer(s, static_cast<unsigned>(i), password.c_str());
|
||||
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
password.assign(password.size(), '\0');
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
if (rc == SSH_AUTH_ERROR) break;
|
||||
console.println("scp: wrong password");
|
||||
}
|
||||
console.println(stopped() ? "scp: stopped" : "scp: error not logged in");
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScpService::Job::run() {
|
||||
sshutil::startLibrary();
|
||||
ssh_session s = ssh_new();
|
||||
if (!s) return (void)console.println("scp: error not enough memory");
|
||||
int verbosity = SSH_LOG_NOLOG, port = args.target.port;
|
||||
long timeout = 10;
|
||||
ssh_options_set(s, SSH_OPTIONS_HOST, args.target.host.c_str());
|
||||
ssh_options_set(s, SSH_OPTIONS_PORT, &port);
|
||||
ssh_options_set(s, SSH_OPTIONS_USER, args.target.user.c_str());
|
||||
ssh_options_set(s, SSH_OPTIONS_TIMEOUT, &timeout);
|
||||
ssh_options_set(s, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
|
||||
if (ssh_connect(s) != SSH_OK) {
|
||||
console.printf("scp: error no connection to %s: %s\n", args.target.hostPort().c_str(), ssh_get_error(s));
|
||||
} else if (known.empty()) {
|
||||
console.printf("scp: error %s is not a server this device trusts yet: connect with the SSH App once\n", args.target.hostPort().c_str());
|
||||
} else if (sshutil::fingerprintOf(s) != known) {
|
||||
console.printf("scp: error %s showed a different key than the one remembered: not connected\n", args.target.hostPort().c_str());
|
||||
} else if (login(s)) {
|
||||
if (args.upload) upload(s);
|
||||
else download(s);
|
||||
}
|
||||
ssh_disconnect(s);
|
||||
ssh_free(s);
|
||||
}
|
||||
|
||||
void ScpService::Job::upload(ssh_session s) {
|
||||
// The remote path is where the file goes; the name pushed is only used if that is a folder.
|
||||
ssh_scp scp = ssh_scp_new(s, SSH_SCP_WRITE, args.remote.c_str());
|
||||
if (!scp || ssh_scp_init(scp) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
if (scp) ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
File f;
|
||||
size_t size = 0;
|
||||
bool opened = onCard([&]() {
|
||||
f = SD.open(args.local.c_str());
|
||||
if (f && !f.isDirectory()) size = f.size();
|
||||
else if (f) f.close();
|
||||
});
|
||||
if (!opened || !f) {
|
||||
console.printf("scp: error cannot read %s\n", args.local.c_str());
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
console.printf("scp: %s (%u bytes) to %s:%s\n", args.local.c_str(), (unsigned)size, args.target.text().c_str(), args.remote.c_str());
|
||||
bool ok = ssh_scp_push_file(scp, baseName(args.local).c_str(), size, 0644) == SSH_OK;
|
||||
uint8_t buf[kPiece];
|
||||
uint32_t started = millis();
|
||||
size_t sent = 0;
|
||||
while (ok && sent < size && !stopped()) {
|
||||
int n = 0;
|
||||
onCard([&]() { n = f.read(buf, std::min(sizeof buf, size - sent)); });
|
||||
if (n <= 0) {
|
||||
console.printf("scp: error the card stopped reading at %u of %u\n", (unsigned)sent, (unsigned)size);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (ssh_scp_write(scp, buf, static_cast<size_t>(n)) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sent += static_cast<size_t>(n);
|
||||
}
|
||||
onCard([&]() { f.close(); });
|
||||
if (ok && stopped()) console.printf("scp: stopped at %u of %u bytes\n", (unsigned)sent, (unsigned)size);
|
||||
else if (ok) console.printf("scp: sent %u bytes in %lu ms\n", (unsigned)sent, (unsigned long)(millis() - started));
|
||||
else if (!ok && sent == 0) console.printf("scp: error the server refused it: %s\n", ssh_get_error(s));
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
}
|
||||
|
||||
void ScpService::Job::download(ssh_session s) {
|
||||
ssh_scp scp = ssh_scp_new(s, SSH_SCP_READ, args.remote.c_str());
|
||||
if (!scp || ssh_scp_init(scp) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
if (scp) ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
// Into a part file, renamed once it is all there: a failure never leaves half a file under the real name.
|
||||
std::string part = args.local + ".part";
|
||||
File f;
|
||||
bool ok = false;
|
||||
size_t got = 0, size = 0;
|
||||
uint32_t started = millis();
|
||||
if (ssh_scp_pull_request(scp) != SSH_SCP_REQUEST_NEWFILE) {
|
||||
const char* why = ssh_get_error(s);
|
||||
console.printf("scp: error %s\n", why && *why ? why : "that isn't a plain file: scp copies one file");
|
||||
} else {
|
||||
size = static_cast<size_t>(ssh_scp_request_get_size64(scp));
|
||||
ssh_scp_accept_request(scp);
|
||||
console.printf("scp: %s:%s (%u bytes) to %s\n", args.target.text().c_str(), args.remote.c_str(), (unsigned)size, args.local.c_str());
|
||||
bool opened = false;
|
||||
onCard([&]() {
|
||||
size_t slash = args.local.rfind('/');
|
||||
if (slash > 0 && !SD.exists(args.local.substr(0, slash).c_str())) SD.mkdir(args.local.substr(0, slash).c_str());
|
||||
if (SD.exists(part.c_str())) SD.remove(part.c_str());
|
||||
f = SD.open(part.c_str(), FILE_WRITE);
|
||||
opened = static_cast<bool>(f);
|
||||
});
|
||||
if (!opened) console.printf("scp: error cannot write %s\n", args.local.c_str());
|
||||
ok = opened;
|
||||
uint8_t buf[kPiece];
|
||||
while (ok && got < size && !stopped()) {
|
||||
int n = ssh_scp_read(scp, buf, std::min(sizeof buf, size - got));
|
||||
if (n <= 0) {
|
||||
console.printf("scp: error the connection ended at %u of %u bytes\n", (unsigned)got, (unsigned)size);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
size_t written = 0;
|
||||
onCard([&]() { written = f.write(buf, static_cast<size_t>(n)); });
|
||||
if (written != static_cast<size_t>(n)) {
|
||||
console.println("scp: error the card refused the write: is it full?");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
got += static_cast<size_t>(n);
|
||||
}
|
||||
if (ok && got < size) ok = false, console.printf("scp: stopped at %u of %u bytes\n", (unsigned)got, (unsigned)size);
|
||||
}
|
||||
bool renamed = false;
|
||||
onCard([&]() {
|
||||
if (f) f.close();
|
||||
if (ok) {
|
||||
if (SD.exists(args.local.c_str())) SD.remove(args.local.c_str());
|
||||
renamed = SD.rename(part.c_str(), args.local.c_str());
|
||||
} else if (SD.exists(part.c_str())) {
|
||||
SD.remove(part.c_str());
|
||||
}
|
||||
});
|
||||
if (ok && !renamed) console.println("scp: error the card refused the new name");
|
||||
else if (ok) console.printf("scp: received %u bytes in %lu ms\n", (unsigned)got, (unsigned long)(millis() - started));
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "platform/console.h"
|
||||
#include "scp_args.h"
|
||||
#include "services/file_ops.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// `scp`: one file between the card and a server, over SSH (libssh's SCP), on a task of its own that
|
||||
// prints to the console that asked. It logs in with this device's key and, if the server doesn't
|
||||
// know it, asks for the password: but only when the command was typed in the Shell, which draws
|
||||
// the question masked (asking(), answer()). It goes only to servers the SSH App has already been
|
||||
// told to trust. One at a time, and not while an SSH session is open (they don't fit in memory
|
||||
// together).
|
||||
class ScpService {
|
||||
public:
|
||||
static constexpr size_t kNeedFree = 75 * 1024;
|
||||
|
||||
ScpService(Settings& settings, StorageService& storage, FileOps& files) : settings_(settings), storage_(storage), files_(files) {}
|
||||
|
||||
// "" when it started, or why not.
|
||||
std::string start(const term::ScpArgs& args, bool sshOpen);
|
||||
void cancel() {
|
||||
stop_ = true;
|
||||
asking_ = false;
|
||||
answered_ = true; // whatever it was waiting for
|
||||
}
|
||||
bool busy() const { return busy_; }
|
||||
// The password is wanted (for user@host, in `who`); the Shell calls answer() with what was typed.
|
||||
bool asking() const { return asking_; }
|
||||
void answer(const std::string& password);
|
||||
|
||||
private:
|
||||
struct Job;
|
||||
static void task(void* arg);
|
||||
|
||||
Settings& settings_;
|
||||
StorageService& storage_;
|
||||
FileOps& files_;
|
||||
std::atomic<bool> busy_{false}, stop_{false}, asking_{false}, answered_{false};
|
||||
std::string password_; // written by answer() before answered_ is set, read by the task after
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <libssh_esp32.h>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
@@ -17,27 +19,6 @@ struct Locked {
|
||||
SemaphoreHandle_t lock_;
|
||||
};
|
||||
|
||||
std::string fingerprintOf(ssh_session s) {
|
||||
ssh_key key = nullptr;
|
||||
unsigned char* hash = nullptr;
|
||||
size_t len = 0;
|
||||
std::string out;
|
||||
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
|
||||
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
|
||||
out = text; // "SHA256:..."
|
||||
ssh_string_free_char(text);
|
||||
}
|
||||
ssh_clean_pubkey_hash(&hash);
|
||||
}
|
||||
if (key) ssh_key_free(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
void startLibrary() {
|
||||
static bool started = false;
|
||||
if (!started) libssh_begin();
|
||||
started = true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct SshService::Run {
|
||||
@@ -170,7 +151,7 @@ void SshService::task(void* arg) {
|
||||
}
|
||||
|
||||
void SshService::session(Run& run) {
|
||||
startLibrary();
|
||||
sshutil::startLibrary();
|
||||
ssh_session s = ssh_new();
|
||||
if (!s) return end("Not enough memory for the session");
|
||||
int verbosity = SSH_LOG_NOLOG;
|
||||
@@ -204,7 +185,7 @@ void SshService::session(Run& run) {
|
||||
if (stop_) return fail("Stopped");
|
||||
|
||||
// Is it the server it was last time? The first time, and when it has changed, the user decides.
|
||||
std::string seen = fingerprintOf(s);
|
||||
std::string seen = sshutil::fingerprintOf(s);
|
||||
if (seen.empty()) return fail("The server showed no key");
|
||||
if (seen != run.known) {
|
||||
{
|
||||
@@ -333,7 +314,7 @@ std::string SshService::makeKey() {
|
||||
auto work = [](void* p) {
|
||||
std::shared_ptr<Made> m = *static_cast<std::shared_ptr<Made>*>(p);
|
||||
delete static_cast<std::shared_ptr<Made>*>(p);
|
||||
startLibrary();
|
||||
sshutil::startLibrary();
|
||||
ssh_key key = nullptr, pub = nullptr;
|
||||
char *b64 = nullptr, *pub64 = nullptr;
|
||||
if (ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key) != SSH_OK) m->why = "The key couldn't be made";
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "../memory_store.h"
|
||||
#include "irc_session.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
@@ -326,7 +327,19 @@ void test_action_and_ctcp_version() {
|
||||
f.take();
|
||||
f.recv(":alice!u@h PRIVMSG roro :\x01VERSION\x01");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("NOTICE alice :\x01VERSION roro9stack\x01"));
|
||||
TEST_ASSERT_TRUE(f.sent(std::string("NOTICE alice :\x01VERSION roro9stack ") + versionString() + "\x01"));
|
||||
}
|
||||
|
||||
void test_uname_shows_the_firmware_here_and_sends_nothing() {
|
||||
Fixture f;
|
||||
f.registerNow();
|
||||
f.recv(":roro!u@h JOIN #roro");
|
||||
f.take();
|
||||
f.session->input(f.buffer("#roro"), "/uname", 0);
|
||||
const auto& line = f.session->buffer(f.buffer("#roro")).lines.back();
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(IrcLine::Kind::Info), static_cast<int>(line.kind));
|
||||
TEST_ASSERT_EQUAL_STRING(unameString().c_str(), line.text.c_str());
|
||||
TEST_ASSERT_EQUAL(0, f.take().send.size());
|
||||
}
|
||||
|
||||
void test_topic_is_kept() {
|
||||
@@ -491,6 +504,7 @@ int main() {
|
||||
RUN_TEST(test_private_message_opens_a_query_buffer_and_notifies);
|
||||
RUN_TEST(test_no_notification_while_viewing_that_buffer);
|
||||
RUN_TEST(test_action_and_ctcp_version);
|
||||
RUN_TEST(test_uname_shows_the_firmware_here_and_sends_nothing);
|
||||
RUN_TEST(test_topic_is_kept);
|
||||
RUN_TEST(test_input_message_and_commands);
|
||||
RUN_TEST(test_j_is_short_for_join);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "scp_args.h"
|
||||
|
||||
using roro::term::ScpArgs;
|
||||
using roro::term::parseScp;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_upload() {
|
||||
ScpArgs a;
|
||||
TEST_ASSERT_EQUAL_STRING("", parseScp("/notes/a.txt bob@nas.lan:docs/a.txt", a).c_str());
|
||||
TEST_ASSERT_TRUE(a.upload);
|
||||
TEST_ASSERT_EQUAL_STRING("bob", a.target.user.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("nas.lan", a.target.host.c_str());
|
||||
TEST_ASSERT_EQUAL(22, a.target.port);
|
||||
TEST_ASSERT_EQUAL_STRING("/notes/a.txt", a.local.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("docs/a.txt", a.remote.c_str());
|
||||
}
|
||||
|
||||
void test_download_and_port() {
|
||||
ScpArgs a;
|
||||
TEST_ASSERT_EQUAL_STRING("", parseScp("-P 2222 bob@10.9.0.1:/var/log/syslog /logs/syslog", a).c_str());
|
||||
TEST_ASSERT_FALSE(a.upload);
|
||||
TEST_ASSERT_EQUAL(2222, a.target.port);
|
||||
TEST_ASSERT_EQUAL_STRING("/var/log/syslog", a.remote.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("/logs/syslog", a.local.c_str());
|
||||
}
|
||||
|
||||
void test_download_into_a_folder_takes_the_remote_name() {
|
||||
ScpArgs a;
|
||||
TEST_ASSERT_EQUAL_STRING("", parseScp("bob@h:/etc/motd /notes/", a).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("/notes/motd", a.local.c_str());
|
||||
}
|
||||
|
||||
void test_replace_flag() {
|
||||
ScpArgs a;
|
||||
TEST_ASSERT_EQUAL_STRING("", parseScp("-f -P 22 bob@h:x /a", a).c_str());
|
||||
TEST_ASSERT_TRUE(a.replace);
|
||||
TEST_ASSERT_EQUAL_STRING("", parseScp("bob@h:x /a", a).c_str());
|
||||
TEST_ASSERT_FALSE(a.replace);
|
||||
TEST_ASSERT_TRUE(parseScp("-r bob@h:x /a", a).size() > 0);
|
||||
}
|
||||
|
||||
void test_refusals() {
|
||||
ScpArgs a;
|
||||
TEST_ASSERT_TRUE(parseScp("", a).size() > 0);
|
||||
TEST_ASSERT_TRUE(parseScp("/a.txt /b.txt", a).size() > 0); // neither is a server
|
||||
TEST_ASSERT_TRUE(parseScp("a@h:x b@h:y", a).size() > 0); // both are
|
||||
TEST_ASSERT_TRUE(parseScp("a.txt bob@h:x", a).size() > 0); // not a card path
|
||||
TEST_ASSERT_TRUE(parseScp("/a.txt bob@h:", a).size() > 0); // no remote path
|
||||
TEST_ASSERT_TRUE(parseScp("/notes/ bob@h:x", a).size() > 0); // a folder
|
||||
TEST_ASSERT_TRUE(parseScp("-P 0 /a bob@h:x", a).size() > 0);
|
||||
TEST_ASSERT_TRUE(parseScp("/a bob@bad_host:x", a).size() > 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_upload);
|
||||
RUN_TEST(test_download_and_port);
|
||||
RUN_TEST(test_download_into_a_folder_takes_the_remote_name);
|
||||
RUN_TEST(test_replace_flag);
|
||||
RUN_TEST(test_refusals);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -14,9 +14,16 @@ void test_product_name() {
|
||||
TEST_ASSERT_EQUAL_STRING("roro9stack", roro::kProductName);
|
||||
}
|
||||
|
||||
void test_uname_has_name_version_and_architecture() {
|
||||
std::string expected = std::string("roro9stack ") + roro::versionString() + " " + roro::architectureString();
|
||||
TEST_ASSERT_EQUAL_STRING(expected.c_str(), roro::unameString().c_str());
|
||||
TEST_ASSERT_TRUE(std::strlen(roro::architectureString()) > 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_version_is_never_empty);
|
||||
RUN_TEST(test_product_name);
|
||||
RUN_TEST(test_uname_has_name_version_and_architecture);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user