Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ Point that DNS name at the machine, then run these commands as the owner:

```sh
thirdshift console password --email you@example.com
thirdshift console install > install-console.sh
thirdshift console install --output install-console.sh
# Read the script, then install it:
sudo sh install-console.sh
```

`console install` only prints a script. On Linux it prefers a running Caddy or Apache service; otherwise it checks for `caddy`, `apache2ctl` or `httpd` on PATH. The script writes `thirdshift-console.service`, running the installed binary as you with your HOME and automatic restarts. Only the script needs root. It enables the service, then restarts the proxy.
`console install` only prints a script; `--output <file>` saves it to that file instead, replacing any earlier copy, and writes nothing if the settings can't make a script. On Linux it prefers a running Caddy or Apache service; otherwise it checks for `caddy`, `apache2ctl` or `httpd` on PATH. The script writes `thirdshift-console.service`, running the installed binary as you with your HOME and automatic restarts. Only the script needs root. It enables the service, then restarts the proxy.

For Caddy it writes `/etc/caddy/sites/thirdshift-console.caddy`, imports the sites folder from `/etc/caddy/Caddyfile` if needed, and disables proxy buffering with `flush_interval -1`. Caddy handles HTTPS. It **restarts**, rather than reloads, Caddy to preserve HTTP/3 on Android. For Apache it writes the port-80 site under `/etc/apache2/sites-available` or `/etc/httpd/conf.d`, preserves Host, forwards the request protocol and disables gzip on the Console's event streams. Install certbot with its Apache plugin first; the script runs `certbot --apache -d <name>`. Debian's module/site helpers are included. On an `httpd` machine, enable `proxy`, `proxy_http`, `headers`, `ssl` and `setenvif` and permit proxy connections in the host's SELinux policy first.

