跳板机管理平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

315 lines
6.2 KiB

4 years ago
  1. package main
  2. import (
  3. "database/sql"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "os/signal"
  9. "os/user"
  10. "runtime"
  11. "strconv"
  12. "strings"
  13. "syscall"
  14. "time"
  15. "github.com/manifoldco/promptui"
  16. _ "github.com/mattn/go-sqlite3"
  17. "golang.org/x/crypto/ssh"
  18. "golang.org/x/crypto/ssh/terminal"
  19. )
  20. var (
  21. DarwinAuthFileFmt = "/Users/%s/.ssh/id_rsa"
  22. LinuxAuthFileFmt = "/home/%s/.ssh/id_rsa"
  23. AuthFileFmt = LinuxAuthFileFmt
  24. DatabaseFile = "/usr/local/jumpserver/jumpserver.db"
  25. QuitSelect = "quit"
  26. )
  27. type SSHTerminal struct {
  28. Session *ssh.Session
  29. exitMsg string
  30. stdout io.Reader
  31. stdin io.Writer
  32. stderr io.Reader
  33. }
  34. // ssh server
  35. type HostServer struct {
  36. Name string
  37. Host string
  38. Port int
  39. }
  40. // SSHUser ssh user
  41. type SSHUser struct {
  42. Username string
  43. Password string
  44. IdentityFile string
  45. }
  46. func init() {
  47. if runtime.GOOS == "linux" {
  48. AuthFileFmt = LinuxAuthFileFmt
  49. } else if runtime.GOOS == "darwin" {
  50. AuthFileFmt = DarwinAuthFileFmt
  51. } else {
  52. fmt.Printf("program not support os(%s)\n", runtime.GOOS)
  53. }
  54. }
  55. func (t *SSHTerminal) updateTerminalSize() {
  56. go func() {
  57. // 窗口大小改变
  58. sigwinchCh := make(chan os.Signal, 1)
  59. signal.Notify(sigwinchCh, syscall.SIGWINCH)
  60. fd := int(os.Stdin.Fd())
  61. termWidth, termHeight, err := terminal.GetSize(fd)
  62. if err != nil {
  63. fmt.Println(err)
  64. }
  65. for {
  66. select {
  67. case sigwinch := <-sigwinchCh:
  68. if sigwinch == nil {
  69. return
  70. }
  71. currTermWidth, currTermHeight, err := terminal.GetSize(fd)
  72. // 窗口没有发生改变
  73. if currTermHeight == termHeight && currTermWidth == termWidth {
  74. continue
  75. }
  76. t.Session.WindowChange(currTermHeight, currTermWidth)
  77. if err != nil {
  78. fmt.Printf("Unable to send window-change reqest: %s.", err)
  79. continue
  80. }
  81. termWidth, termHeight = currTermWidth, currTermHeight
  82. }
  83. }
  84. }()
  85. }
  86. func (t *SSHTerminal) interactiveSession() error {
  87. defer func() {
  88. if t.exitMsg == "" {
  89. fmt.Fprintln(os.Stdout, "the connection was closed on the remote side on ", time.Now().Format(time.RFC822))
  90. } else {
  91. fmt.Fprintln(os.Stdout, t.exitMsg)
  92. }
  93. }()
  94. fd := int(os.Stdin.Fd())
  95. state, err := terminal.MakeRaw(fd)
  96. if err != nil {
  97. return err
  98. }
  99. defer terminal.Restore(fd, state)
  100. termWidth, termHeight, err := terminal.GetSize(fd)
  101. if err != nil {
  102. return err
  103. }
  104. termType := os.Getenv("TERM")
  105. if termType == "" {
  106. termType = "xterm-256color"
  107. }
  108. err = t.Session.RequestPty(termType, termHeight, termWidth, ssh.TerminalModes{
  109. ssh.ECHO: 1,
  110. ssh.TTY_OP_ISPEED: 14400,
  111. ssh.TTY_OP_OSPEED: 14400,
  112. })
  113. if err != nil {
  114. return err
  115. }
  116. t.updateTerminalSize()
  117. t.stdin, err = t.Session.StdinPipe()
  118. if err != nil {
  119. return err
  120. }
  121. t.stdout, err = t.Session.StdoutPipe()
  122. if err != nil {
  123. return err
  124. }
  125. t.stderr, err = t.Session.StderrPipe()
  126. go io.Copy(os.Stderr, t.stderr)
  127. go io.Copy(os.Stdout, t.stdout)
  128. go func() {
  129. buf := make([]byte, 128)
  130. for {
  131. n, err := os.Stdin.Read(buf)
  132. if err != nil {
  133. fmt.Println(err)
  134. return
  135. }
  136. if n > 0 {
  137. _, err = t.stdin.Write(buf[:n])
  138. if err != nil {
  139. fmt.Println(err)
  140. t.exitMsg = err.Error()
  141. return
  142. }
  143. }
  144. }
  145. }()
  146. err = t.Session.Shell()
  147. if err != nil {
  148. return err
  149. }
  150. err = t.Session.Wait()
  151. if err != nil {
  152. return err
  153. }
  154. return nil
  155. }
  156. func NewTerminal(server *HostServer, sshUser *SSHUser) (err error) {
  157. key, err := ioutil.ReadFile(fmt.Sprintf(AuthFileFmt, sshUser.Username))
  158. if err != nil {
  159. return
  160. }
  161. signer, err := ssh.ParsePrivateKey(key)
  162. if err != nil {
  163. return
  164. }
  165. sshConfig := &ssh.ClientConfig{
  166. User: sshUser.Username,
  167. Auth: []ssh.AuthMethod{
  168. ssh.Password(sshUser.Password),
  169. ssh.PublicKeys(signer),
  170. },
  171. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  172. }
  173. client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", server.Host, server.Port), sshConfig)
  174. if err != nil {
  175. fmt.Println(err)
  176. }
  177. defer client.Close()
  178. session, err := client.NewSession()
  179. if err != nil {
  180. return err
  181. }
  182. defer session.Close()
  183. s := SSHTerminal{
  184. Session: session,
  185. }
  186. return s.interactiveSession()
  187. }
  188. func handlerSignal() {
  189. c := make(chan os.Signal, 1)
  190. signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
  191. for {
  192. s := <-c
  193. switch s {
  194. case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
  195. return
  196. case syscall.SIGHUP:
  197. default:
  198. return
  199. }
  200. }
  201. }
  202. func main() {
  203. // 获取当前用户
  204. userInfo, err := user.Current()
  205. if err != nil {
  206. fmt.Println("user.Current failed!")
  207. return
  208. }
  209. // 连接数据库
  210. db, err := sql.Open("sqlite3", DatabaseFile)
  211. if err != nil {
  212. fmt.Printf("open database failed! err=%v", err)
  213. return
  214. }
  215. // 查询当前用户有访问权限的主机
  216. rows, err := db.Query(fmt.Sprintf("SELECT name, ip, port FROM hosts where isdelete=0 and name in(select hostname from hostuser where username='%s');", userInfo.Username))
  217. if err != nil {
  218. fmt.Printf("db.Query failed! err=%v", err)
  219. return
  220. }
  221. // ui展示列表
  222. menuLabels := make([]string, 0)
  223. menuLabels = append(menuLabels, QuitSelect) // 退出
  224. for rows.Next() {
  225. var (
  226. name string
  227. ip string
  228. port int
  229. )
  230. err = rows.Scan(&name, &ip, &port)
  231. if err != nil {
  232. fmt.Printf("rows.Scan failed! err=%v", err)
  233. continue
  234. }
  235. menuLabels = append(menuLabels, fmt.Sprintf("%s:%s:%d", name, ip, port))
  236. }
  237. rows.Close()
  238. db.Close()
  239. // 选项列表
  240. prompt := promptui.Select{
  241. Label: "Select Host (quit for ctrl+c or select quit)",
  242. Items: menuLabels,
  243. Size: len(menuLabels),
  244. }
  245. for {
  246. _, selectLabel, err := prompt.Run()
  247. if err != nil {
  248. fmt.Printf("Prompt failed %v\n", err)
  249. return
  250. }
  251. fmt.Printf("You choose %q\n", selectLabel)
  252. if selectLabel == QuitSelect {
  253. return
  254. }
  255. // 获取选中信息
  256. hostItems := strings.Split(selectLabel, ":")
  257. if len(hostItems) != 3 {
  258. fmt.Printf("invalid ssh host: %s", selectLabel)
  259. continue
  260. }
  261. port, err := strconv.ParseInt(hostItems[2], 10, 32)
  262. if err != nil {
  263. fmt.Printf("invalid ssh port: %s", selectLabel[2])
  264. continue
  265. }
  266. server := &HostServer{Name: hostItems[0], Host: hostItems[1], Port: int(port)}
  267. user := &SSHUser{Username: userInfo.Username, IdentityFile: fmt.Sprintf(AuthFileFmt, userInfo.Username)}
  268. // 新建一个终端
  269. err = NewTerminal(server, user)
  270. if err != nil {
  271. fmt.Printf("NewTerminal err=%v\n", err)
  272. }
  273. }
  274. }