config.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. package client
  2. import (
  3. "fmt"
  4. "github.com/go-yaml/go-yaml-v1"
  5. "io/ioutil"
  6. "net"
  7. "net/url"
  8. "ngrok/log"
  9. "os"
  10. "os/user"
  11. "path"
  12. "regexp"
  13. "strconv"
  14. "strings"
  15. )
  16. type Configuration struct {
  17. HttpProxy string `yaml:"http_proxy,omitempty"`
  18. ServerAddr string `yaml:"server_addr,omitempty"`
  19. InspectAddr string `yaml:"inspect_addr,omitempty"`
  20. TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"`
  21. AuthToken string `yaml:"auth_token,omitempty"`
  22. Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
  23. LogTo string `yaml:"-"`
  24. Path string `yaml:"-"`
  25. }
  26. type TunnelConfiguration struct {
  27. Subdomain string `yaml:"subdomain,omitempty"`
  28. Hostname string `yaml:"hostname,omitempty"`
  29. Protocols map[string]string `yaml:"proto,omitempty"`
  30. HttpAuth string `yaml:"auth,omitempty"`
  31. RemotePort uint16 `yaml:"remote_port,omitempty"`
  32. }
  33. func LoadConfiguration(opts *Options) (config *Configuration, err error) {
  34. configPath := opts.config
  35. if configPath == "" {
  36. configPath = defaultPath()
  37. }
  38. log.Info("Reading configuration file %s", configPath)
  39. configBuf, err := ioutil.ReadFile(configPath)
  40. if err != nil {
  41. // failure to read a configuration file is only a fatal error if
  42. // the user specified one explicitly
  43. if opts.config != "" {
  44. err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
  45. return
  46. }
  47. }
  48. // deserialize/parse the config
  49. config = new(Configuration)
  50. if err = yaml.Unmarshal(configBuf, &config); err != nil {
  51. err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
  52. return
  53. }
  54. // try to parse the old .ngrok format for backwards compatibility
  55. matched := false
  56. content := strings.TrimSpace(string(configBuf))
  57. if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
  58. return
  59. } else if matched {
  60. config = &Configuration{AuthToken: content}
  61. }
  62. // set configuration defaults
  63. if config.ServerAddr == "" {
  64. config.ServerAddr = defaultServerAddr
  65. }
  66. if config.InspectAddr == "" {
  67. config.InspectAddr = "127.0.0.1:4040"
  68. }
  69. if config.HttpProxy == "" {
  70. config.HttpProxy = os.Getenv("http_proxy")
  71. }
  72. // validate and normalize configuration
  73. if config.InspectAddr != "disabled" {
  74. if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
  75. return
  76. }
  77. }
  78. if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
  79. return
  80. }
  81. if config.HttpProxy != "" {
  82. var proxyUrl *url.URL
  83. if proxyUrl, err = url.Parse(config.HttpProxy); err != nil {
  84. return
  85. } else {
  86. if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
  87. err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
  88. return
  89. }
  90. }
  91. }
  92. for name, t := range config.Tunnels {
  93. if t == nil || t.Protocols == nil || len(t.Protocols) == 0 {
  94. err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name)
  95. return
  96. }
  97. for k, addr := range t.Protocols {
  98. tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
  99. if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
  100. return
  101. }
  102. if err = validateProtocol(k, tunnelName); err != nil {
  103. return
  104. }
  105. }
  106. // use the name of the tunnel as the subdomain if none is specified
  107. if t.Hostname == "" && t.Subdomain == "" {
  108. // XXX: a crude heuristic, really we should be checking if the last part
  109. // is a TLD
  110. if len(strings.Split(name, ".")) > 1 {
  111. t.Hostname = name
  112. } else {
  113. t.Subdomain = name
  114. }
  115. }
  116. }
  117. // override configuration with command-line options
  118. config.LogTo = opts.logto
  119. config.Path = configPath
  120. if opts.authtoken != "" {
  121. config.AuthToken = opts.authtoken
  122. }
  123. switch opts.command {
  124. // start a single tunnel, the default, simple ngrok behavior
  125. case "default":
  126. config.Tunnels = make(map[string]*TunnelConfiguration)
  127. config.Tunnels["default"] = &TunnelConfiguration{
  128. Subdomain: opts.subdomain,
  129. Hostname: opts.hostname,
  130. HttpAuth: opts.httpauth,
  131. Protocols: make(map[string]string),
  132. }
  133. for _, proto := range strings.Split(opts.protocol, "+") {
  134. if err = validateProtocol(proto, "default"); err != nil {
  135. return
  136. }
  137. if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil {
  138. return
  139. }
  140. }
  141. // start tunnels
  142. case "start":
  143. if len(opts.args) == 0 {
  144. err = fmt.Errorf("You must specify at least one tunnel to start")
  145. return
  146. }
  147. requestedTunnels := make(map[string]bool)
  148. for _, arg := range opts.args {
  149. requestedTunnels[arg] = true
  150. if _, ok := config.Tunnels[arg]; !ok {
  151. err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
  152. return
  153. }
  154. }
  155. for name, _ := range config.Tunnels {
  156. if !requestedTunnels[name] {
  157. delete(config.Tunnels, name)
  158. }
  159. }
  160. default:
  161. err = fmt.Errorf("Unknown command: %s", opts.command)
  162. return
  163. }
  164. return
  165. }
  166. func defaultPath() string {
  167. user, err := user.Current()
  168. // user.Current() does not work on linux when cross compiling because
  169. // it requires CGO; use os.Getenv("HOME") hack until we compile natively
  170. homeDir := os.Getenv("HOME")
  171. if err != nil {
  172. log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
  173. } else {
  174. homeDir = user.HomeDir
  175. }
  176. return path.Join(homeDir, ".ngrok")
  177. }
  178. func normalizeAddress(addr string, propName string) (string, error) {
  179. // normalize port to address
  180. if _, err := strconv.Atoi(addr); err == nil {
  181. addr = ":" + addr
  182. }
  183. host, port, err := net.SplitHostPort(addr)
  184. if err != nil {
  185. return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
  186. }
  187. if host == "" {
  188. host = "127.0.0.1"
  189. }
  190. return fmt.Sprintf("%s:%s", host, port), nil
  191. }
  192. func validateProtocol(proto, propName string) (err error) {
  193. switch proto {
  194. case "http", "https", "http+https", "tcp":
  195. default:
  196. err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
  197. }
  198. return
  199. }
  200. func SaveAuthToken(configPath, authtoken string) (err error) {
  201. // empty configuration by default for the case that we can't read it
  202. c := new(Configuration)
  203. // read the configuration
  204. oldConfigBytes, err := ioutil.ReadFile(configPath)
  205. if err == nil {
  206. // unmarshal if we successfully read the configuration file
  207. if err = yaml.Unmarshal(oldConfigBytes, c); err != nil {
  208. return
  209. }
  210. }
  211. // no need to save, the authtoken is already the correct value
  212. if c.AuthToken == authtoken {
  213. return
  214. }
  215. // update auth token
  216. c.AuthToken = authtoken
  217. // rewrite configuration
  218. newConfigBytes, err := yaml.Marshal(c)
  219. if err != nil {
  220. return
  221. }
  222. err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
  223. return
  224. }