Files
shell-rs/src/secrets/temporary.rs
T
XuzandClaude Opus 5.5 d5370f8bac 外部连接:改名并在打开时收起左侧栏
命名(用户定的):命令行链接打开的叫「外部连接」(不保存、只开终端);以后在
ShellRS 里直接输入地址、不保存但功能齐全的叫「临时连接」。代码里共同的「不
保存」统一叫 temporary(HostStore 的 temporary / is_temporary、
SecretRef::Temporary、TemporarySecretStore,和 CLI 的 JSON 字段一致),「只开
终端」改由来源决定:HostStore::is_external,login_of 只对它置 shell_only,右
侧栏也看它。shellrs list 表格写「(外部连接)」。

外部连接打开时左侧栏开着就收起(同 ⌘B),终端占满窗口;链接有误时不收。
命令片段面板本来就列出全部片段,外部连接照常都能看到;以后做适用范围时外部
和临时连接只显示「所有主机」的,规则记在 CLAUDE.md。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:17:03 +08:00

124 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashMap;
use std::sync::Mutex;
use anyhow::Result;
use zeroize::Zeroizing;
use super::{SecretRef, SecretStore, SharedSecretStore};
/// 钥匙串外面包的一层:不保存的连接(外部连接)的密码([`SecretRef::Temporary`])
/// 只记在这层内存里,其余的原样交给里面的钥匙串。
///
/// `HostStore` 把注入的秘密存储包成这样,连接器读到的也是它,所以终端和 SFTP
/// 用得上链接里的密码,而这个密码不会落进钥匙串;没有钥匙串的机器上照样能用。
pub struct TemporarySecretStore {
keychain: SharedSecretStore,
passwords: Mutex<HashMap<u64, Zeroizing<String>>>,
}
impl TemporarySecretStore {
pub fn new(keychain: SharedSecretStore) -> Self {
Self {
keychain,
passwords: Mutex::new(HashMap::new()),
}
}
fn passwords(&self) -> std::sync::MutexGuard<'_, HashMap<u64, Zeroizing<String>>> {
self.passwords
.lock()
.unwrap_or_else(|error| error.into_inner())
}
/// 记下临时主机 `host` 的密码。只碰内存,可以在 UI 线程上调用。
pub fn remember(&self, host: u64, password: Zeroizing<String>) {
self.passwords().insert(host, password);
}
/// 忘掉临时主机 `host` 的密码。只碰内存。
pub fn forget(&self, host: u64) {
self.passwords().remove(&host);
}
}
impl SecretStore for TemporarySecretStore {
fn get(&self, secret: &SecretRef) -> Result<Option<Zeroizing<String>>> {
match secret {
SecretRef::Temporary { host } => Ok(self.passwords().get(host).cloned()),
_ => self.keychain.get(secret),
}
}
fn set(&self, secret: &SecretRef, value: &str) -> Result<()> {
match secret {
SecretRef::Temporary { host } => {
self.remember(*host, Zeroizing::new(value.to_string()));
Ok(())
}
_ => self.keychain.set(secret, value),
}
}
fn delete(&self, secret: &SecretRef) -> Result<()> {
match secret {
SecretRef::Temporary { host } => {
self.forget(*host);
Ok(())
}
_ => self.keychain.delete(secret),
}
}
fn is_available(&self) -> bool {
self.keychain.is_available()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::secrets::{InMemorySecretStore, NoSecretStore};
#[test]
fn link_passwords_stay_in_memory_and_the_rest_reaches_the_keychain() {
let keychain = Arc::new(InMemorySecretStore::default());
let store = TemporarySecretStore::new(keychain.clone());
let link = SecretRef::temporary(7);
store.remember(7, Zeroizing::new("token".into()));
assert_eq!(
store.get(&link).unwrap().as_deref().map(String::as_str),
Some("token")
);
assert!(keychain.is_empty());
let saved = SecretRef::password("root", "10.0.1.12", 22);
store.set(&saved, "hunter2").unwrap();
assert_eq!(keychain.len(), 1);
assert_eq!(
store.get(&saved).unwrap().as_deref().map(String::as_str),
Some("hunter2")
);
// Another host's password is not the link's.
assert!(store.get(&SecretRef::temporary(8)).unwrap().is_none());
store.forget(7);
assert!(store.get(&link).unwrap().is_none());
assert_eq!(keychain.len(), 1);
}
#[test]
fn without_a_keychain_link_passwords_still_work() {
let store = TemporarySecretStore::new(Arc::new(NoSecretStore));
assert!(!store.is_available());
store.set(&SecretRef::temporary(1), "token").unwrap();
assert!(store.get(&SecretRef::temporary(1)).unwrap().is_some());
assert!(
store
.set(&SecretRef::password("root", "h", 22), "x")
.is_err()
);
}
}