Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/release-assets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,92 @@ permissions:
contents: read

jobs:
daemon:
# A binary per platform, so installing the daemon is a download rather than a Rust
# toolchain and a C compiler. The extension is on the store now, which means people arrive
# without either.
#
# Unsigned, deliberately. Gatekeeper and SmartScreen act on a flag the *downloading
# application* attaches — `com.apple.quarantine`, Mark-of-the-Web — and `curl` and `scp` do
# not attach one. A command-line tool installed the way command-line tools are installed
# meets neither. Signing would cost a few hundred a year to remove a warning on the one
# route the README does not recommend.
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
# Every platform that can build should, even when one cannot. A macOS runner out of
# capacity is not a reason to leave Linux users with nothing.
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
os: windows-latest
exe: .exe
- target: aarch64-apple-darwin
os: macos-latest
exe: ""
- target: x86_64-apple-darwin
os: macos-latest
exe: ""
# Built on the older runner on purpose: a binary linked against a newer glibc will
# not start on an older one, and the people this is for are on login nodes somebody
# else administers. `musl` would remove the floor entirely and is not here yet,
# because `ring` wants a C toolchain targeting musl and an untested build inside a
# release job fails at the worst possible moment.
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
exe: ""
env:
TAG: ${{ inputs.tag }}
TARGET: ${{ matrix.target }}
EXE: ${{ matrix.exe }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.tag }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
toolchain: stable
targets: ${{ matrix.target }}

# `--locked` so the binary somebody downloads was built from the dependency versions in
# the tag rather than from whatever resolved on the day.
- name: build
run: cargo build --release --locked --bin ssh-browser --target ${{ matrix.target }}

# Named for the target rather than the runner: that is the string a reader matches
# against their own machine, and it is what `cargo-binstall` looks for.
- name: name it after the platform
shell: bash
run: |
# No version in the name, and that is what makes
# `releases/latest/download/ssh-browser-<target>` work -- the URL that never goes
# stale cannot interpolate one. Named with the version, it 404s, which is how this
# was found: by running the command the README gives rather than reading it.
#
# The version is still in the path of a pinned download,
# `releases/download/v0.5.1/...`, which is where it belongs.
out="ssh-browser-$TARGET$EXE"
cp "target/$TARGET/release/ssh-browser$EXE" "$out"
# Published so somebody can check what they downloaded against what this built,
# which is what an unsigned binary can offer instead of a signature.
#
# `sha256sum`, not `shasum`: the Git Bash on the Windows runner has the first and
# not the second, and this job was dispatched against an existing tag precisely so
# that finding out cost a failed test run rather than a failed release.
sha256sum "$out" > "$out.sha256"
cat "$out.sha256"
echo "ASSET=$out" >> "$GITHUB_ENV"

- name: upload
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$TAG" "$ASSET" "$ASSET.sha256" --clobber

extension:
name: attach the extension
runs-on: ubuntu-latest
Expand Down
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,40 @@ that is most of them, and all of what `file://` breaks.

## Usage

Every release carries a binary per platform. One file, nothing to unpack:

```
curl -L https://github.com/QAtlasHub/ssh-browser/releases/latest/download/ssh-browser-x86_64-unknown-linux-gnu -o ssh-browser
chmod +x ssh-browser
./ssh-browser serve docs=myhost:/srv/docs cluster=login-node
```

Swap the target for `aarch64-apple-darwin`, `x86_64-apple-darwin` or
`x86_64-pc-windows-msvc.exe`. Each has a `.sha256` beside it.

**`curl`, not your browser — and that is why it is the first thing on this page.** These
binaries are unsigned, and macOS and Windows both refuse an unsigned executable: but only one
the *browser* downloaded. Gatekeeper reads a `com.apple.quarantine` flag and SmartScreen a
Mark-of-the-Web, and both are attached by the downloading application. `curl` and `scp` attach
neither. A signature would cost a few hundred a year to remove a warning on the one route this
page does not recommend.

With cargo, the same binary without the URL:

```
cargo binstall ssh-browser
```

Or build it, which is also what to do for a platform not in that list:

```
cargo install ssh-browser
ssh-browser serve docs=myhost:/srv/docs cluster=login-node
```

**What that needs on your machine:** a Rust toolchain and a C compiler. The C compiler is for
`ring`, which the TLS stack compiles — `cc` on Linux and macOS, the MSVC build tools or a MinGW
toolchain on Windows. Nothing else: no OpenSSL, no system TLS library, no daemon, no service.
Everything TLS is inside the binary, and `deny.toml` bans the OpenSSL crates so it stays that way.
**That last one needs a Rust toolchain and a C compiler.** The C compiler is for `ring`, which
the TLS stack compiles — `cc` on Linux and macOS, the MSVC build tools or a MinGW toolchain on
Windows. Nothing else: no OpenSSL, no system TLS library, no daemon, no service. Everything TLS
is inside the binary, and `deny.toml` bans the OpenSSL crates so it stays that way.

