configuration.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package config
  2. import (
  3. "fmt"
  4. "io"
  5. "net/url"
  6. "os"
  7. "path/filepath"
  8. "runtime"
  9. "time"
  10. "github.com/mitchellh/go-homedir"
  11. "github.com/pkg/errors"
  12. "github.com/rs/zerolog"
  13. "github.com/urfave/cli/v2"
  14. "gopkg.in/yaml.v2"
  15. "github.com/cloudflare/cloudflared/validation"
  16. )
  17. var (
  18. // DefaultConfigFiles is the file names from which we attempt to read configuration.
  19. DefaultConfigFiles = []string{"config.yml", "config.yaml"}
  20. // DefaultUnixConfigLocation is the primary location to find a config file
  21. DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
  22. // DefaultUnixLogLocation is the primary location to find log files
  23. DefaultUnixLogLocation = "/var/log/cloudflared"
  24. // Launchd doesn't set root env variables, so there is default
  25. // Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
  26. defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
  27. defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
  28. ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
  29. )
  30. const (
  31. DefaultCredentialFile = "cert.pem"
  32. // BastionFlag is to enable bastion, or jump host, operation
  33. BastionFlag = "bastion"
  34. )
  35. // DefaultConfigDirectory returns the default directory of the config file
  36. func DefaultConfigDirectory() string {
  37. if runtime.GOOS == "windows" {
  38. path := os.Getenv("CFDPATH")
  39. if path == "" {
  40. path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
  41. if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
  42. return ""
  43. }
  44. }
  45. return path
  46. }
  47. return DefaultUnixConfigLocation
  48. }
  49. // DefaultLogDirectory returns the default directory for log files
  50. func DefaultLogDirectory() string {
  51. if runtime.GOOS == "windows" {
  52. return DefaultConfigDirectory()
  53. }
  54. return DefaultUnixLogLocation
  55. }
  56. // DefaultConfigPath returns the default location of a config file
  57. func DefaultConfigPath() string {
  58. dir := DefaultConfigDirectory()
  59. if dir == "" {
  60. return DefaultConfigFiles[0]
  61. }
  62. return filepath.Join(dir, DefaultConfigFiles[0])
  63. }
  64. // DefaultConfigSearchDirectories returns the default folder locations of the config
  65. func DefaultConfigSearchDirectories() []string {
  66. dirs := make([]string, len(defaultUserConfigDirs))
  67. copy(dirs, defaultUserConfigDirs)
  68. if runtime.GOOS != "windows" {
  69. dirs = append(dirs, defaultNixConfigDirs...)
  70. }
  71. return dirs
  72. }
  73. // FileExists checks to see if a file exist at the provided path.
  74. func FileExists(path string) (bool, error) {
  75. f, err := os.Open(path)
  76. if err != nil {
  77. if os.IsNotExist(err) {
  78. // ignore missing files
  79. return false, nil
  80. }
  81. return false, err
  82. }
  83. _ = f.Close()
  84. return true, nil
  85. }
  86. // FindDefaultConfigPath returns the first path that contains a config file.
  87. // If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
  88. // contains a config file, return empty string.
  89. func FindDefaultConfigPath() string {
  90. for _, configDir := range DefaultConfigSearchDirectories() {
  91. for _, configFile := range DefaultConfigFiles {
  92. dirPath, err := homedir.Expand(configDir)
  93. if err != nil {
  94. continue
  95. }
  96. path := filepath.Join(dirPath, configFile)
  97. if ok, _ := FileExists(path); ok {
  98. return path
  99. }
  100. }
  101. }
  102. return ""
  103. }
  104. // FindOrCreateConfigPath returns the first path that contains a config file
  105. // or creates one in the primary default path if it doesn't exist
  106. func FindOrCreateConfigPath() string {
  107. path := FindDefaultConfigPath()
  108. if path == "" {
  109. // create the default directory if it doesn't exist
  110. path = DefaultConfigPath()
  111. if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
  112. return ""
  113. }
  114. // write a new config file out
  115. file, err := os.Create(path)
  116. if err != nil {
  117. return ""
  118. }
  119. defer file.Close()
  120. logDir := DefaultLogDirectory()
  121. _ = os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
  122. c := Root{
  123. LogDirectory: logDir,
  124. }
  125. if err := yaml.NewEncoder(file).Encode(&c); err != nil {
  126. return ""
  127. }
  128. }
  129. return path
  130. }
  131. // ValidateUnixSocket ensures --unix-socket param is used exclusively
  132. // i.e. it fails if a user specifies both --url and --unix-socket
  133. func ValidateUnixSocket(c *cli.Context) (string, error) {
  134. if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
  135. return "", errors.New("--unix-socket must be used exclusivly.")
  136. }
  137. return c.String("unix-socket"), nil
  138. }
  139. // ValidateUrl will validate url flag correctness. It can be either from --url or argument
  140. // Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
  141. func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
  142. var url = c.String("url")
  143. if allowURLFromArgs && c.NArg() > 0 {
  144. if c.IsSet("url") {
  145. return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
  146. }
  147. url = c.Args().Get(0)
  148. }
  149. validUrl, err := validation.ValidateUrl(url)
  150. return validUrl, err
  151. }
  152. type UnvalidatedIngressRule struct {
  153. Hostname string
  154. Path string
  155. Service string
  156. OriginRequest OriginRequestConfig `yaml:"originRequest"`
  157. }
  158. // OriginRequestConfig is a set of optional fields that users may set to
  159. // customize how cloudflared sends requests to origin services. It is used to set
  160. // up general config that apply to all rules, and also, specific per-rule
  161. // config.
  162. // Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
  163. type OriginRequestConfig struct {
  164. // HTTP proxy timeout for establishing a new connection
  165. ConnectTimeout *time.Duration `yaml:"connectTimeout"`
  166. // HTTP proxy timeout for completing a TLS handshake
  167. TLSTimeout *time.Duration `yaml:"tlsTimeout"`
  168. // HTTP proxy TCP keepalive duration
  169. TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"`
  170. // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
  171. NoHappyEyeballs *bool `yaml:"noHappyEyeballs"`
  172. // HTTP proxy maximum keepalive connection pool size
  173. KeepAliveConnections *int `yaml:"keepAliveConnections"`
  174. // HTTP proxy timeout for closing an idle connection
  175. KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"`
  176. // Sets the HTTP Host header for the local webserver.
  177. HTTPHostHeader *string `yaml:"httpHostHeader"`
  178. // Hostname on the origin server certificate.
  179. OriginServerName *string `yaml:"originServerName"`
  180. // Path to the CA for the certificate of your origin.
  181. // This option should be used only if your certificate is not signed by Cloudflare.
  182. CAPool *string `yaml:"caPool"`
  183. // Disables TLS verification of the certificate presented by your origin.
  184. // Will allow any certificate from the origin to be accepted.
  185. // Note: The connection from your machine to Cloudflare's Edge is still encrypted.
  186. NoTLSVerify *bool `yaml:"noTLSVerify"`
  187. // Disables chunked transfer encoding.
  188. // Useful if you are running a WSGI server.
  189. DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"`
  190. // Runs as jump host
  191. BastionMode *bool `yaml:"bastionMode"`
  192. // Listen address for the proxy.
  193. ProxyAddress *string `yaml:"proxyAddress"`
  194. // Listen port for the proxy.
  195. ProxyPort *uint `yaml:"proxyPort"`
  196. // Valid options are 'socks' or empty.
  197. ProxyType *string `yaml:"proxyType"`
  198. // IP rules for the proxy service
  199. IPRules []IngressIPRule `yaml:"ipRules"`
  200. }
  201. type IngressIPRule struct {
  202. Prefix *string `yaml:"prefix"`
  203. Ports []int `yaml:"ports"`
  204. Allow bool `yaml:"allow"`
  205. }
  206. type Configuration struct {
  207. TunnelID string `yaml:"tunnel"`
  208. Ingress []UnvalidatedIngressRule
  209. WarpRouting WarpRoutingConfig `yaml:"warp-routing"`
  210. OriginRequest OriginRequestConfig `yaml:"originRequest"`
  211. sourceFile string
  212. }
  213. type WarpRoutingConfig struct {
  214. Enabled bool `yaml:"enabled"`
  215. }
  216. type configFileSettings struct {
  217. Configuration `yaml:",inline"`
  218. // older settings will be aggregated into the generic map, should be read via cli.Context
  219. Settings map[string]interface{} `yaml:",inline"`
  220. }
  221. func (c *Configuration) Source() string {
  222. return c.sourceFile
  223. }
  224. func (c *configFileSettings) Int(name string) (int, error) {
  225. if raw, ok := c.Settings[name]; ok {
  226. if v, ok := raw.(int); ok {
  227. return v, nil
  228. }
  229. return 0, fmt.Errorf("expected int found %T for %s", raw, name)
  230. }
  231. return 0, nil
  232. }
  233. func (c *configFileSettings) Duration(name string) (time.Duration, error) {
  234. if raw, ok := c.Settings[name]; ok {
  235. switch v := raw.(type) {
  236. case time.Duration:
  237. return v, nil
  238. case string:
  239. return time.ParseDuration(v)
  240. }
  241. return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
  242. }
  243. return 0, nil
  244. }
  245. func (c *configFileSettings) Float64(name string) (float64, error) {
  246. if raw, ok := c.Settings[name]; ok {
  247. if v, ok := raw.(float64); ok {
  248. return v, nil
  249. }
  250. return 0, fmt.Errorf("expected float found %T for %s", raw, name)
  251. }
  252. return 0, nil
  253. }
  254. func (c *configFileSettings) String(name string) (string, error) {
  255. if raw, ok := c.Settings[name]; ok {
  256. if v, ok := raw.(string); ok {
  257. return v, nil
  258. }
  259. return "", fmt.Errorf("expected string found %T for %s", raw, name)
  260. }
  261. return "", nil
  262. }
  263. func (c *configFileSettings) StringSlice(name string) ([]string, error) {
  264. if raw, ok := c.Settings[name]; ok {
  265. if slice, ok := raw.([]interface{}); ok {
  266. strSlice := make([]string, len(slice))
  267. for i, v := range slice {
  268. str, ok := v.(string)
  269. if !ok {
  270. return nil, fmt.Errorf("expected string, found %T for %v", i, v)
  271. }
  272. strSlice[i] = str
  273. }
  274. return strSlice, nil
  275. }
  276. return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
  277. }
  278. return nil, nil
  279. }
  280. func (c *configFileSettings) IntSlice(name string) ([]int, error) {
  281. if raw, ok := c.Settings[name]; ok {
  282. if slice, ok := raw.([]interface{}); ok {
  283. intSlice := make([]int, len(slice))
  284. for i, v := range slice {
  285. str, ok := v.(int)
  286. if !ok {
  287. return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
  288. }
  289. intSlice[i] = str
  290. }
  291. return intSlice, nil
  292. }
  293. if v, ok := raw.([]int); ok {
  294. return v, nil
  295. }
  296. return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
  297. }
  298. return nil, nil
  299. }
  300. func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
  301. return nil, errors.New("option type Generic not supported")
  302. }
  303. func (c *configFileSettings) Bool(name string) (bool, error) {
  304. if raw, ok := c.Settings[name]; ok {
  305. if v, ok := raw.(bool); ok {
  306. return v, nil
  307. }
  308. return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
  309. }
  310. return false, nil
  311. }
  312. var configuration configFileSettings
  313. func GetConfiguration() *Configuration {
  314. return &configuration.Configuration
  315. }
  316. // ReadConfigFile returns InputSourceContext initialized from the configuration file.
  317. // On repeat calls returns with the same file, returns without reading the file again; however,
  318. // if value of "config" flag changes, will read the new config file
  319. func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, error) {
  320. configFile := c.String("config")
  321. if configuration.Source() == configFile || configFile == "" {
  322. if configuration.Source() == "" {
  323. return nil, ErrNoConfigFile
  324. }
  325. return &configuration, nil
  326. }
  327. log.Debug().Msgf("Loading configuration from %s", configFile)
  328. file, err := os.Open(configFile)
  329. if err != nil {
  330. if os.IsNotExist(err) {
  331. err = ErrNoConfigFile
  332. }
  333. return nil, err
  334. }
  335. defer file.Close()
  336. if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
  337. if err == io.EOF {
  338. log.Error().Msgf("Configuration file %s was empty", configFile)
  339. return &configuration, nil
  340. }
  341. return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
  342. }
  343. configuration.sourceFile = configFile
  344. return &configuration, nil
  345. }