Expand Down
15 changes: 12 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ pub enum Command {
Console {
no_sign_in: bool,
},
ConsoleInstall,
ConsoleInstall {
output: Option<String>,
},
ConsolePassword(Option<String>),
ConsoleSignOut {
email: Option<String>,
Expand Down Expand Up @@ -340,7 +342,14 @@ pub fn parse(args: &[String]) -> Result<Command> {
return match &args[1..] {
[] => Ok(Command::Console { no_sign_in: false }),
[flag] if flag == "--no-sign-in" => Ok(Command::Console { no_sign_in: true }),
[action] if action == "install" => Ok(Command::ConsoleInstall),
[action] if action == "install" => Ok(Command::ConsoleInstall { output: None }),
[action, flag, file]
if action == "install" && flag == "--output" && !file.starts_with('-') =>
{
Ok(Command::ConsoleInstall {
output: Some(file.clone()),
})
}
[action] if action == "password" => Ok(Command::ConsolePassword(None)),
[action, flag, email]
if action == "password" && flag == "--email" && !email.starts_with('-') =>
Expand All @@ -352,7 +361,7 @@ pub fn parse(args: &[String]) -> Result<Command> {
Ok(Command::ConsoleLog { json: true })
}
_ => bail!(
"usage: thirdshift console [--no-sign-in] | install | password [--email <address>] | sign-out [--email <address>] [--json] | log [--json]"
"usage: thirdshift console [--no-sign-in] | install [--output <file>] | password [--email <address>] | sign-out [--email <address>] [--json] | log [--json]"
),
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod auth;
mod conversation;
pub use account_commands::{password, print_log, sign_out};
mod install;
pub use install::install;
pub use install::{install, install_to};
mod live;
mod logs;
mod look;
Expand Down
17 changes: 15 additions & 2 deletions src/console/install.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Only this command knows about systemd and the machine's proxy layout.
//! It reads local settings and prints a script; it never installs anything.
//! It reads local settings and prints or saves a script; it never installs anything.
use std::fs;
use std::path::Path;
use std::process::Command;

use anyhow::{Context, Result, ensure};

use crate::args::shell_command;
use crate::config::{self, UserConfig};

enum Proxy {
Expand Down Expand Up @@ -94,7 +96,7 @@ pub fn install() -> Result<String> {
};
Ok(format!(
r#"#!/bin/sh
# Save this script, then run: sudo sh install-console.sh
# Read this script, then run it with sudo sh.
set -eu

cat > /etc/systemd/system/thirdshift-console.service <<'THIRDSHIFT_UNIT'
Expand All @@ -121,6 +123,17 @@ systemctl restart thirdshift-console.service
))
}

/// Save the script for the owner to read; nothing is written when it can't be made.
pub fn install_to(path: &Path) -> Result<String> {
let script = install()?;
fs::write(path, script).with_context(|| format!("can't write {}", path.display()))?;
let run = shell_command(vec!["sudo".into(), "sh".into(), path_text(path)?.into()]);
Ok(format!(
"Wrote {}. Read it, then run: {run}",
path.display()
))
}

fn caddy(host: &str, port: u16) -> String {
format!(
r#"mkdir -p /etc/caddy/sites
Expand Down
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ usage: thirdshift <Issue URL> Run the factory on the iss
thirdshift setup Choose defaults and write the User config
thirdshift status [--all] [--json] List this machine's Commands
thirdshift console [--no-sign-in] Serve this machine's Console on localhost
thirdshift console install Print a service and proxy install script for sudo
thirdshift console install [--output <file>] Print, or save, a service and proxy install script for sudo
thirdshift console password [--email <address>] Set a Console password privately
thirdshift console sign-out [--email <address>] [--json] End an account's sessions
thirdshift console log [--json] Read this machine's Console log
Expand Down Expand Up @@ -145,7 +145,7 @@ fn main() -> ExitCode {
}
Ok(Command::Update) => return outcome(update::update()),
Ok(Command::Setup) => return outcome(setup::setup()),
Ok(Command::ConsoleInstall) => {
Ok(Command::ConsoleInstall { output: None }) => {
return match console::install() {
Ok(script) => {
print!("{script}");
Expand All @@ -154,6 +154,9 @@ fn main() -> ExitCode {
Err(error) => failure(&error),
};
}
Ok(Command::ConsoleInstall { output: Some(path) }) => {
return outcome(console::install_to(std::path::Path::new(&path)));
}
Ok(Command::ConsolePassword(email)) => return outcome(console::password(email.as_deref())),
Ok(Command::ConsoleTheme { name, json }) => {
return printed(console::theme(name.as_deref(), json));
Expand Down
2 changes: 1 addition & 1 deletion src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ fn ask_if(
/// Installation is an owner-run command that prints the script for sudo.
fn offer_console_install(outside: &mut impl Outside, answers: &Answers) {
if answers.console.is_some() {
outside.say("To install the Console, run `thirdshift console install`; it prints a script to run with sudo.".into());
outside.say("To install the Console, run `thirdshift console install --output install-console.sh`, read the script, then run `sudo sh install-console.sh`.".into());
}
}

Expand Down
84 changes: 84 additions & 0 deletions tests/console_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ impl Machine {
}

fn install(&self) -> Output {
self.install_with(&[])
}

fn install_with(&self, extra: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_thirdshift"))
.args(["console", "install"])
.args(extra)
.current_dir(self.root.path())
.env("HOME", self.path("home"))
.env("PATH", self.path("bin"))
.env("USER", "not-the-owner")
Expand Down Expand Up @@ -300,3 +306,81 @@ fn unit_preserves_literal_paths_with_shell_and_systemd_metacharacters() {
);
}
}

#[test]
fn output_saves_the_printed_script_and_names_the_sudo_command() {
let machine = Machine::new();
machine.tool("caddy", "exit 0");
let printed = machine.script();
let output = machine.install_with(&["--output", "install-console.sh"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty());
assert_eq!(
fs::read_to_string(machine.path("install-console.sh")).unwrap(),
printed
);
assert!(
String::from_utf8_lossy(&output.stderr).contains("sudo sh install-console.sh"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
// A second install replaces the saved script rather than refusing.
fs::write(machine.path("install-console.sh"), "stale").unwrap();
assert!(
machine
.install_with(&["--output", "install-console.sh"])
.status
.success()
);
assert_eq!(
fs::read_to_string(machine.path("install-console.sh")).unwrap(),
printed
);
}

#[test]
fn output_saves_nothing_when_the_settings_are_unusable() {
let machine = Machine::new();
machine.tool("caddy", "exit 0");
machine.configure("", 9123);
let output = machine.install_with(&["--output", "install-console.sh"]);
assert!(!output.status.success());
assert!(!machine.path("install-console.sh").exists());
assert!(String::from_utf8_lossy(&output.stderr).contains("console.address"));
}

#[test]
fn output_needs_a_file_name() {
let machine = Machine::new();
for extra in [&["--output"][..], &["--output", "--output"], &["--json"]] {
let output = machine.install_with(extra);
assert!(!output.status.success(), "accepted {extra:?}");
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains("install [--output <file>]"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
}

#[test]
fn output_names_a_copyable_sudo_command_for_a_spaced_path() {
let machine = Machine::new();
machine.tool("caddy", "exit 0");
let output = machine.install_with(&["--output", "my script.sh"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("sudo sh 'my script.sh'"), "{stderr}");
// The saved script must not tell its reader to run some other file.
let saved = fs::read_to_string(machine.path("my script.sh")).unwrap();
assert!(!saved.contains("install-console.sh"), "{saved}");
}
4 changes: 3 additions & 1 deletion tests/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ fn setup_console_questions_use_email_to_keep_comments_and_offer_install() {
assert!(text.contains(comment), "{text}");
}
assert!(
result.stderr.contains("thirdshift console install"),
result
.stderr
.contains("thirdshift console install --output install-console.sh"),
"{}",
result.stderr
);
Expand Down
Loading