Skip to content

Commit

Permalink
Add --name option, default user@hostname (#91)
Browse files Browse the repository at this point in the history
* Add --name option, default user@hostname

This appears in the title of the browser tab. If not specified, it will default to detecting the user and hostname of your machine using OS APIs.

This change is backwards-compatible and forwards-compatible between old clients and old server versions.

Resolves #90 and #85.

* Remove console.log()

* Fix clippy issue
  • Loading branch information
ekzhang authored May 5, 2024
1 parent ef30dde commit c1b3556
Show file tree
Hide file tree
Showing 19 changed files with 104 additions and 29 deletions.
36 changes: 31 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.2.3"
version = "0.2.4"
authors = ["Eric Zhang <[email protected]>"]
license = "MIT"
description = "A secure web-based, collaborative terminal."
Expand All @@ -17,7 +17,7 @@ clap = { version = "4.4.2", features = ["derive", "env"] }
prost = "0.12.0"
rand = "0.8.5"
serde = { version = "1.0.188", features = ["derive", "rc"] }
sshx-core = { version = "0.2.3", path = "crates/sshx-core" }
sshx-core = { version = "0.2.4", path = "crates/sshx-core" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-stream = { version = "0.1.14", features = ["sync"] }
tonic = { version = "0.10.0", features = ["tls", "tls-webpki-roots"] }
Expand Down
2 changes: 2 additions & 0 deletions crates/sshx-core/proto/sshx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ message TerminalSize {
message OpenRequest {
string origin = 1; // Web origin of the server.
bytes encrypted_zeros = 2; // Encrypted zero block, for client verification.
string name = 3; // Name of the session (user@hostname).
}

// Details of a newly-created sshx session.
Expand Down Expand Up @@ -101,6 +102,7 @@ message SerializedSession {
map<uint32, SerializedShell> shells = 2;
uint32 next_sid = 3;
uint32 next_uid = 4;
string name = 5;
}

message SerializedShell {
Expand Down
1 change: 1 addition & 0 deletions crates/sshx-server/src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ impl SshxService for GrpcServer {
None => {
let metadata = Metadata {
encrypted_zeros: request.encrypted_zeros,
name: request.name,
};
self.0.insert(&name, Arc::new(Session::new(metadata)));
}
Expand Down
3 changes: 3 additions & 0 deletions crates/sshx-server/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const SHELL_STORED_BYTES: u64 = 1 << 21; // 2 MiB
pub struct Metadata {
/// Used to validate that clients have the correct encryption key.
pub encrypted_zeros: Bytes,

/// Name of the session (human-readable).
pub name: String,
}

/// In-memory state for a single sshx session.
Expand Down
2 changes: 2 additions & 0 deletions crates/sshx-server/src/session/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl Session {
.collect(),
next_sid: ids.0 .0,
next_uid: ids.1 .0,
name: self.metadata().name.clone(),
};
let data = message.encode_to_vec();
ensure!(data.len() < MAX_SNAPSHOT_SIZE, "snapshot too large");
Expand All @@ -73,6 +74,7 @@ impl Session {
let message = SerializedSession::decode(&*data)?;
let metadata = Metadata {
encrypted_zeros: message.encrypted_zeros,
name: message.name,
};

let session = Self::new(metadata);
Expand Down
2 changes: 1 addition & 1 deletion crates/sshx-server/src/web/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct WsUser {
#[serde(rename_all = "camelCase")]
pub enum WsServer {
/// Initial server message, with the user's ID and session metadata.
Hello(Uid),
Hello(Uid, String),
/// The user's authentication was invalid.
InvalidAuth(),
/// A snapshot of all current users in the session.
Expand Down
5 changes: 3 additions & 2 deletions crates/sshx-server/src/web/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ async fn handle_socket(socket: &mut WebSocket, session: Arc<Session>) -> Result<
})
}

let metadata = session.metadata();
let user_id = session.counter().next_uid();
session.sync_now();
send(socket, WsServer::Hello(user_id)).await?;
send(socket, WsServer::Hello(user_id, metadata.name.clone())).await?;

match recv(socket).await? {
Some(WsClient::Authenticate(bytes)) if bytes == session.metadata().encrypted_zeros => {}
Some(WsClient::Authenticate(bytes)) if bytes == metadata.encrypted_zeros => {}
_ => {
send(socket, WsServer::InvalidAuth()).await?;
return Ok(());
Expand Down
2 changes: 1 addition & 1 deletion crates/sshx-server/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ impl ClientSocket {
let flush_task = async {
while let Some(msg) = self.recv().await {
match msg {
WsServer::Hello(user_id) => self.user_id = user_id,
WsServer::Hello(user_id, _) => self.user_id = user_id,
WsServer::InvalidAuth() => panic!("invalid authentication"),
WsServer::Users(users) => self.users = BTreeMap::from_iter(users),
WsServer::UserDiff(id, maybe_user) => {
Expand Down
1 change: 1 addition & 0 deletions crates/sshx-server/tests/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ async fn test_rpc() -> Result<()> {
let req = OpenRequest {
origin: "sshx.io".into(),
encrypted_zeros: Encrypt::new("").zeros().into(),
name: String::new(),
};
let resp = client.open(req).await?;
assert!(!resp.into_inner().name.is_empty());
Expand Down
2 changes: 1 addition & 1 deletion crates/sshx-server/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod common;
async fn test_basic_restore() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand Down
14 changes: 7 additions & 7 deletions crates/sshx-server/tests/with_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub mod common;
#[tokio::test]
async fn test_handshake() -> Result<()> {
let server = TestServer::new().await;
let controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
controller.close().await?;
Ok(())
}
Expand All @@ -23,7 +23,7 @@ async fn test_handshake() -> Result<()> {
async fn test_command() -> Result<()> {
let server = TestServer::new().await;
let runner = Runner::Shell("/bin/bash".into());
let mut controller = Controller::new(&server.endpoint(), runner).await?;
let mut controller = Controller::new(&server.endpoint(), "", runner).await?;

let session = server
.state()
Expand Down Expand Up @@ -69,7 +69,7 @@ async fn test_ws_missing() -> Result<()> {
async fn test_ws_basic() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand Down Expand Up @@ -101,7 +101,7 @@ async fn test_ws_basic() -> Result<()> {
async fn test_ws_resize() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand Down Expand Up @@ -145,7 +145,7 @@ async fn test_ws_resize() -> Result<()> {
async fn test_users_join() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand Down Expand Up @@ -174,7 +174,7 @@ async fn test_users_join() -> Result<()> {
async fn test_users_metadata() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand All @@ -199,7 +199,7 @@ async fn test_users_metadata() -> Result<()> {
async fn test_chat_messages() -> Result<()> {
let server = TestServer::new().await;

let mut controller = Controller::new(&server.endpoint(), Runner::Echo).await?;
let mut controller = Controller::new(&server.endpoint(), "", Runner::Echo).await?;
let name = controller.name().to_owned();
let key = controller.encryption_key().to_owned();
tokio::spawn(async move { controller.run().await });
Expand Down
1 change: 1 addition & 0 deletions crates/sshx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ tokio-stream.workspace = true
tonic.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
whoami = { version = "1.5.1", default-features = false }
10 changes: 6 additions & 4 deletions crates/sshx/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,21 @@ pub struct Controller {

impl Controller {
/// Construct a new controller, connecting to the remote server.
pub async fn new(origin: &str, runner: Runner) -> Result<Self> {
pub async fn new(origin: &str, name: &str, runner: Runner) -> Result<Self> {
debug!(%origin, "connecting to server");
let encryption_key = rand_alphanumeric(14); // 83.3 bits of entropy

let encryption_key2 = encryption_key.clone();
let kdf_task = task::spawn_blocking(move || Encrypt::new(&encryption_key2));

let kdf_task = {
let encryption_key = encryption_key.clone();
task::spawn_blocking(move || Encrypt::new(&encryption_key))
};
let mut client = Self::connect(origin).await?;
let encrypt = kdf_task.await?;

let req = OpenRequest {
origin: origin.into(),
encrypted_zeros: encrypt.zeros().into(),
name: name.into(),
};
let mut resp = client.open(req).await?.into_inner();
resp.url = resp.url + "#" + &encryption_key;
Expand Down
17 changes: 16 additions & 1 deletion crates/sshx/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ struct Args {
/// Quiet mode, only prints the URL to stdout.
#[clap(short, long)]
quiet: bool,

/// Session name displayed in the title (defaults to user@hostname).
#[clap(long)]
name: Option<String>,
}

fn print_greeting(shell: &str, controller: &Controller) {
Expand Down Expand Up @@ -52,8 +56,19 @@ async fn start(args: Args) -> Result<()> {
None => get_default_shell().await,
};

let name = args.name.unwrap_or_else(|| {
let mut name = whoami::username();
if let Ok(host) = whoami::fallible::hostname() {
// Trim domain information like .lan or .local
let host = host.split('.').next().unwrap_or(&host);
name += "@";
name += host;
}
name
});

let runner = Runner::Shell(shell.clone());
let mut controller = Controller::new(&args.server, runner).await?;
let mut controller = Controller::new(&args.server, &name, runner).await?;
if args.quiet {
println!("{}", controller.url());
} else {
Expand Down
14 changes: 12 additions & 2 deletions src/lib/Session.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<script lang="ts">
import { onDestroy, onMount, tick, beforeUpdate, afterUpdate } from "svelte";
import {
onDestroy,
onMount,
tick,
beforeUpdate,
afterUpdate,
createEventDispatcher,
} from "svelte";
import { fade } from "svelte/transition";
import { debounce, throttle } from "lodash-es";
Expand All @@ -24,6 +31,8 @@
export let id: string;
const dispatch = createEventDispatcher<{ receiveName: string }>();
// The magic numbers "left" and "top" are used to approximately center the
// terminal at the time that it is first created.
//
Expand Down Expand Up @@ -128,7 +137,8 @@
srocket = new Srocket<WsServer, WsClient>(`/api/s/${id}`, {
onMessage(message) {
if (message.hello) {
userId = message.hello;
userId = message.hello[0];
dispatch("receiveName", message.hello[1]);
makeToast({
kind: "success",
message: `Connected to the server.`,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type WsUser = {

/** Server message type, see the Rust version. */
export type WsServer = {
hello?: Uid;
hello?: [Uid, string];
invalidAuth?: [];
users?: [Uid, WsUser][];
userDiff?: [Uid, WsUser | null];
Expand Down
Loading

0 comments on commit c1b3556

Please sign in to comment.