437 lines
14 KiB
Go
437 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
homedir "github.com/mitchellh/go-homedir"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
"github.com/urfave/cli/v2"
|
|
yaml "gopkg.in/yaml.v3"
|
|
|
|
"github.com/cloudflare/cloudflared/validation"
|
|
)
|
|
|
|
var (
|
|
// DefaultConfigFiles is the file names from which we attempt to read configuration.
|
|
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
|
|
|
// DefaultUnixConfigLocation is the primary location to find a config file
|
|
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
|
|
|
|
// DefaultUnixLogLocation is the primary location to find log files
|
|
DefaultUnixLogLocation = "/var/log/cloudflared"
|
|
|
|
// Launchd doesn't set root env variables, so there is default
|
|
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
|
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
|
|
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
|
|
|
|
ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
|
|
)
|
|
|
|
const (
|
|
DefaultCredentialFile = "cert.pem"
|
|
|
|
// BastionFlag is to enable bastion, or jump host, operation
|
|
BastionFlag = "bastion"
|
|
)
|
|
|
|
// DefaultConfigDirectory returns the default directory of the config file
|
|
func DefaultConfigDirectory() string {
|
|
if runtime.GOOS == "windows" {
|
|
path := os.Getenv("CFDPATH")
|
|
if path == "" {
|
|
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
|
|
if _, err := os.Stat(path); os.IsNotExist(err) { // doesn't exist, so return an empty failure string
|
|
return ""
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
return DefaultUnixConfigLocation
|
|
}
|
|
|
|
// DefaultLogDirectory returns the default directory for log files
|
|
func DefaultLogDirectory() string {
|
|
if runtime.GOOS == "windows" {
|
|
return DefaultConfigDirectory()
|
|
}
|
|
return DefaultUnixLogLocation
|
|
}
|
|
|
|
// DefaultConfigPath returns the default location of a config file
|
|
func DefaultConfigPath() string {
|
|
dir := DefaultConfigDirectory()
|
|
if dir == "" {
|
|
return DefaultConfigFiles[0]
|
|
}
|
|
return filepath.Join(dir, DefaultConfigFiles[0])
|
|
}
|
|
|
|
// DefaultConfigSearchDirectories returns the default folder locations of the config
|
|
func DefaultConfigSearchDirectories() []string {
|
|
dirs := make([]string, len(defaultUserConfigDirs))
|
|
copy(dirs, defaultUserConfigDirs)
|
|
if runtime.GOOS != "windows" {
|
|
dirs = append(dirs, defaultNixConfigDirs...)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// FileExists checks to see if a file exist at the provided path.
|
|
func FileExists(path string) (bool, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// ignore missing files
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
_ = f.Close()
|
|
return true, nil
|
|
}
|
|
|
|
// FindDefaultConfigPath returns the first path that contains a config file.
|
|
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
|
|
// contains a config file, return empty string.
|
|
func FindDefaultConfigPath() string {
|
|
for _, configDir := range DefaultConfigSearchDirectories() {
|
|
for _, configFile := range DefaultConfigFiles {
|
|
dirPath, err := homedir.Expand(configDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
path := filepath.Join(dirPath, configFile)
|
|
if ok, _ := FileExists(path); ok {
|
|
return path
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// FindOrCreateConfigPath returns the first path that contains a config file
|
|
// or creates one in the primary default path if it doesn't exist
|
|
func FindOrCreateConfigPath() string {
|
|
path := FindDefaultConfigPath()
|
|
|
|
if path == "" {
|
|
// create the default directory if it doesn't exist
|
|
path = DefaultConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
|
return ""
|
|
}
|
|
|
|
// write a new config file out
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer file.Close()
|
|
|
|
logDir := DefaultLogDirectory()
|
|
_ = os.MkdirAll(logDir, os.ModePerm) // try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
|
|
|
|
c := Root{
|
|
LogDirectory: logDir,
|
|
}
|
|
if err := yaml.NewEncoder(file).Encode(&c); err != nil {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
|
// i.e. it fails if a user specifies both --url and --unix-socket
|
|
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
|
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
|
|
return "", errors.New("--unix-socket must be used exclusivly.")
|
|
}
|
|
return c.String("unix-socket"), nil
|
|
}
|
|
|
|
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
|
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
|
func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
|
|
var url = c.String("url")
|
|
if allowURLFromArgs && c.NArg() > 0 {
|
|
if c.IsSet("url") {
|
|
return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
|
}
|
|
url = c.Args().Get(0)
|
|
}
|
|
validUrl, err := validation.ValidateUrl(url)
|
|
return validUrl, err
|
|
}
|
|
|
|
type UnvalidatedIngressRule struct {
|
|
Hostname string `json:"hostname,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Service string `json:"service,omitempty"`
|
|
OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
|
|
}
|
|
|
|
// OriginRequestConfig is a set of optional fields that users may set to
|
|
// customize how cloudflared sends requests to origin services. It is used to set
|
|
// up general config that apply to all rules, and also, specific per-rule
|
|
// config.
|
|
// Note:
|
|
// - To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
|
|
// - To specify a time.Duration in json, use int64 of the nanoseconds
|
|
type OriginRequestConfig struct {
|
|
// HTTP proxy timeout for establishing a new connection
|
|
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
|
// HTTP proxy timeout for completing a TLS handshake
|
|
TLSTimeout *CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout,omitempty"`
|
|
// HTTP proxy TCP keepalive duration
|
|
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
|
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
|
|
NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs,omitempty"`
|
|
// HTTP proxy maximum keepalive connection pool size
|
|
KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections,omitempty"`
|
|
// HTTP proxy timeout for closing an idle connection
|
|
KeepAliveTimeout *CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout,omitempty"`
|
|
// Sets the HTTP Host header for the local webserver.
|
|
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
|
|
// Hostname on the origin server certificate.
|
|
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
|
|
// Path to the CA for the certificate of your origin.
|
|
// This option should be used only if your certificate is not signed by Cloudflare.
|
|
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
|
|
// Disables TLS verification of the certificate presented by your origin.
|
|
// Will allow any certificate from the origin to be accepted.
|
|
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
|
|
NoTLSVerify *bool `yaml:"noTLSVerify" json:"noTLSVerify,omitempty"`
|
|
// Disables chunked transfer encoding.
|
|
// Useful if you are running a WSGI server.
|
|
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding,omitempty"`
|
|
// Runs as jump host
|
|
BastionMode *bool `yaml:"bastionMode" json:"bastionMode,omitempty"`
|
|
// Listen address for the proxy.
|
|
ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress,omitempty"`
|
|
// Listen port for the proxy.
|
|
ProxyPort *uint `yaml:"proxyPort" json:"proxyPort,omitempty"`
|
|
// Valid options are 'socks' or empty.
|
|
ProxyType *string `yaml:"proxyType" json:"proxyType,omitempty"`
|
|
// IP rules for the proxy service
|
|
IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules,omitempty"`
|
|
// Attempt to connect to origin with HTTP/2
|
|
Http2Origin *bool `yaml:"http2Origin" json:"http2Origin,omitempty"`
|
|
}
|
|
|
|
type IngressIPRule struct {
|
|
Prefix *string `yaml:"prefix" json:"prefix"`
|
|
Ports []int `yaml:"ports" json:"ports"`
|
|
Allow bool `yaml:"allow" json:"allow"`
|
|
}
|
|
|
|
type Configuration struct {
|
|
TunnelID string `yaml:"tunnel"`
|
|
Ingress []UnvalidatedIngressRule
|
|
WarpRouting WarpRoutingConfig `yaml:"warp-routing"`
|
|
OriginRequest OriginRequestConfig `yaml:"originRequest"`
|
|
sourceFile string
|
|
}
|
|
|
|
type WarpRoutingConfig struct {
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
}
|
|
|
|
type configFileSettings struct {
|
|
Configuration `yaml:",inline"`
|
|
// older settings will be aggregated into the generic map, should be read via cli.Context
|
|
Settings map[string]interface{} `yaml:",inline"`
|
|
}
|
|
|
|
func (c *Configuration) Source() string {
|
|
return c.sourceFile
|
|
}
|
|
|
|
func (c *configFileSettings) Int(name string) (int, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if v, ok := raw.(int); ok {
|
|
return v, nil
|
|
}
|
|
return 0, fmt.Errorf("expected int found %T for %s", raw, name)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func (c *configFileSettings) Duration(name string) (time.Duration, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
switch v := raw.(type) {
|
|
case time.Duration:
|
|
return v, nil
|
|
case string:
|
|
return time.ParseDuration(v)
|
|
}
|
|
return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func (c *configFileSettings) Float64(name string) (float64, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if v, ok := raw.(float64); ok {
|
|
return v, nil
|
|
}
|
|
return 0, fmt.Errorf("expected float found %T for %s", raw, name)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func (c *configFileSettings) String(name string) (string, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if v, ok := raw.(string); ok {
|
|
return v, nil
|
|
}
|
|
return "", fmt.Errorf("expected string found %T for %s", raw, name)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (c *configFileSettings) StringSlice(name string) ([]string, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if slice, ok := raw.([]interface{}); ok {
|
|
strSlice := make([]string, len(slice))
|
|
for i, v := range slice {
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected string, found %T for %v", i, v)
|
|
}
|
|
strSlice[i] = str
|
|
}
|
|
return strSlice, nil
|
|
}
|
|
return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *configFileSettings) IntSlice(name string) ([]int, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if slice, ok := raw.([]interface{}); ok {
|
|
intSlice := make([]int, len(slice))
|
|
for i, v := range slice {
|
|
str, ok := v.(int)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
|
|
}
|
|
intSlice[i] = str
|
|
}
|
|
return intSlice, nil
|
|
}
|
|
if v, ok := raw.([]int); ok {
|
|
return v, nil
|
|
}
|
|
return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
|
|
return nil, errors.New("option type Generic not supported")
|
|
}
|
|
|
|
func (c *configFileSettings) Bool(name string) (bool, error) {
|
|
if raw, ok := c.Settings[name]; ok {
|
|
if v, ok := raw.(bool); ok {
|
|
return v, nil
|
|
}
|
|
return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
var configuration configFileSettings
|
|
|
|
func GetConfiguration() *Configuration {
|
|
return &configuration.Configuration
|
|
}
|
|
|
|
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
|
|
// On repeat calls returns with the same file, returns without reading the file again; however,
|
|
// if value of "config" flag changes, will read the new config file
|
|
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
|
|
configFile := c.String("config")
|
|
if configuration.Source() == configFile || configFile == "" {
|
|
if configuration.Source() == "" {
|
|
return nil, "", ErrNoConfigFile
|
|
}
|
|
return &configuration, "", nil
|
|
}
|
|
|
|
log.Debug().Msgf("Loading configuration from %s", configFile)
|
|
file, err := os.Open(configFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = ErrNoConfigFile
|
|
}
|
|
return nil, "", err
|
|
}
|
|
defer file.Close()
|
|
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
|
|
if err == io.EOF {
|
|
log.Error().Msgf("Configuration file %s was empty", configFile)
|
|
return &configuration, "", nil
|
|
}
|
|
return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
|
}
|
|
configuration.sourceFile = configFile
|
|
|
|
// Parse it again, with strict mode, to find warnings.
|
|
if file, err := os.Open(configFile); err == nil {
|
|
decoder := yaml.NewDecoder(file)
|
|
decoder.KnownFields(true)
|
|
var unusedConfig configFileSettings
|
|
if err := decoder.Decode(&unusedConfig); err != nil {
|
|
warnings = err.Error()
|
|
}
|
|
}
|
|
|
|
return &configuration, warnings, nil
|
|
}
|
|
|
|
// A CustomDuration is a Duration that has custom serialization for JSON.
|
|
// JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers
|
|
// are in nanoseconds, which in 32bit integers limits to just 2 seconds.
|
|
// This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains
|
|
// the YAML serde assumptions.
|
|
type CustomDuration struct {
|
|
time.Duration
|
|
}
|
|
|
|
func (s CustomDuration) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(s.Duration.Seconds())
|
|
}
|
|
|
|
func (s *CustomDuration) UnmarshalJSON(data []byte) error {
|
|
seconds, err := strconv.ParseInt(string(data), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Duration = time.Duration(seconds * int64(time.Second))
|
|
return nil
|
|
}
|
|
|
|
func (s *CustomDuration) MarshalYAML() (interface{}, error) {
|
|
return s.Duration.String(), nil
|
|
}
|
|
|
|
func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
return unmarshal(&s.Duration)
|
|
}
|