mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-11 06:06:35 +00:00

cloudflared tail will now fetch the management token from by making a request to the Cloudflare API using the cert.pem (acquired from cloudflared login). Refactored some of the credentials code into it's own package as to allow for easier use between subcommands outside of `cloudflared tunnel`.
451 lines
15 KiB
Go
451 lines
15 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 (
|
|
// 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"`
|
|
// Access holds all access related configs
|
|
Access *AccessConfig `yaml:"access" json:"access,omitempty"`
|
|
}
|
|
|
|
type AccessConfig struct {
|
|
// Required when set to true will fail every request that does not arrive through an access authenticated endpoint.
|
|
Required bool `yaml:"required" json:"required,omitempty"`
|
|
|
|
// TeamName is the organization team name to get the public key certificates for.
|
|
TeamName string `yaml:"teamName" json:"teamName"`
|
|
|
|
// AudTag is the AudTag to verify access JWT against.
|
|
AudTag []string `yaml:"audTag" json:"audTag"`
|
|
}
|
|
|
|
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"`
|
|
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
|
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
|
}
|
|
|
|
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 does not exist and config file was not specificly specified then return ErrNoConfigFile found.
|
|
if os.IsNotExist(err) && !c.IsSet("config") {
|
|
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)
|
|
}
|