configuration.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package tunnel
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "os"
  9. "os/exec"
  10. "strings"
  11. )
  12. // Configurator is an interface with a single `Apply` method.
  13. // Available Configurators:
  14. // - Configuration{}
  15. // - WithServers
  16. //
  17. // See `Start` package-level function.
  18. type Configurator interface {
  19. Apply(*Configuration)
  20. }
  21. // ConfiguratorFunc a function signature that completes the `Configurator` interface.
  22. type ConfiguratorFunc func(*Configuration)
  23. // Apply should set the Configuration "tc".
  24. func (opt ConfiguratorFunc) Apply(tc *Configuration) {
  25. opt(tc)
  26. }
  27. // WithServers its a helper which returns a new Configuration
  28. // added one or more Tunnels based on `http.Server` instances.
  29. func WithServers(servers ...*http.Server) ConfiguratorFunc {
  30. return func(tc *Configuration) {
  31. for _, srv := range servers {
  32. tunnel := Tunnel{
  33. Addr: srv.Addr,
  34. }
  35. tc.Tunnels = append(tc.Tunnels, tunnel)
  36. srv.RegisterOnShutdown(func() {
  37. tc.StopTunnel(tunnel)
  38. })
  39. }
  40. }
  41. }
  42. type (
  43. // Configuration contains configuration
  44. // for the optional tunneling through ngrok feature.
  45. // Note that the ngrok should be already installed at the host machine.
  46. Configuration struct {
  47. // Client defaults to the http.DefaultClient,
  48. // callers can use this field to change it.
  49. Client *http.Client
  50. // AuthToken field is optionally and can be used
  51. // to authenticate the ngrok access.
  52. // ngrok authtoken <YOUR_AUTHTOKEN>
  53. AuthToken string `ini:"auth_token" json:"authToken,omitempty" yaml:"AuthToken" toml:"AuthToken"`
  54. // No:
  55. // Config is optionally and can be used
  56. // to load ngrok configuration from file system path.
  57. //
  58. // If you don't specify a location for a configuration file,
  59. // ngrok tries to read one from the default location $HOME/.ngrok2/ngrok.yml.
  60. // The configuration file is optional; no error is emitted if that path does not exist.
  61. // Config string `json:"config,omitempty" yaml:"Config" toml:"Config"`
  62. // Bin is the system binary path of the ngrok executable file.
  63. // If it's empty then it will try to find it through system env variables.
  64. Bin string `ini:"bin" json:"bin,omitempty" yaml:"Bin" toml:"Bin"`
  65. // WebUIAddr is the web interface address of an already-running ngrok instance.
  66. // The package will try to fetch the default web interface address(http://127.0.0.1:4040)
  67. // to determinate if a ngrok instance is running before try to start it manually.
  68. // However if a custom web interface address is used,
  69. // this field must be set e.g. http://127.0.0.1:5050.
  70. WebInterface string `ini:"web_interface" json:"webInterface,omitempty" yaml:"WebInterface" toml:"WebInterface"`
  71. // Region is optionally, can be used to set the region which defaults to "us".
  72. // Available values are:
  73. // "us" for United States
  74. // "eu" for Europe
  75. // "ap" for Asia/Pacific
  76. // "au" for Australia
  77. // "sa" for South America
  78. // "jp" forJapan
  79. // "in" for India
  80. Region string `ini:"region" json:"region,omitempty" yaml:"Region" toml:"Region"`
  81. // Tunnels the collection of the tunnels.
  82. // Most of the times you only need one.
  83. Tunnels []Tunnel `ini:"tunnels" json:"tunnels" yaml:"Tunnels" toml:"Tunnels"`
  84. }
  85. // Tunnel is the Tunnels field of the Configuration structure.
  86. Tunnel struct {
  87. // Name is the only one required field,
  88. // it is used to create and close tunnels, e.g. "MyApp".
  89. // If this field is not empty then ngrok tunnels will be created
  90. // when the app is up and running.
  91. Name string `ini:"name" json:"name" yaml:"Name" toml:"Name"`
  92. // Addr should be set of form 'hostname:port'.
  93. Addr string `ini:"addr" json:"addr,omitempty" yaml:"Addr" toml:"Addr"`
  94. // Hostname is a static subdomain that can be used instead of random URLs
  95. // when paid account.
  96. Hostname string `ini:"hostname" json:"hostname,omitempty" yaml:"Hostname" toml:"Hostname"`
  97. }
  98. )
  99. var _ Configurator = Configuration{}
  100. func getConfiguration(c Configurator) Configuration {
  101. cfg := Configuration{}
  102. c.Apply(&cfg)
  103. if cfg.Client == nil {
  104. cfg.Client = http.DefaultClient
  105. }
  106. if cfg.WebInterface == "" {
  107. cfg.WebInterface = DefaultWebInterface
  108. }
  109. return cfg
  110. }
  111. // Apply implements the Option on the Configuration structure.
  112. func (tc Configuration) Apply(c *Configuration) {
  113. *c = tc
  114. }
  115. func (tc Configuration) isEnabled() bool {
  116. return len(tc.Tunnels) > 0
  117. }
  118. func (tc Configuration) isNgrokRunning() bool {
  119. resp, err := tc.Client.Get(tc.WebInterface)
  120. if err != nil {
  121. return false
  122. }
  123. resp.Body.Close()
  124. return true
  125. }
  126. // https://ngrok.com/docs/ngrok-agent/api
  127. type ngrokTunnel struct {
  128. Name string `json:"name"`
  129. Addr string `json:"addr"`
  130. Proto string `json:"proto"`
  131. Auth string `json:"basic_auth,omitempty"`
  132. // BindTLS bool `json:"bind_tls"`
  133. Schemes []string `json:"schemes"`
  134. Hostname string `json:"hostname"`
  135. }
  136. // ErrExec returns when ngrok executable was not found in the PATH or NGROK environment variable.
  137. var ErrExec = errors.New(`"ngrok" executable not found, please install it from: https://ngrok.com/download`)
  138. // StartTunnel starts the ngrok, if not already running,
  139. // creates and starts a localhost tunnel. It binds the "publicAddr" pointer
  140. // to the value of the ngrok's output public address.
  141. func (tc Configuration) StartTunnel(t Tunnel, publicAddr *string) error {
  142. tunnelAPIRequest := ngrokTunnel{
  143. Name: t.Name,
  144. Addr: t.Addr,
  145. Hostname: t.Hostname,
  146. Proto: "http",
  147. Schemes: []string{"https"},
  148. // BindTLS: true,
  149. }
  150. if !tc.isNgrokRunning() {
  151. ngrokBin := "ngrok" // environment binary.
  152. if tc.Bin == "" {
  153. _, err := exec.LookPath(ngrokBin)
  154. if err != nil {
  155. ngrokEnvVar, found := os.LookupEnv("NGROK")
  156. if !found {
  157. return ErrExec
  158. }
  159. ngrokBin = ngrokEnvVar
  160. }
  161. } else {
  162. ngrokBin = tc.Bin
  163. }
  164. // if tc.AuthToken != "" {
  165. // cmd := exec.Command(ngrokBin, "config", "add-authtoken", tc.AuthToken)
  166. // err := cmd.Run()
  167. // if err != nil {
  168. // return err
  169. // }
  170. // }
  171. // start -none, start without tunnels.
  172. // and finally the -log stdout logs to the stdout otherwise the pipe will never be able to read from, spent a lot of time on this lol.
  173. cmd := exec.Command(ngrokBin, "start", "--none", "--log", "stdout")
  174. // if tc.Config != "" {
  175. // cmd.Args = append(cmd.Args, []string{"--config", tc.Config}...)
  176. // }
  177. if tc.AuthToken != "" {
  178. cmd.Args = append(cmd.Args, []string{"--authtoken", tc.AuthToken}...)
  179. }
  180. if tc.Region != "" {
  181. cmd.Args = append(cmd.Args, []string{"--region", tc.Region}...)
  182. }
  183. // cmd.Stdout = os.Stdout
  184. // cmd.Stderr = os.Stderr
  185. stdout, err := cmd.StdoutPipe()
  186. if err != nil {
  187. return err
  188. }
  189. // stderr, err := cmd.StderrPipe()
  190. // if err != nil {
  191. // return err
  192. // }
  193. if err := cmd.Start(); err != nil {
  194. return err
  195. }
  196. p := make([]byte, 256)
  197. okText := []byte("client session established")
  198. for {
  199. n, err := stdout.Read(p)
  200. if err != nil {
  201. // if errors.Is(err, io.EOF) {
  202. // return nil
  203. // }
  204. return err
  205. }
  206. // we need this one:
  207. // msg="client session established"
  208. // note that this will block if something terrible happens
  209. // but ngrok's errors are strong so the error is easy to be resolved without any logs.
  210. if bytes.Contains(p[:n], okText) {
  211. break
  212. }
  213. }
  214. }
  215. return tc.createTunnel(tunnelAPIRequest, publicAddr)
  216. }
  217. func (tc Configuration) createTunnel(tunnelAPIRequest ngrokTunnel, publicAddr *string) error {
  218. url := fmt.Sprintf("%s/api/tunnels", tc.WebInterface)
  219. requestData, err := json.Marshal(tunnelAPIRequest)
  220. if err != nil {
  221. return err
  222. }
  223. resp, err := tc.Client.Post(url, "application/json", bytes.NewBuffer(requestData))
  224. if err != nil {
  225. return err
  226. }
  227. defer resp.Body.Close()
  228. type publicAddrOrErrResp struct {
  229. PublicAddr string `json:"public_url"`
  230. Details struct {
  231. ErrorText string `json:"err"` // when can't bind more addresses, status code was successful.
  232. } `json:"details"`
  233. ErrMsg string `json:"msg"` // when ngrok is not yet ready, status code was unsuccessful.
  234. }
  235. var apiResponse publicAddrOrErrResp
  236. err = json.NewDecoder(resp.Body).Decode(&apiResponse)
  237. if err != nil {
  238. return err
  239. }
  240. if errText := strings.Join([]string{apiResponse.ErrMsg, apiResponse.Details.ErrorText}, ": "); len(errText) > 2 {
  241. return errors.New(errText)
  242. }
  243. *publicAddr = apiResponse.PublicAddr
  244. return nil
  245. }
  246. // StopTunnel removes and stops a tunnel from a running ngrok instance.
  247. func (tc Configuration) StopTunnel(t Tunnel) error {
  248. url := fmt.Sprintf("%s/api/tunnels/%s", tc.WebInterface, t.Name)
  249. req, err := http.NewRequest(http.MethodDelete, url, nil)
  250. if err != nil {
  251. return err
  252. }
  253. resp, err := tc.Client.Do(req)
  254. if err != nil {
  255. return err
  256. }
  257. defer resp.Body.Close()
  258. if resp.StatusCode != http.StatusNoContent {
  259. return fmt.Errorf("stopTunnel: unexpected status code: %d", resp.StatusCode)
  260. }
  261. return nil
  262. }