Files
mrlee 302248a907
CI / verify (push) Canceled after 0s
chore(release): 发布 v2.0.0
- 内置 SSH 终端(/terminal):复用已保存的 SSH 密码/私钥与独立 known_hosts 校验
- 终端二次认证、会话审计(/terminal/audits)、录像审计与常用命令
- DDNS 服务商名称与自定义 IP,新增迁移 037/038/039
- CHANGELOG 与 VERSION 更新至 2.0.0
2026-10-16 20:18:04 +08:00

73 lines
2.0 KiB
Go

package terminal
import (
"fmt"
"io"
"sync"
"golang.org/x/crypto/ssh"
)
// sshTerminal 把 x/crypto/ssh 的交互式会话包装成 Terminal。
// 申请了 PTY 之后,远端 shell 的 stdout/stderr 会合并到同一条输出流,无需分别读取。
type sshTerminal struct {
client *ssh.Client
session *ssh.Session
stdin io.WriteCloser
stdout io.Reader
closeOnce sync.Once
closeErr error
}
// NewSSHTerminal 在已认证的 SSH 连接上申请 PTY 并启动登录 shell。
func NewSSHTerminal(client *ssh.Client, cols, rows int) (Terminal, error) {
session, err := client.NewSession()
if err != nil {
return nil, fmt.Errorf("创建 SSH 会话失败: %w", err)
}
modes := ssh.TerminalModes{ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400}
if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
_ = session.Close()
return nil, fmt.Errorf("申请 PTY 失败: %w", err)
}
stdin, err := session.StdinPipe()
if err != nil {
_ = session.Close()
return nil, fmt.Errorf("打开 SSH 输入流失败: %w", err)
}
stdout, err := session.StdoutPipe()
if err != nil {
_ = session.Close()
return nil, fmt.Errorf("打开 SSH 输出流失败: %w", err)
}
if err := session.Shell(); err != nil {
_ = session.Close()
return nil, fmt.Errorf("启动远端 shell 失败: %w", err)
}
return &sshTerminal{client: client, session: session, stdin: stdin, stdout: stdout}, nil
}
// Read 读取远端输出。
func (t *sshTerminal) Read(p []byte) (int, error) { return t.stdout.Read(p) }
// Write 把按键写入远端 PTY。
func (t *sshTerminal) Write(p []byte) (int, error) { return t.stdin.Write(p) }
// Resize 通知远端窗口尺寸变化。
func (t *sshTerminal) Resize(cols, rows int) error {
if cols <= 0 || rows <= 0 {
return nil
}
return t.session.WindowChange(rows, cols)
}
// Close 关闭 SSH 会话与底层连接;重复调用安全。
func (t *sshTerminal) Close() error {
t.closeOnce.Do(func() {
_ = t.session.Close()
t.closeErr = t.client.Close()
})
return t.closeErr
}