configuration.go 15 KB

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