4 Commits
6 changed files with 131 additions and 63 deletions
+5 -2
View File
@@ -31,7 +31,10 @@ pass, err := remote.GetVncPassword()
cfg := remote.VncServerDefaultConfig(true /* 随机密码 */) // VncServerConfig cfg := remote.VncServerDefaultConfig(true /* 随机密码 */) // VncServerConfig
err = remote.WriteVncServerConfig(cfg) err = remote.WriteVncServerConfig(cfg)
cfg, err = remote.ReadVncServerConfig() cfg, err = remote.ReadVncServerConfig()
_, msg, err := remote.InstallVncServer() // 安装服务(需管理员权限) // 绿色安装最新版 TightVNC:校验 exe → 写注册表全集(随机密码, 需管理员权限) →
// 注册系统服务(装而不启, 远程时再 VncStart; 已运行则重启加载新配置)。
// 返回生成的明文密码供调用方上报/展示。
pass, err := remote.InstallVncServer(`D:\app\tvnc\tvnserver.exe`)
err = remote.VncInit() // 初始化/修复配置 err = remote.VncInit() // 初始化/修复配置
err = remote.VncStart() // VncStop / VncRestart err = remote.VncStart() // VncStop / VncRestart
err = remote.VncInstall() // VncUninstall err = remote.VncInstall() // VncUninstall
@@ -55,6 +58,6 @@ err = remote.EnableSoftwareSASGeneration()
- `GetServiceStatus` 系列对未安装服务返回 -1 而非错误;`GetRdpServiceStatus` / - `GetServiceStatus` 系列对未安装服务返回 -1 而非错误;`GetRdpServiceStatus` /
`GetVncServiceStatus` 走 `kardianos/service`,未安装返回 error。 `GetVncServiceStatus` 走 `kardianos/service`,未安装返回 error。
- `InstallVncServer` / `VncInit` / CAD 开关需要管理员权限,失败信息见返回 msg 或 error。 - `InstallVncServer` / `VncInit` / CAD 开关需要管理员权限,失败信息见返回 error;安装配置对齐最新版 TightVNC(2.8.x) 注册表 37 项全集。
- VNC 密码格式与 TightVNC 兼容(`KarpelesLab/vncpasswd` 实现),加解密为确定性 - VNC 密码格式与 TightVNC 兼容(`KarpelesLab/vncpasswd` 实现),加解密为确定性
变换,注意密文落盘位置的访问控制。 变换,注意密文落盘位置的访问控制。
+65 -1
View File
@@ -2,7 +2,10 @@
package remote package remote
import "testing" import (
"reflect"
"testing"
)
func TestGetServiceStatusNonExist(t *testing.T) { func TestGetServiceStatusNonExist(t *testing.T) {
// 不存在的服务应返回错误(Radmin 服务普通机器上不会安装) // 不存在的服务应返回错误(Radmin 服务普通机器上不会安装)
@@ -49,3 +52,64 @@ func TestVncServerDefaultConfig(t *testing.T) {
t.Error("HttpPort 不应为 0") t.Error("HttpPort 不应为 0")
} }
} }
// TestVncClientDefaultConfigLatest 锁定对最新版 TightVNC(2.8.x) 注册表全集的支持:
// 值与本机最新版安装器落盘值逐项对齐(AcceptHttpConnections/RunControlInterface
// 为安全策略有意取 0:不开 5800 Web 通道、不显托盘)。
func TestVncClientDefaultConfigLatest(t *testing.T) {
cfg := VncClientDefaultConfig(false)
if cfg.RfbPort != 5900 || cfg.HttpPort != 5800 {
t.Errorf("端口 = %d/%d, want 5900/5800", cfg.RfbPort, cfg.HttpPort)
}
// 最新版捕获能力:镜像驱动 + D3D + 透明窗口 + 视频区域识别
if cfg.UseMirrorDriver != 1 || cfg.UseD3D != 1 {
t.Errorf("捕获开关 = mirror:%d d3d:%d, want 1/1", cfg.UseMirrorDriver, cfg.UseD3D)
}
if cfg.GrabTransparentWindows != 1 || cfg.VideoRecognitionInterval != 3000 {
t.Errorf("透明窗口/视频识别 = %d/%d, want 1/3000", cfg.GrabTransparentWindows, cfg.VideoRecognitionInterval)
}
// 安全策略:不开 HTTP 通道、不出托盘控制台、不接 RDP 会话
if cfg.AcceptHttpConnections != 0 || cfg.RunControlInterface != 0 || cfg.ConnectToRdp != 0 {
t.Errorf("安全策略 = http:%d tray:%d rdp:%d, want 0/0/0", cfg.AcceptHttpConnections, cfg.RunControlInterface, cfg.ConnectToRdp)
}
// 连接行为:VNC 认证 + 接受 RFB + 本机输入优先 + 断开踢旧连接
if cfg.AcceptRfbConnections != 1 || cfg.UseVncAuthentication != 1 || cfg.LocalInputPriority != 1 || cfg.DisconnectClients != 1 {
t.Errorf("连接行为异常: %+v", cfg)
}
if cfg.QueryTimeout != 30 || cfg.LocalInputPriorityTimeout != 3 || cfg.PollingInterval != 1000 || cfg.AllowLoopback != 1 {
t.Errorf("超时/轮询/环回异常: %+v", cfg)
}
if len(cfg.Password) == 0 {
t.Error("固定密码密文不应为空")
}
if dec := VncPasswordDecrypt(cfg.Password); dec != "tvncpass" {
t.Errorf("固定密码回解 = %q, want tvncpass", dec)
}
}
// TestInstallVncServerMissingExe 安装入口对 tvnserver.exe 缺失必须报错(不触注册表/服务)
func TestInstallVncServerMissingExe(t *testing.T) {
_, err := InstallVncServer(`Z:\no\such\dir\tvnserver.exe`)
if err == nil {
t.Fatal("exe 不存在时应返回错误")
}
}
// TestVncServerConfigFieldCount 锁定配置字段总数:必须与本机最新版 TightVNC(2.8.x)
// 注册表 SOFTWARE\TightVNC\Server 下的 37 个值一一对应。加/删字段前先核对注册表全集,
// 并同步检查 ReadVncServerConfig / WriteVncServerConfig 的逐项覆盖(此前曾漏 LogLevel 写、
// ConnectToRdp 误读、LogLevel 漏读, 靠本测试与脚本对账拦截)。
func TestVncServerConfigFieldCount(t *testing.T) {
if got := reflect.TypeOf(VncServerConfig{}).NumField(); got != 37 {
t.Errorf("VncServerConfig 字段数 = %d, want 37(最新版注册表全集)", got)
}
}
// TestVncPasswordDecryptShortInput 短于 8 字节的密文(缺失/截断)必须返回空串而非 panic
func TestVncPasswordDecryptShortInput(t *testing.T) {
for _, data := range [][]byte{nil, {}, []byte("abc"), []byte("1234567")} {
if got := VncPasswordDecrypt(data); got != "" {
t.Errorf("VncPasswordDecrypt(len=%d) = %q, want 空串且不 panic", len(data), got)
}
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
// Package remote 提供 RDP/VNC 等远程控制的服务状态查询、配置读写与启停控制。 // Package remote 提供 RDP/VNC 等远程控制的服务状态查询、配置读写与启停控制。
package remote package remote
// VncServerConfig TightVNC 服务端配置(对应注册表 SOFTWARE\TightVNC\Server 下的键值) // VncServerConfig TightVNC 服务端配置(对应注册表 SOFTWARE\TightVNC\Server 下的键值)。
// 37 项 = 最新版 TightVNC(2.8.x) 标准安装落盘的全集(经本机注册表与 tvnserver.exe
// 字符串表双重核对)。另有 ViewOnlyPassword / ControlPassword 两个条件值(只读密码、
// 控制接口管理密码,开对应功能才会写入,本库暂不覆盖——WriteVncServerConfig 是
// 无条件全量写,加字段会向每台客户机落空值),待产品启用只读接入/控制认证时再扩展。
type VncServerConfig struct { type VncServerConfig struct {
ExtraPorts string `json:"extraPorts"` // 额外端口 ExtraPorts string `json:"extraPorts"` // 额外端口
QueryTimeout uint32 `json:"queryTimeout"` // 查询超时 QueryTimeout uint32 `json:"queryTimeout"` // 查询超时
+45 -49
View File
@@ -3,6 +3,9 @@
package remote package remote
import ( import (
"fmt"
"os"
"github.com/KarpelesLab/vncpasswd" "github.com/KarpelesLab/vncpasswd"
"github.com/kardianos/service" "github.com/kardianos/service"
"golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/registry"
@@ -10,47 +13,9 @@ import (
var VncService service.Service var VncService service.Service
// ReadVncServerConfig 从注册表读取 TightVNC 服务端配置 // ReadVncServerConfig 从注册表读取 TightVNC 服务端配置(键值缺失按零值返回)
func ReadVncServerConfig() (VncServerConfig, error) { func ReadVncServerConfig() (VncServerConfig, error) {
config := VncServerConfig{ var config VncServerConfig
ExtraPorts: "",
QueryTimeout: 0,
QueryAcceptOnTimeout: 0,
LocalInputPriorityTimeout: 0,
LocalInputPriority: 0,
BlockRemoteInput: 0,
BlockLocalInput: 0,
IpAccessControl: "",
RfbPort: 0,
HttpPort: 0,
DisconnectAction: 0,
AcceptRfbConnections: 0,
UseVncAuthentication: 0,
UseControlAuthentication: 0,
RepeatControlAuthentication: 0,
LoopbackOnly: 0,
AcceptHttpConnections: 0,
LogLevel: 0,
EnableFileTransfers: 0,
RemoveWallpaper: 0,
UseD3D: 0,
UseMirrorDriver: 0,
EnableUrlParams: 0,
Password: []byte{},
AlwaysShared: 0,
NeverShared: 0,
DisconnectClients: 0,
PollingInterval: 0,
AllowLoopback: 0,
VideoRecognitionInterval: 0,
GrabTransparentWindows: 0,
SaveLogToAllUsersPath: 0,
RunControlInterface: 0,
IdleTimeout: 0,
VideoClasses: "",
VideoRects: "",
ConnectToRdp: 0,
}
key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\TightVNC\\Server", registry.ALL_ACCESS) key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\TightVNC\\Server", registry.ALL_ACCESS)
if err != nil { if err != nil {
@@ -75,6 +40,7 @@ func ReadVncServerConfig() (VncServerConfig, error) {
config.RepeatControlAuthentication, _, _ = GetDwordValue(key, "RepeatControlAuthentication") config.RepeatControlAuthentication, _, _ = GetDwordValue(key, "RepeatControlAuthentication")
config.LoopbackOnly, _, _ = GetDwordValue(key, "LoopbackOnly") config.LoopbackOnly, _, _ = GetDwordValue(key, "LoopbackOnly")
config.AcceptHttpConnections, _, _ = GetDwordValue(key, "AcceptHttpConnections") config.AcceptHttpConnections, _, _ = GetDwordValue(key, "AcceptHttpConnections")
config.LogLevel, _, _ = GetDwordValue(key, "LogLevel")
config.EnableFileTransfers, _, _ = GetDwordValue(key, "EnableFileTransfers") config.EnableFileTransfers, _, _ = GetDwordValue(key, "EnableFileTransfers")
config.RemoveWallpaper, _, _ = GetDwordValue(key, "RemoveWallpaper") config.RemoveWallpaper, _, _ = GetDwordValue(key, "RemoveWallpaper")
config.UseD3D, _, _ = GetDwordValue(key, "UseD3D") config.UseD3D, _, _ = GetDwordValue(key, "UseD3D")
@@ -93,7 +59,7 @@ func ReadVncServerConfig() (VncServerConfig, error) {
config.IdleTimeout, _, _ = GetDwordValue(key, "IdleTimeout") config.IdleTimeout, _, _ = GetDwordValue(key, "IdleTimeout")
config.VideoClasses, _, _ = key.GetStringValue("VideoClasses") config.VideoClasses, _, _ = key.GetStringValue("VideoClasses")
config.VideoRects, _, _ = key.GetStringValue("VideoRects") config.VideoRects, _, _ = key.GetStringValue("VideoRects")
config.ConnectToRdp, _, _ = GetDwordValue(key, "ExtraPorts") config.ConnectToRdp, _, _ = GetDwordValue(key, "ConnectToRdp")
return config, nil return config, nil
} }
@@ -109,16 +75,42 @@ func GetDwordValue(k registry.Key, name string) (val uint32, valtype uint32, err
var vncSvcConfig = &service.Config{ var vncSvcConfig = &service.Config{
Name: "tvnserver", Name: "tvnserver",
DisplayName: "TightVNC Server", DisplayName: "TightVNC Server",
Description: "", Description: "TightVNC Server (managed by remote-core)",
Arguments: []string{"-service"}, Arguments: []string{"-service"},
} }
// InstallVncServer 安装VNC服务端 返回:结果,密码,错误 // InstallVncServer 绿色安装 TightVNC 服务端(需管理员权限)。
func InstallVncServer() (bool, string, error) { // 最新版注册表全集配置(VncClientDefaultConfig 37 项, 随机 8 位密码)→ 注册系统服务。
//安装服务 // 装而不启:服务注册后保持 Stopped, 等远程指令再 VncStart(避免常驻监听开端口);
//导入注册表 // 若服务已在运行则重启以加载新配置。返回生成的明文密码(调用方自行上报/展示)。
//生成随机密码 func InstallVncServer(exePath string) (string, error) {
return true, "12345678", nil if finfo, err := os.Stat(exePath); err != nil || finfo.IsDir() {
return "", fmt.Errorf("tvnserver.exe 不存在: %s", exePath)
}
// 最新版 TightVNC 注册表配置全集(含随机密码)
config := VncClientDefaultConfig(true)
if err := WriteVncServerConfig(config); err != nil {
return "", err
}
// 注册系统服务:未安装→安装; 运行中→重启加载新配置; 已停止→保持
vncSvcConfig.Executable = exePath
if err := VncInit(); err != nil {
return "", err
}
status, err := VncService.Status()
switch {
case err != nil: // 未安装(kardianos 对未安装服务 Status 返回错误)
if err := VncInstall(); err != nil {
return "", err
}
case status == service.StatusRunning:
if err := VncRestart(); err != nil {
return "", err
}
}
return VncPasswordDecrypt(config.Password), nil
} }
// VncInit 初始化 VNC 服务对象,VncInstall/VncStart 等操作前必须调用 // VncInit 初始化 VNC 服务对象,VncInstall/VncStart 等操作前必须调用
@@ -186,7 +178,11 @@ func VncPasswordCrypt(password string) []byte {
return vncpasswd.Crypt(password) return vncpasswd.Crypt(password)
} }
// VncPasswordDecrypt 解密 VNC 密码 // VncPasswordDecrypt 解密 VNC 密码。密文恒为 8 字节(标准 VNC DES),
// 不足 8 字节(缺失/截断)直接返回空串——底层 DES 解密会按前 8 字节切片,短输入必 panic。
func VncPasswordDecrypt(data []byte) string { func VncPasswordDecrypt(data []byte) string {
if len(data) < 8 {
return ""
}
return vncpasswd.Decrypt(data) return vncpasswd.Decrypt(data)
} }
+4
View File
@@ -280,6 +280,10 @@ func WriteVncServerConfig(config VncServerConfig) error {
if err != nil { if err != nil {
return err return err
} }
err = key.SetDWordValue("LogLevel", config.LogLevel)
if err != nil {
return err
}
err = key.SetDWordValue("EnableFileTransfers", config.EnableFileTransfers) err = key.SetDWordValue("EnableFileTransfers", config.EnableFileTransfers)
if err != nil { if err != nil {
return err return err
+7 -10
View File
@@ -176,17 +176,14 @@ func getWebShell(recDir string) gin.HandlerFunc {
} }
} else if opData.Op == "resize" { } else if opData.Op == "resize" {
fmt.Println("resize") // Data 反序列化后是 map[string]interface{}, 不能直接断言 ResizeDataType
value, ok := opData.Data.(ResizeDataType) // (此前断言恒失败, 前端 resize 被静默吞掉, PTY 固定 180x30)
if ok { raw, merr := json.Marshal(opData.Data)
err = proc.SetSize(value.Cols, value.Rows) var size ResizeDataType
if err != nil { if merr == nil && json.Unmarshal(raw, &size) == nil && size.Cols > 0 && size.Rows > 0 {
logrus.Errorf("重置大小错误 %d bytes", n) if serr := proc.SetSize(size.Cols, size.Rows); serr != nil {
} else { logrus.Errorf("重置大小错误: %s", serr.Error())
fmt.Println("重置大小成功")
} }
} else {
} }
} }