又一个Rust练手项目-wssh(SSH over Websocket Client)

打印 上一主题 下一主题

主题 209|帖子 209|积分 627

原文地址https://blog.fanscore.cn/a/61/
1. wssh

1.1 开发背景

公司内部的发布系统提供一个连接到k8s pod的web终端,可以在网页中连接到k8s pod内。实现原理大概为通过websocket协议署理了k8s pod ssh,然后在前端通过xterm.js+websocket实现了web终端的结果。
但是每次必要进pod内调试点东西都必要打开浏览器进到发布系统里一通点点点才能进入,而发布系统页面加载的又非常慢,所以效率非常低。
因此使用Rust实现了一个命令行工具,可以在本机终端中通过命令连接到k8s pod,实现了类似于ssh client的结果。这样一来不仅简化了我登陆pod的过程,又熟悉了Rust,还输出了篇博客。
项目地址:github.com/Orlion/wssh
1.2 结果


  • 通过-e test指定为测试情况,实行后会先调用发布系统的应用列表api查询出所有应用,然后在输出中列出所有应用供用户选择

  • 选择应用后通过连接到websocket server,websocket server转发到与pod的ssh连接,实现“SSH”到应用的pod的结果

2. 原理

公司发布系统的现状:

起首我们的发布系统提供了一个Websocket Server,这个server实际署理了到k8s pod ssh连接。然后在前端通过xterm.js模拟了一个终端,通过websocket连接到server。
wssh替换了前端:

3. 实现细节

3.1 命令行参数解析

wssh命令行参数解析使用了clap这个库
  1. let clap_command = clap::Command::new("wssh")
  2.     .version("0.1.0") // 指定版本号
  3.     .author("Orlion") // 作者
  4.     .about("SSH over Websocket 客户端")
  5.     .arg(  // 添加命令行参数
  6.         clap::Arg::new("env")
  7.             .long("env")
  8.             .short('e')
  9.             .help("环境 test/preview")
  10.             .value_name("ENV")
  11.             .required(true),
  12.     );
  13. let matches = clap_command.get_matches();
  14. // 获取--env参数值
  15. let env = matches.get_one::<String>("env").expect("请输入--env参数");
复制代码
3.2 发布系统登录

如1.1节所述,wssh会调用发布系统的api,发布系统必要先登录才能调用,但是调用登录api比较麻烦,还必要用户输入账号密码,因此wssh使用了github.com/thewh1teagle/rookie 库直接读取发布系统域名下的cookie,免去了输入账号密码的麻烦,非常的简朴。
  1. let domains = vec!["jumpserver.domain.com".into()];
  2. let cookies = rookie::chrome(Some(domains)).map_err(|e| { // 使用rookie从chrome获取jumpserver的cookie
  3.     error::from_string(format!("获取jumpserver cookie失败: {}", e.to_string()))
  4. })?;
  5. let mut cookie_map: HashMap<String, Cookie> = HashMap::new();
  6. for cookie in cookies {
  7.     if cookie.name == "sessionid" || cookie.name == "JUMPSERVER_SESS_ID" {
  8.         cookie_map.insert(cookie.name.clone(), cookie);
  9.     }
  10. }
  11. let cookies = cookie_map
  12.     .values()
  13.     .map(|cookie| format!("{}={}", cookie.name, cookie.value))
  14.     .collect::<Vec<String>>()
  15.     .join("; ");
  16. }
复制代码
3.3 命令行中输出应用列表

在命令行中输出列表供用户选择假如手动输出的话出来的结果是比较差的,因此找到了dialoguer这个库,这个库提供了一个模糊搜索的组件FuzzySelect
  1. let app_index =
  2.     dialoguer::FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default())
  3.         .with_prompt("请选择应用") // 提示信息
  4.         .item("0. 退出") // 为用户提供退出的选项
  5.         .items(&app_selections) // 输出应用列表
  6.         .default(0) // 默认选择退出
  7.         .interact()
  8.         .map_err(|e| error::from_string(format!("选择应用失败: {}", e.to_string())))?;
复制代码
3.4 通过websocket登陆到pod

起首使用tokio_tungstenite库创建websocket连接。
  1. let uri = format!(
  2.     "wss://jumpserver.domain.com/ssh?ssh_token={}",
  3.     urlencoding::encode(ssh_token),
  4. );
  5. let (socket, response) = tokio_tungstenite::connect_async(uri)
  6.     .await
  7.     .map_err(|e| error::from_string(format!("websocket连接失败: {}", e.to_string())))?;
复制代码
开发这部分连接功能时踩了个“坑”,缘故原由是刚开始开发时对Rust的异步特性不熟悉,所以想使用同步多线程的方案,所以开始使用了tungstenite::connect()创建了同步连接,后来在举行两个线程并行读写时遇到了问题,缘故原由是connect返回的对象的read()方法和write()方法接收的是&mut self,因为Rust不允许同时存在两个可变引用,所以并发读写是不大概的。
所以后来换成了tokio_tungstenite::connect_async()函数,这个函数返回的对象提供了split()方法可以将一个连接切分成一个读句柄和一个写句柄,这样就可以并行读写了。
别的查阅文档的过程中也得知了TCP连接可拆分而TLS连接是不可拆分的,所以假如你的websocket server可以通过ws而没有强制wss的话可以使用rs-websocket这个古老的库,这个库的同步连接方法返回的TCP连接是可以拆分的。
3.5 尺度输出的调整

要在本地输出长途ssh server输出的内容之前还必要做以下三个调整。

  • 发送window-change请求
    本地终端窗口大小初始化和发生变更时都必要同步ssh server的,以便获得一致的显示结果,假如不发送大概会导致显示内容被截断或者格式不正确,并且vim等命令依赖于准确的终端尺寸来显示界面。
  • 将尺度输出设置为raw模式。在raw模式下,尺度输出表现为

    • 没有行缓存,会逐字节输出
    • 不会回显输入,必须由步伐写入
    • 输出未规范化(例如,\n 表示“向下一行”,而不是“换行符”)

  1. let mut stdout = std::io::stdout().into_raw_mode()
复制代码
4. 总结


通过这个项目又加深了对Rust的理解,过程中还首次用到了反人类的生命周期标注
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

星球的眼睛

高级会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表