service.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // Package ws 提供运维诊断通道(移植自 sdf-serve internal/ws):
  2. // ping/traceroute 起子进程逐行推流,telnet 做 TCP 拨测。
  3. package ws
  4. import (
  5. "bufio"
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "io"
  10. "net"
  11. "os/exec"
  12. "regexp"
  13. "runtime"
  14. "strconv"
  15. "time"
  16. "github.com/gorilla/websocket"
  17. "golang.org/x/text/encoding/simplifiedchinese"
  18. "golang.org/x/text/transform"
  19. "udeServer/internal/types"
  20. )
  21. type Service struct{}
  22. func NewService() *Service {
  23. return &Service{}
  24. }
  25. // ValidateTarget 校验 ping/traceroute/telnet 的目标地址:仅允许 IP 或
  26. // 字母/数字/点/连字符,阻断空格与特殊字符注入。
  27. func ValidateTarget(target string) bool {
  28. if net.ParseIP(target) != nil {
  29. return true
  30. }
  31. re := regexp.MustCompile(`^[a-zA-Z0-9.-]+$`)
  32. return re.MatchString(target)
  33. }
  34. // ValidatePort 校验 telnet 端口范围。
  35. func ValidatePort(port int) bool {
  36. return port > 0 && port <= 65535
  37. }
  38. // Ping 实时输出(跨平台)
  39. func (s *Service) Ping(conn *websocket.Conn, req types.WSRequest) {
  40. count := req.Count
  41. if count == 0 {
  42. count = 4
  43. }
  44. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  45. defer cancel()
  46. var args []string
  47. if runtime.GOOS == "windows" {
  48. args = []string{req.Target, "/n", fmt.Sprintf("%d", count)}
  49. } else {
  50. args = []string{"-c", fmt.Sprintf("%d", count), req.Target}
  51. }
  52. cmd := exec.CommandContext(ctx, "ping", args...)
  53. stdout, err := cmd.StdoutPipe()
  54. if err != nil {
  55. _ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
  56. return
  57. }
  58. if err := cmd.Start(); err != nil {
  59. _ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
  60. return
  61. }
  62. scanner := bufio.NewScanner(stdout)
  63. for scanner.Scan() {
  64. line := scanner.Text()
  65. if runtime.GOOS == "windows" {
  66. line = gbkToUtf8([]byte(line))
  67. }
  68. _ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "running", Output: line})
  69. }
  70. if err := cmd.Wait(); err != nil {
  71. _ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
  72. return
  73. }
  74. _ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "done", Output: "Ping finished"})
  75. }
  76. // Traceroute 实时输出(跨平台)
  77. func (s *Service) Traceroute(conn *websocket.Conn, req types.WSRequest) {
  78. maxHops := req.MaxHops
  79. if maxHops == 0 {
  80. maxHops = 30
  81. }
  82. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  83. defer cancel()
  84. var cmd *exec.Cmd
  85. if runtime.GOOS == "windows" {
  86. // Windows 使用 tracert,/h 指定最大跳数
  87. cmd = exec.CommandContext(ctx, "tracert", "/h", fmt.Sprintf("%d", maxHops), req.Target)
  88. } else {
  89. // Linux/macOS 使用 traceroute
  90. cmd = exec.CommandContext(ctx, "traceroute", "-m", fmt.Sprintf("%d", maxHops), req.Target)
  91. }
  92. stdout, err := cmd.StdoutPipe()
  93. if err != nil {
  94. _ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
  95. return
  96. }
  97. if err := cmd.Start(); err != nil {
  98. _ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
  99. return
  100. }
  101. scanner := bufio.NewScanner(stdout)
  102. for scanner.Scan() {
  103. line := scanner.Text()
  104. if runtime.GOOS == "windows" {
  105. line = gbkToUtf8([]byte(line)) // Windows 输出是 GBK
  106. }
  107. _ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "running", Output: line})
  108. }
  109. if err := cmd.Wait(); err != nil {
  110. _ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
  111. return
  112. }
  113. _ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "done", Output: "Traceroute finished"})
  114. }
  115. // Telnet 实时输出
  116. func (s *Service) Telnet(conn *websocket.Conn, req types.WSRequest) {
  117. address := net.JoinHostPort(req.Host, strconv.Itoa(req.Port))
  118. _ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "running", Output: fmt.Sprintf("尝试连接 %s ...", address)})
  119. timeout := 5 * time.Second
  120. start := time.Now()
  121. type dialResult struct {
  122. msg string
  123. ok bool
  124. }
  125. connCh := make(chan dialResult, 1)
  126. go func() {
  127. _, err := net.DialTimeout("tcp", address, timeout)
  128. if err != nil {
  129. connCh <- dialResult{fmt.Sprintf("连接 %s 失败: %v", address, err), false}
  130. } else {
  131. connCh <- dialResult{fmt.Sprintf("连接 %s 成功", address), true}
  132. }
  133. }()
  134. select {
  135. case res := <-connCh:
  136. status := "done"
  137. if !res.ok {
  138. status = "error"
  139. }
  140. _ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: status, Output: res.msg})
  141. case <-time.After(timeout + 1*time.Second):
  142. _ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "error", Output: "连接超时"})
  143. }
  144. duration := time.Since(start).Milliseconds()
  145. _ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "done", Output: fmt.Sprintf("耗时 %d ms", duration)})
  146. }
  147. // gbkToUtf8 转换 Windows 子进程 GBK 输出。
  148. func gbkToUtf8(data []byte) string {
  149. reader := transform.NewReader(bytes.NewReader(data), simplifiedchinese.GBK.NewDecoder())
  150. result, _ := io.ReadAll(reader)
  151. return string(result)
  152. }