sshprox.go 5.8 KB

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