123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- package client
- import (
- "fmt"
- "github.com/go-yaml/go-yaml-v1"
- "io/ioutil"
- "net"
- "net/url"
- "ngrok/log"
- "os"
- "os/user"
- "path"
- "regexp"
- "strconv"
- "strings"
- )
- type Configuration struct {
- HttpProxy string `yaml:"http_proxy,omitempty"`
- ServerAddr string `yaml:"server_addr,omitempty"`
- InspectAddr string `yaml:"inspect_addr,omitempty"`
- TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"`
- AuthToken string `yaml:"auth_token,omitempty"`
- Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
- LogTo string `yaml:"-"`
- Path string `yaml:"-"`
- }
- type TunnelConfiguration struct {
- Subdomain string `yaml:"subdomain,omitempty"`
- Hostname string `yaml:"hostname,omitempty"`
- Protocols map[string]string `yaml:"proto,omitempty"`
- HttpAuth string `yaml:"auth,omitempty"`
- RemotePort uint16 `yaml:"remote_port,omitempty"`
- }
- func LoadConfiguration(opts *Options) (config *Configuration, err error) {
- configPath := opts.config
- if configPath == "" {
- configPath = defaultPath()
- }
- log.Info("Reading configuration file %s", configPath)
- configBuf, err := ioutil.ReadFile(configPath)
- if err != nil {
- // failure to read a configuration file is only a fatal error if
- // the user specified one explicitly
- if opts.config != "" {
- err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
- return
- }
- }
- // deserialize/parse the config
- config = new(Configuration)
- if err = yaml.Unmarshal(configBuf, &config); err != nil {
- err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
- return
- }
- // try to parse the old .ngrok format for backwards compatibility
- matched := false
- content := strings.TrimSpace(string(configBuf))
- if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
- return
- } else if matched {
- config = &Configuration{AuthToken: content}
- }
- // set configuration defaults
- if config.ServerAddr == "" {
- config.ServerAddr = defaultServerAddr
- }
- if config.InspectAddr == "" {
- config.InspectAddr = "127.0.0.1:4040"
- }
- if config.HttpProxy == "" {
- config.HttpProxy = os.Getenv("http_proxy")
- }
- // validate and normalize configuration
- if config.InspectAddr != "disabled" {
- if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
- return
- }
- }
- if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
- return
- }
- if config.HttpProxy != "" {
- var proxyUrl *url.URL
- if proxyUrl, err = url.Parse(config.HttpProxy); err != nil {
- return
- } else {
- if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
- err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
- return
- }
- }
- }
- for name, t := range config.Tunnels {
- if t == nil || t.Protocols == nil || len(t.Protocols) == 0 {
- err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name)
- return
- }
- for k, addr := range t.Protocols {
- tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
- if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
- return
- }
- if err = validateProtocol(k, tunnelName); err != nil {
- return
- }
- }
- // use the name of the tunnel as the subdomain if none is specified
- if t.Hostname == "" && t.Subdomain == "" {
- // XXX: a crude heuristic, really we should be checking if the last part
- // is a TLD
- if len(strings.Split(name, ".")) > 1 {
- t.Hostname = name
- } else {
- t.Subdomain = name
- }
- }
- }
- // override configuration with command-line options
- config.LogTo = opts.logto
- config.Path = configPath
- if opts.authtoken != "" {
- config.AuthToken = opts.authtoken
- }
- switch opts.command {
- // start a single tunnel, the default, simple ngrok behavior
- case "default":
- config.Tunnels = make(map[string]*TunnelConfiguration)
- config.Tunnels["default"] = &TunnelConfiguration{
- Subdomain: opts.subdomain,
- Hostname: opts.hostname,
- HttpAuth: opts.httpauth,
- Protocols: make(map[string]string),
- }
- for _, proto := range strings.Split(opts.protocol, "+") {
- if err = validateProtocol(proto, "default"); err != nil {
- return
- }
- if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil {
- return
- }
- }
- // start tunnels
- case "start":
- if len(opts.args) == 0 {
- err = fmt.Errorf("You must specify at least one tunnel to start")
- return
- }
- requestedTunnels := make(map[string]bool)
- for _, arg := range opts.args {
- requestedTunnels[arg] = true
- if _, ok := config.Tunnels[arg]; !ok {
- err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
- return
- }
- }
- for name, _ := range config.Tunnels {
- if !requestedTunnels[name] {
- delete(config.Tunnels, name)
- }
- }
- default:
- err = fmt.Errorf("Unknown command: %s", opts.command)
- return
- }
- return
- }
- func defaultPath() string {
- user, err := user.Current()
- // user.Current() does not work on linux when cross compiling because
- // it requires CGO; use os.Getenv("HOME") hack until we compile natively
- homeDir := os.Getenv("HOME")
- if err != nil {
- log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
- } else {
- homeDir = user.HomeDir
- }
- return path.Join(homeDir, ".ngrok")
- }
- func normalizeAddress(addr string, propName string) (string, error) {
- // normalize port to address
- if _, err := strconv.Atoi(addr); err == nil {
- addr = ":" + addr
- }
- host, port, err := net.SplitHostPort(addr)
- if err != nil {
- return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
- }
- if host == "" {
- host = "127.0.0.1"
- }
- return fmt.Sprintf("%s:%s", host, port), nil
- }
- func validateProtocol(proto, propName string) (err error) {
- switch proto {
- case "http", "https", "http+https", "tcp":
- default:
- err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
- }
- return
- }
- func SaveAuthToken(configPath, authtoken string) (err error) {
- // empty configuration by default for the case that we can't read it
- c := new(Configuration)
- // read the configuration
- oldConfigBytes, err := ioutil.ReadFile(configPath)
- if err == nil {
- // unmarshal if we successfully read the configuration file
- if err = yaml.Unmarshal(oldConfigBytes, c); err != nil {
- return
- }
- }
- // no need to save, the authtoken is already the correct value
- if c.AuthToken == authtoken {
- return
- }
- // update auth token
- c.AuthToken = authtoken
- // rewrite configuration
- newConfigBytes, err := yaml.Marshal(c)
- if err != nil {
- return
- }
- err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
- return
- }
|