sshprox.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package sshprox
  2. import (
  3. "embed"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "runtime"
  13. "strconv"
  14. "strings"
  15. "github.com/google/uuid"
  16. "imuslab.com/zoraxy/mod/reverseproxy"
  17. "imuslab.com/zoraxy/mod/utils"
  18. "imuslab.com/zoraxy/mod/websocketproxy"
  19. )
  20. /*
  21. SSH Proxy
  22. This is a tool to bind gotty into Zoraxy
  23. so that you can do something similar to
  24. online ssh terminal
  25. */
  26. /*
  27. Bianry embedding
  28. Make sure when compile, gotty binary exists in static.gotty
  29. */
  30. var (
  31. //go:embed gotty/*
  32. gotty embed.FS
  33. )
  34. type Manager struct {
  35. StartingPort int
  36. Instances []*Instance
  37. }
  38. type Instance struct {
  39. UUID string
  40. ExecPath string
  41. RemoteAddr string
  42. RemotePort int
  43. AssignedPort int
  44. conn *reverseproxy.ReverseProxy //HTTP proxy
  45. tty *exec.Cmd //SSH connection ported to web interface
  46. Parent *Manager
  47. }
  48. func NewSSHProxyManager() *Manager {
  49. return &Manager{
  50. StartingPort: 14810,
  51. Instances: []*Instance{},
  52. }
  53. }
  54. //Get the next free port in the list
  55. func (m *Manager) GetNextPort() int {
  56. nextPort := m.StartingPort
  57. occupiedPort := make(map[int]bool)
  58. for _, instance := range m.Instances {
  59. occupiedPort[instance.AssignedPort] = true
  60. }
  61. for {
  62. if !occupiedPort[nextPort] {
  63. return nextPort
  64. }
  65. nextPort++
  66. }
  67. }
  68. func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
  69. targetInstance, err := m.GetInstanceById(instanceId)
  70. if err != nil {
  71. http.Error(w, err.Error(), http.StatusNotFound)
  72. return
  73. }
  74. if targetInstance.tty == nil {
  75. //Server side already closed
  76. http.Error(w, "Connection already closed", http.StatusInternalServerError)
  77. return
  78. }
  79. r.Header.Set("X-Forwarded-Host", r.Host)
  80. requestURL := r.URL.String()
  81. if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
  82. //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
  83. r.Header.Set("A-Upgrade", "websocket")
  84. requestURL = strings.TrimPrefix(requestURL, "/")
  85. u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
  86. wspHandler := websocketproxy.NewProxy(u)
  87. wspHandler.ServeHTTP(w, r)
  88. return
  89. }
  90. targetInstance.conn.ProxyHTTP(w, r)
  91. }
  92. func (m *Manager) GetInstanceById(instanceId string) (*Instance, error) {
  93. for _, instance := range m.Instances {
  94. if instance.UUID == instanceId {
  95. return instance, nil
  96. }
  97. }
  98. return nil, fmt.Errorf("instance not found: %s", instanceId)
  99. }
  100. func (m *Manager) NewSSHProxy(binaryRoot string) (*Instance, error) {
  101. //Check if the binary exists in system/gotty/
  102. binary := "gotty_" + runtime.GOOS + "_" + runtime.GOARCH
  103. if runtime.GOOS == "windows" {
  104. binary = binary + ".exe"
  105. }
  106. //Extract it from embedfs if not exists locally
  107. execPath := filepath.Join(binaryRoot, binary)
  108. //Create the storage folder structure
  109. os.MkdirAll(filepath.Dir(execPath), 0775)
  110. //Create config file if not exists
  111. if !utils.FileExists(filepath.Join(filepath.Dir(execPath), ".gotty")) {
  112. configFile, _ := gotty.ReadFile("gotty/.gotty")
  113. os.WriteFile(filepath.Join(filepath.Dir(execPath), ".gotty"), configFile, 0775)
  114. }
  115. //Create web.ssh binary if not exists
  116. if !utils.FileExists(execPath) {
  117. //Try to extract it from embedded fs
  118. executable, err := gotty.ReadFile("gotty/" + binary)
  119. if err != nil {
  120. //Binary not found in embedded
  121. return nil, errors.New("platform not supported")
  122. }
  123. //Extract to target location
  124. err = os.WriteFile(execPath, executable, 0777)
  125. if err != nil {
  126. //Binary not found in embedded
  127. log.Println("Extract web.ssh failed: " + err.Error())
  128. return nil, errors.New("web.ssh sub-program extract failed")
  129. }
  130. }
  131. //Convert the binary path to realpath
  132. realpath, err := filepath.Abs(execPath)
  133. if err != nil {
  134. return nil, err
  135. }
  136. thisInstance := Instance{
  137. UUID: uuid.New().String(),
  138. ExecPath: realpath,
  139. AssignedPort: -1,
  140. Parent: m,
  141. }
  142. m.Instances = append(m.Instances, &thisInstance)
  143. return &thisInstance, nil
  144. }
  145. //Create a new Connection to target address
  146. func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIpAddr string, remotePort int) error {
  147. //Create a gotty instance
  148. connAddr := remoteIpAddr
  149. if username != "" {
  150. connAddr = username + "@" + remoteIpAddr
  151. }
  152. configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
  153. title := username + "@" + remoteIpAddr
  154. if remotePort != 22 {
  155. title = title + ":" + strconv.Itoa(remotePort)
  156. }
  157. sshCommand := []string{"ssh", "-t", connAddr, "-p", strconv.Itoa(remotePort)}
  158. cmd := exec.Command(i.ExecPath, "-w", "-p", strconv.Itoa(listenPort), "--once", "--config", configPath, "--title-format", title, "bash", "-c", strings.Join(sshCommand, " "))
  159. cmd.Dir = filepath.Dir(i.ExecPath)
  160. cmd.Env = append(os.Environ(), "TERM=xterm")
  161. cmd.Stdout = os.Stdout
  162. cmd.Stderr = os.Stderr
  163. go func() {
  164. cmd.Run()
  165. i.Destroy()
  166. }()
  167. i.tty = cmd
  168. i.AssignedPort = listenPort
  169. i.RemoteAddr = remoteIpAddr
  170. i.RemotePort = remotePort
  171. //Create a new proxy agent for this root
  172. path, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(listenPort))
  173. if err != nil {
  174. return err
  175. }
  176. //Create new proxy objects to the proxy
  177. proxy := reverseproxy.NewReverseProxy(path)
  178. i.conn = proxy
  179. return nil
  180. }
  181. func (i *Instance) Destroy() {
  182. // Remove the instance from the Manager's Instances list
  183. for idx, inst := range i.Parent.Instances {
  184. if inst == i {
  185. // Remove the instance from the slice by swapping it with the last instance and slicing the slice
  186. i.Parent.Instances[len(i.Parent.Instances)-1], i.Parent.Instances[idx] = i.Parent.Instances[idx], i.Parent.Instances[len(i.Parent.Instances)-1]
  187. i.Parent.Instances = i.Parent.Instances[:len(i.Parent.Instances)-1]
  188. break
  189. }
  190. }
  191. }