At runtime it needs `ssh` on your `PATH` and nothing more. Trusting the https certificate uses a
command your OS already has — `certutil`, `security`, or `update-ca-certificates` — and
Expand Down
12 changes: 12 additions & 0 deletions crates/ssh-browser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ keywords = ["ssh", "sftp", "browser", "proxy", "origin"]
# from a shell would be neither.
categories = ["network-programming", "command-line-utilities"]

# Where the built binaries are, so `cargo binstall ssh-browser` takes one instead of
# compiling. Nothing but `cargo-binstall` reads this table; cargo ignores `package.metadata`.
#
# Spelled out rather than left to its defaults, because it tries several shapes and a
# near-miss falls back to building from source — which is the C compiler this exists to avoid,
# arrived at silently. The names come from `release-assets.yml`; if one moves, both move.
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/ssh-browser-{ target }{ binary-ext }"
# Not an archive. One file is the whole program, and unpacking a tarball that contains a
# single binary is a step which exists only because most projects have more than one.
pkg-fmt = "bin"

[lints]
workspace = true

Expand Down
13 changes: 11 additions & 2 deletions e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -978,8 +978,17 @@ async function main() {
}));
check("with no daemon, the first run says what this is and what to run", () => {
assert.match(firstRun.view, /daemon is not running/);
assert.match(firstRun.cmd, /cargo install ssh-browser/);
assert.match(firstRun.cmd, /ssh-browser serve/);
// A download rather than a build. Somebody arriving from the store has neither a Rust
// toolchain nor a C compiler, and `cargo install` was a wall dressed as an instruction.
assert.match(firstRun.cmd, /curl -L -o ssh-browser/);
assert.match(firstRun.cmd, /releases\/latest\/download\/ssh-browser-/);
// `.exe` on Windows, none elsewhere, so the check is about the instruction rather
// than the filename. This runs on whichever platform is running it -- souta's
// Windows and CI's Linux take different branches of the same function.
assert.match(firstRun.cmd, /ssh-browser(\.exe)? serve/);
// Not a link to the releases page: a browser download is the one thing that attaches
// the quarantine flag these unsigned binaries would then be refused for.
assert.doesNotMatch(firstRun.cmd, /^https/m);
assert.ok(firstRun.repo.includes("QAtlasHub/ssh-browser"), `no source link: ${firstRun.repo}`);
});
// Two versions of the same bad news, one of them in red, reads as two problems.
Expand Down
42 changes: 40 additions & 2 deletions extension/src/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,38 @@ function renderList(): void {
/// extension for the store, who will install it with no daemon anywhere. One red line
/// naming a command they have never heard of is not enough to act on, and "does not
/// function" is a fair reading of it.
/// What to run to get the daemon, for the platform reading this.
///
/// `curl`, not a link to the releases page, and the difference is not convenience. These
/// binaries are unsigned; macOS and Windows refuse an unsigned executable, but only one the
/// *browser* downloaded — Gatekeeper reads a `com.apple.quarantine` flag and SmartScreen a
/// Mark-of-the-Web, and both are attached by whatever did the downloading. `curl` attaches
/// neither. Putting a download link here would walk somebody into the one warning the whole
/// arrangement avoids.
///
/// The platform is a guess and says so by naming the file: somebody on an Intel Mac can see
/// `aarch64` and change it. Guessing wrong is a wrong filename, which is visible; asking would
/// be a question before the first useful thing has happened.
function installCommand(): string {
const url = (target: string) =>
`https://github.com/QAtlasHub/ssh-browser/releases/latest/download/ssh-browser-${target}`;
const agent = navigator.userAgent;
if (agent.includes("Windows")) {
return [
`curl -L -o ssh-browser.exe ${url("x86_64-pc-windows-msvc.exe")}`,
".\\ssh-browser.exe serve",
].join("\n");
}
// Apple Silicon by default, because it is most Macs now and the alternative is named in the
// line above it either way.
const target = agent.includes("Mac OS X")
? "aarch64-apple-darwin"
: "x86_64-unknown-linux-gnu";
return [`curl -L -o ssh-browser ${url(target)}`, "chmod +x ssh-browser", "./ssh-browser serve"].join(
"\n",
);
}

function renderOffline(firstRun: boolean): void {
const view = clear();
if (!firstRun) {
Expand All @@ -318,9 +350,15 @@ function renderOffline(firstRun: boolean): void {
"half; the daemon runs on your own machine and does the SSH.",
),
);
view.append(node("pre", "cmd", "cargo install ssh-browser\nssh-browser serve"));
view.append(node("pre", "cmd", installCommand()));
view.append(
node("p", "note", "Then reload this page. It talks to 127.0.0.1 and to nothing else."),
node(
"p",
"note",
"Then reload this page. It talks to 127.0.0.1 and to nothing else. With cargo, " +
"cargo binstall ssh-browser fetches the same binary; cargo install ssh-browser " +
"builds it, which needs a C compiler.",
),
);

const repo = document.createElement("a");
Expand Down