configuration.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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 exclusivly.")
  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. TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
  233. }
  234. type configFileSettings struct {
  235. Configuration `yaml:",inline"`
  236. // older settings will be aggregated into the generic map, should be read via cli.Context
  237. Settings map[string]interface{} `yaml:",inline"`
  238. }
  239. func (c *Configuration) Source() string {
  240. return c.sourceFile
  241. }
  242. func (c *configFileSettings) Int(name string) (int, error) {
  243. if raw, ok := c.Settings[name]; ok {
  244. if v, ok := raw.(int); ok {
  245. return v, nil
  246. }
  247. return 0, fmt.Errorf("expected int found %T for %s", raw, name)
  248. }
  249. return 0, nil
  250. }
  251. func (c *configFileSettings) Duration(name string) (time.Duration, error) {
  252. if raw, ok := c.Settings[name]; ok {
  253. switch v := raw.(type) {
  254. case time.Duration:
  255. return v, nil
  256. case string:
  257. return time.ParseDuration(v)
  258. }
  259. return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
  260. }
  261. return 0, nil
  262. }
  263. func (c *configFileSettings) Float64(name string) (float64, error) {
  264. if raw, ok := c.Settings[name]; ok {
  265. if v, ok := raw.(float64); ok {
  266. return v, nil
  267. }
  268. return 0, fmt.Errorf("expected float found %T for %s", raw, name)
  269. }
  270. return 0, nil
  271. }
  272. func (c *configFileSettings) String(name string) (string, error) {
  273. if raw, ok := c.Settings[name]; ok {
  274. if v, ok := raw.(string); ok {
  275. return v, nil
  276. }
  277. return "", fmt.Errorf("expected string found %T for %s", raw, name)
  278. }
  279. return "", nil
  280. }
  281. func (c *configFileSettings) StringSlice(name string) ([]string, error) {
  282. if raw, ok := c.Settings[name]; ok {
  283. if slice, ok := raw.([]interface{}); ok {
  284. strSlice := make([]string, len(slice))
  285. for i, v := range slice {
  286. str, ok := v.(string)
  287. if !ok {
  288. return nil, fmt.Errorf("expected string, found %T for %v", i, v)
  289. }
  290. strSlice[i] = str
  291. }
  292. return strSlice, nil
  293. }
  294. return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
  295. }
  296. return nil, nil
  297. }
  298. func (c *configFileSettings) IntSlice(name string) ([]int, error) {
  299. if raw, ok := c.Settings[name]; ok {
  300. if slice, ok := raw.([]interface{}); ok {
  301. intSlice := make([]int, len(slice))
  302. for i, v := range slice {
  303. str, ok := v.(int)
  304. if !ok {
  305. return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
  306. }
  307. intSlice[i] = str
  308. }
  309. return intSlice, nil
  310. }
  311. if v, ok := raw.([]int); ok {
  312. return v, nil
  313. }
  314. return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
  315. }
  316. return nil, nil
  317. }
  318. func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
  319. return nil, errors.New("option type Generic not supported")
  320. }
  321. func (c *configFileSettings) Bool(name string) (bool, error) {
  322. if raw, ok := c.Settings[name]; ok {
  323. if v, ok := raw.(bool); ok {
  324. return v, nil
  325. }
  326. return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
  327. }
  328. return false, nil
  329. }
  330. var configuration configFileSettings
  331. func GetConfiguration() *Configuration {
  332. return &configuration.Configuration
  333. }
  334. // ReadConfigFile returns InputSourceContext initialized from the configuration file.
  335. // On repeat calls returns with the same file, returns without reading the file again; however,
  336. // if value of "config" flag changes, will read the new config file
  337. func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
  338. configFile := c.String("config")
  339. if configuration.Source() == configFile || configFile == "" {
  340. if configuration.Source() == "" {
  341. return nil, "", ErrNoConfigFile
  342. }
  343. return &configuration, "", nil
  344. }
  345. log.Debug().Msgf("Loading configuration from %s", configFile)
  346. file, err := os.Open(configFile)
  347. if err != nil {
  348. // If does not exist and config file was not specificly specified then return ErrNoConfigFile found.
  349. if os.IsNotExist(err) && !c.IsSet("config") {
  350. err = ErrNoConfigFile
  351. }
  352. return nil, "", err
  353. }
  354. defer file.Close()
  355. if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
  356. if err == io.EOF {
  357. log.Error().Msgf("Configuration file %s was empty", configFile)
  358. return &configuration, "", nil
  359. }
  360. return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
  361. }
  362. configuration.sourceFile = configFile
  363. // Parse it again, with strict mode, to find warnings.
  364. if file, err := os.Open(configFile); err == nil {
  365. decoder := yaml.NewDecoder(file)
  366. decoder.KnownFields(true)
  367. var unusedConfig configFileSettings
  368. if err := decoder.Decode(&unusedConfig); err != nil {
  369. warnings = err.Error()
  370. }
  371. }
  372. return &configuration, warnings, nil
  373. }
  374. // A CustomDuration is a Duration that has custom serialization for JSON.
  375. // JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers
  376. // are in nanoseconds, which in 32bit integers limits to just 2 seconds.
  377. // This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains
  378. // the YAML serde assumptions.
  379. type CustomDuration struct {
  380. time.Duration
  381. }
  382. func (s CustomDuration) MarshalJSON() ([]byte, error) {
  383. return json.Marshal(s.Duration.Seconds())
  384. }
  385. func (s *CustomDuration) UnmarshalJSON(data []byte) error {
  386. seconds, err := strconv.ParseInt(string(data), 10, 64)
  387. if err != nil {
  388. return err
  389. }
  390. s.Duration = time.Duration(seconds * int64(time.Second))
  391. return nil
  392. }
  393. func (s *CustomDuration) MarshalYAML() (interface{}, error) {
  394. return s.Duration.String(), nil
  395. }
  396. func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
  397. return unmarshal(&s.Duration)
  398. }