blob: 37cffbe7c2be697cef249ad13a8abe637f4bd318 [file] [log] [blame]
package config
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-util"
)
// Options is a structure used to hold the route configuration options that can be supplied for the backend routes.
type Options struct {
Algorithm string `json:"alg"`
}
// Host is a structure that holds the host info for the backend route.
type Host struct {
Protocol string `json:"protocol"`
Hostname string `json:"hostname"`
Port int `json:"port"`
}
// BackendRoute holds all the information about a configured route, for which Traffic Ops serves as a reverse proxy.
type BackendRoute struct {
Path string `json:"path"`
Method string `json:"method"`
Hosts []Host `json:"hosts"`
Opts Options `json:"opts"`
ID int `json:"routeId"`
Insecure bool `json:"insecure"`
Permissions []string `json:"permissions"`
Index int
}
// BackendConfig is a structure that holds the configuration supplied to Traffic Ops, which makes it act as a reverse proxy to the specified routes.
type BackendConfig struct {
Routes []BackendRoute `json:"routes"`
}
// Config reflects the structure of the cdn.conf file
type Config struct {
URL *url.URL `json:"-"`
CertPath string `json:"-"`
KeyPath string `json:"-"`
ConfigHypnotoad `json:"hypnotoad"`
ConfigTrafficOpsGolang `json:"traffic_ops_golang"`
ConfigTO *ConfigTO `json:"to"`
SMTP *ConfigSMTP `json:"smtp"`
ConfigPortal `json:"portal"`
ConfigLetsEncrypt `json:"lets_encrypt"`
ConfigAcmeRenewal `json:"acme_renewal"`
AcmeAccounts []ConfigAcmeAccount `json:"acme_accounts"`
DB ConfigDatabase `json:"db"`
Secrets []string `json:"secrets"`
TrafficVaultEnabled bool
ConfigLDAP *ConfigLDAP
UserCacheRefreshIntervalSec int `json:"user_cache_refresh_interval_sec"`
ServerUpdateStatusCacheRefreshIntervalSec int `json:"server_update_status_cache_refresh_interval_sec"`
LDAPEnabled bool
LDAPConfPath string `json:"ldap_conf_location"`
ConfigInflux *ConfigInflux
InfluxEnabled bool
InfluxDBConfPath string `json:"influxdb_conf_path"`
Version string
DisableAutoCertDeletion bool `json:"disable_auto_cert_deletion"`
UseIMS bool `json:"use_ims"`
RoleBasedPermissions bool `json:"role_based_permissions"`
DefaultCertificateInfo *DefaultCertificateInfo `json:"default_certificate_info"`
Cdni *CdniConf `json:"cdni"`
}
// ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
type ConfigHypnotoad struct {
Listen []string `json:"listen"`
// NOTE: don't care about any other fields for now
}
// ConfigTrafficOpsGolang carries settings specific to traffic_ops_golang server
type ConfigTrafficOpsGolang struct {
// Deprecated in 5.0
Insecure bool `json:"insecure"`
// end deprecated
Port string `json:"port"`
ProxyTimeout int `json:"proxy_timeout"`
ProxyKeepAlive int `json:"proxy_keep_alive"`
ProxyTLSTimeout int `json:"proxy_tls_timeout"`
ProxyReadHeaderTimeout int `json:"proxy_read_header_timeout"`
ReadTimeout int `json:"read_timeout"`
RequestTimeout int `json:"request_timeout"`
ReadHeaderTimeout int `json:"read_header_timeout"`
WriteTimeout int `json:"write_timeout"`
IdleTimeout int `json:"idle_timeout"`
LogLocationError string `json:"log_location_error"`
LogLocationWarning string `json:"log_location_warning"`
LogLocationInfo string `json:"log_location_info"`
LogLocationDebug string `json:"log_location_debug"`
LogLocationEvent string `json:"log_location_event"`
MaxDBConnections int `json:"max_db_connections"`
DBMaxIdleConnections int `json:"db_max_idle_connections"`
DBConnMaxLifetimeSeconds int `json:"db_conn_max_lifetime_seconds"`
DBQueryTimeoutSeconds int `json:"db_query_timeout_seconds"`
Plugins []string `json:"plugins"`
PluginConfig map[string]json.RawMessage `json:"plugin_config"`
PluginSharedConfig map[string]interface{} `json:"plugin_shared_config"`
ProfilingEnabled bool `json:"profiling_enabled"`
ProfilingLocation string `json:"profiling_location"`
// Deprecated: use 'port' in traffic_vault_config instead.
RiakPort *uint `json:"riak_port"`
WhitelistedOAuthUrls []string `json:"whitelisted_oauth_urls"`
OAuthClientSecret string `json:"oauth_client_secret"`
RoutingBlacklist `json:"routing_blacklist"`
SupportedDSMetrics []string `json:"supported_ds_metrics"`
TLSConfig *tls.Config `json:"tls_config"`
TrafficVaultBackend string `json:"traffic_vault_backend"`
TrafficVaultConfig json.RawMessage `json:"traffic_vault_config"`
// CRConfigUseRequestHost is whether to use the client request host header in the CRConfig. If false, uses the tm.url parameter.
// This defaults to false. Traffic Ops used to always use the host header, setting this true will resume that legacy behavior.
// See https://github.com/apache/trafficcontrol/issues/2224
// Deprecated: will be removed in the next major version.
CRConfigUseRequestHost bool `json:"crconfig_snapshot_use_client_request_host"`
// CRConfigEmulateOldPath is whether to emulate the legacy CRConfig request path when generating a new CRConfig. This primarily exists in the event a tool relies on the legacy path '/tools/write_crconfig'.
// Deprecated: will be removed in the next major version.
CRConfigEmulateOldPath bool `json:"crconfig_emulate_old_path"`
}
// RoutingBlacklist contains a list of route IDs that are disabled,
// and whether or not to ignore unknown routes.
type RoutingBlacklist struct {
IgnoreUnknownRoutes bool `json:"ignore_unknown_routes"`
DisabledRoutes []int `json:"disabled_routes"`
}
// ConfigTO contains information to identify Traffic Ops in a network sense.
type ConfigTO struct {
BaseURL *rfc.URL `json:"base_url"`
EmailFrom *rfc.EmailAddress `json:"email_from"`
NoAccountFoundMessage *string `json:"no_account_found_msg"`
}
// ConfigPortal contains information that can direct users to a friendly UI
type ConfigPortal struct {
BaseURL rfc.URL `json:"base_url"`
DocsURL rfc.URL `json:"docs_url"`
EmailFrom rfc.EmailAddress `json:"email_from"`
PasswdResetPath string `json:"pass_reset_path"`
UserRegisterPath string `json:"user_register_path"`
}
// ConfigSMTP contains configuration information for connecting to and authenticating with an SMTP
// server.
type ConfigSMTP struct {
Address string `json:"address"`
Enabled bool `json:"enabled"`
Password string `json:"password"`
User string `json:"user"`
}
// ConfigLetsEncrypt contains configuration information for integration with the Let's Encrypt certificate authority.
type ConfigLetsEncrypt struct {
Email string `json:"user_email,omitempty"`
SendExpEmail bool `json:"send_expiration_email"`
ConvertSelfSigned bool `json:"convert_self_signed"`
RenewDaysBeforeExpiration int `json:"renew_days_before_expiration"`
Environment string `json:"environment"`
}
// ConfigAcmeRenewal continas configuration information for automated ACME renewals.
type ConfigAcmeRenewal struct {
SummaryEmail string `json:"summary_email"`
RenewDaysBeforeExpiration int `json:"renew_days_before_expiration"`
}
// ConfigAcmeAccount contains all account information for a single ACME provider to be registered with External Account Binding
type ConfigAcmeAccount struct {
AcmeProvider string `json:"acme_provider"`
UserEmail string `json:"user_email"`
AcmeUrl string `json:"acme_url"`
Kid string `json:"kid"`
HmacEncoded string `json:"hmac_encoded"`
}
type DefaultCertificateInfo struct {
BusinessUnit string `json:"business_unit"`
City string `json:"city"`
Organization string `json:"organization"`
Country string `json:"country"`
State string `json:"state"`
}
func (d *DefaultCertificateInfo) Validate() (error, bool) {
missingList := []string{}
if d.BusinessUnit == "" {
missingList = append(missingList, "BusinessUnit")
}
if d.City == "" {
missingList = append(missingList, "City")
}
if d.Organization == "" {
missingList = append(missingList, "Organization")
}
if d.Country == "" {
missingList = append(missingList, "Country")
}
if d.State == "" {
missingList = append(missingList, "State")
}
if len(missingList) != 0 {
return fmt.Errorf("default certificate information is missing: %s", missingList), false
}
return nil, true
}
// ConfigDatabase reflects the structure of the database.conf file
type ConfigDatabase struct {
Description string `json:"description"`
DBName string `json:"dbname"`
Hostname string `json:"hostname"`
User string `json:"user"`
Password string `json:"password"`
Port string `json:"port"`
Type string `json:"type"`
SSL bool `json:"ssl"`
}
type ConfigLDAP struct {
AdminPass string `json:"admin_pass"`
SearchBase string `json:"search_base"`
AdminDN string `json:"admin_dn"`
Host string `json:"host"`
SearchQuery string `json:"search_query"`
Insecure bool `json:"insecure"`
LDAPTimeoutSecs int `json:"ldap_timeout_secs"`
}
type ConfigInflux struct {
User string `json:"user"`
Password string `json:"password"`
DSDBName string `json:"deliveryservice_stats_db_name"`
CacheDBName string `json:"cache_stats_db_name"`
Secure *bool `json:"secure"`
}
type CdniConf struct {
DCdnId string `json:"dcdn_id"`
}
// NewFakeConfig returns a fake Config struct with just enough data to view Routes.
func NewFakeConfig() Config {
c := Config{}
c.URL, _ = url.Parse("http://example.com")
c.Secrets = append(c.Secrets, "foo")
return c
}
const (
DefaultLDAPTimeoutSecs = 60
DefaultDBQueryTimeoutSecs = 20
DefaultDBPort = "5432"
MinPort = 1
MaxPort = 65535
)
// ErrorLog - critical messages
func (c Config) ErrorLog() log.LogLocation {
return log.LogLocation(c.LogLocationError)
}
// WarningLog - warning messages
func (c Config) WarningLog() log.LogLocation {
return log.LogLocation(c.LogLocationWarning)
}
// InfoLog - information messages
func (c Config) InfoLog() log.LogLocation { return log.LogLocation(c.LogLocationInfo) }
// DebugLog - troubleshooting messages
func (c Config) DebugLog() log.LogLocation {
return log.LogLocation(c.LogLocationDebug)
}
// EventLog - access.log high level transactions
func (c Config) EventLog() log.LogLocation {
return log.LogLocation(c.LogLocationEvent)
}
const BlockStartup = true
const AllowStartup = false
func LoadBackendConfig(backendConfigPath string) (BackendConfig, error) {
confBytes, err := ioutil.ReadFile(backendConfigPath)
if err != nil {
return BackendConfig{}, fmt.Errorf("reading backend conf '%s': %v", backendConfigPath, err)
}
cfg := BackendConfig{}
err = json.Unmarshal(confBytes, &cfg)
if err != nil {
return BackendConfig{}, fmt.Errorf("unmarshalling '%s': %v", backendConfigPath, err)
}
for _, r := range cfg.Routes {
if r.Opts.Algorithm != "" && r.Opts.Algorithm != "roundrobin" {
return cfg, errors.New("algorithm can only be roundrobin or blank")
}
for _, h := range r.Hosts {
rawURL := h.Protocol + "://" + h.Hostname + ":" + strconv.Itoa(h.Port)
if _, err = url.ParseRequestURI(rawURL); err != nil {
return cfg, fmt.Errorf("couldn't convert host info into a valid URI: %v", err)
}
}
}
return cfg, nil
}
func LoadCdnConfig(cdnConfPath string) (Config, error) {
// load json from cdn.conf
confBytes, err := ioutil.ReadFile(cdnConfPath)
if err != nil {
return Config{}, fmt.Errorf("reading CDN conf '%s': %v", cdnConfPath, err)
}
cfg := Config{}
err = json.Unmarshal(confBytes, &cfg)
if err != nil {
return Config{}, fmt.Errorf("unmarshalling '%s': %v", cdnConfPath, err)
}
if cfg.SMTP == nil {
cfg.SMTP = &ConfigSMTP{}
}
return cfg, nil
}
// LoadConfig - reads the config file into the Config struct
func LoadConfig(cdnConfPath string, dbConfPath string, appVersion string) (Config, []error, bool) {
// load cdn.conf
cfg, err := LoadCdnConfig(cdnConfPath)
if err != nil {
return Config{}, []error{fmt.Errorf("Loading cdn config from '%s': %v", cdnConfPath, err)}, BlockStartup
}
cfg.Version = appVersion
// load json from database.conf
dbConfBytes, err := ioutil.ReadFile(dbConfPath)
if err != nil {
return Config{}, []error{fmt.Errorf("reading db conf '%s': %v", dbConfPath, err)}, BlockStartup
}
err = json.Unmarshal(dbConfBytes, &cfg.DB)
if err != nil {
return Config{}, []error{fmt.Errorf("unmarshalling '%s': %v", dbConfPath, err)}, BlockStartup
}
if portNum, err := strconv.Atoi(cfg.DB.Port); err != nil || portNum < MinPort || MaxPort < portNum {
_, _ = fmt.Fprintf(os.Stderr, "error parsing database port: '%s' is invalid. Using default %s\n", cfg.DB.Port, DefaultDBPort)
cfg.DB.Port = DefaultDBPort
}
cfg, err = ParseConfig(cfg)
if err != nil {
return Config{}, []error{fmt.Errorf("parsing config '%s': %v", cdnConfPath, err)}, BlockStartup
}
// check for and load ldap.conf
if cfg.LDAPConfPath != "" {
cfg.LDAPEnabled, cfg.ConfigLDAP, err = GetLDAPConfig(cfg.LDAPConfPath)
if err != nil {
cfg.LDAPEnabled = false
return cfg, []error{fmt.Errorf("parsing ldap config '%s': %v", cfg.LDAPConfPath, err)}, BlockStartup
}
} else { // ldap config location not specified in cdn.conf, check in directory with cdn.conf for backwards compatibility with perl.
confDir := filepath.Dir(cdnConfPath)
genericLDAPConfPath := filepath.Join(confDir, "ldap.conf")
if _, err := os.Stat(genericLDAPConfPath); !os.IsNotExist(err) { // ldap.conf exists and we should error if it is not readable/parseable.
cfg.LDAPEnabled, cfg.ConfigLDAP, err = GetLDAPConfig(genericLDAPConfPath)
if err != nil { // no config or unparseable, do not enable LDAP
cfg.LDAPEnabled = false
return cfg, []error{err}, BlockStartup
}
} else {
cfg.LDAPEnabled = false // no ldap.conf, disable and allow startup
}
}
idbPath := cfg.InfluxDBConfPath
if idbPath == "" {
mojoMode := os.Getenv("MOJO_MODE")
if cwd, err := os.Getwd(); mojoMode != "" && err != nil {
idbPath = filepath.Join(cwd, "conf", mojoMode, "influxdb.conf")
} else {
idbPath = filepath.Join(filepath.Dir(cdnConfPath), "influxdb.conf")
}
}
if _, err = os.Stat(idbPath); err != nil {
if os.IsNotExist(err) {
cfg.InfluxEnabled = false
} else {
return cfg, []error{err}, BlockStartup
}
} else if cfg.InfluxEnabled, cfg.ConfigInflux, err = GetInfluxConfig(idbPath); err != nil {
return cfg, []error{err}, BlockStartup
}
return cfg, []error{}, AllowStartup
}
// GetCertPath - extracts path to cert .cert file
func (c Config) GetCertPath() string {
v, ok := c.URL.Query()["cert"]
if ok {
return v[0]
}
return ""
}
// GetKeyPath - extracts path to cert .key file
func (c Config) GetKeyPath() string {
v, ok := c.URL.Query()["key"]
if ok {
return v[0]
}
return ""
}
const (
DBMaxIdleConnectionsDefault = 10 // if this is higher than MaxDBConnections it will be automatically adjusted below it by the db/sql library
DBConnMaxLifetimeSecondsDefault = 60
)
// ParseConfig validates required fields, and parses non-JSON types
func ParseConfig(cfg Config) (Config, error) {
missings := ""
if cfg.Port == "" {
missings += "port, "
}
if len(cfg.Secrets) == 0 {
missings += "secrets, "
}
if cfg.LogLocationError == "" {
cfg.LogLocationError = log.LogLocationNull
}
if cfg.LogLocationWarning == "" {
cfg.LogLocationWarning = log.LogLocationNull
}
if cfg.LogLocationInfo == "" {
cfg.LogLocationInfo = log.LogLocationNull
}
if cfg.LogLocationDebug == "" {
cfg.LogLocationDebug = log.LogLocationNull
}
if cfg.LogLocationEvent == "" {
cfg.LogLocationEvent = log.LogLocationNull
}
if cfg.DBMaxIdleConnections == 0 {
cfg.DBMaxIdleConnections = DBMaxIdleConnectionsDefault
}
if cfg.DBConnMaxLifetimeSeconds == 0 {
cfg.DBConnMaxLifetimeSeconds = DBConnMaxLifetimeSecondsDefault
}
if cfg.DBQueryTimeoutSeconds == 0 {
cfg.DBQueryTimeoutSeconds = DefaultDBQueryTimeoutSecs
}
if cfg.UserCacheRefreshIntervalSec < 0 {
cfg.UserCacheRefreshIntervalSec = 0
}
if cfg.ServerUpdateStatusCacheRefreshIntervalSec < 0 {
cfg.ServerUpdateStatusCacheRefreshIntervalSec = 0
}
invalidTOURLStr := ""
var err error
if len(cfg.Listen) < 1 {
missings += `"listen", `
} else {
listen := cfg.Listen[0]
if cfg.URL, err = url.Parse(listen); err != nil {
invalidTOURLStr = fmt.Sprintf("invalid Traffic Ops URL '%s': %v", listen, err)
}
cfg.KeyPath = cfg.GetKeyPath()
cfg.CertPath = cfg.GetCertPath()
newURL := url.URL{Scheme: cfg.URL.Scheme, Host: cfg.URL.Host, Path: cfg.URL.Path}
cfg.URL = &newURL
}
if cfg.ConfigTO == nil {
missings += "to, "
} else {
if cfg.ConfigTO.BaseURL == nil || cfg.ConfigTO.BaseURL.String() == "" {
missings += "to.base_url, "
}
if cfg.ConfigTO.EmailFrom == nil || cfg.ConfigTO.EmailFrom.String() == "<@>" {
missings += "to.email_from, "
}
if cfg.ConfigTO.NoAccountFoundMessage == nil || *cfg.ConfigTO.NoAccountFoundMessage == "" {
missings += "to.no_account_found_msg, "
}
}
if len(missings) > 0 {
missings = "missing fields: " + missings[:len(missings)-2] // strip final `, `
}
errStr := missings
if errStr != "" && invalidTOURLStr != "" {
errStr += "; "
}
errStr += invalidTOURLStr
if errStr != "" {
return Config{}, fmt.Errorf(errStr)
}
if err := ValidateRoutingBlacklist(cfg.RoutingBlacklist); err != nil {
return Config{}, err
}
return cfg, nil
}
func ValidateRoutingBlacklist(blacklist RoutingBlacklist) error {
seenDisabledIDs := make(map[int]struct{}, len(blacklist.DisabledRoutes))
for _, id := range blacklist.DisabledRoutes {
if _, found := seenDisabledIDs[id]; !found {
seenDisabledIDs[id] = struct{}{}
} else {
return fmt.Errorf("route ID %d is listed multiple times in disabled_routes", id)
}
}
return nil
}
func GetLDAPConfig(LDAPConfPath string) (bool, *ConfigLDAP, error) {
LDAPConfBytes, err := ioutil.ReadFile(LDAPConfPath)
if err != nil {
return false, nil, fmt.Errorf("reading LDAP conf '%v': %v", LDAPConfPath, err)
}
LDAPconf, err := getLDAPConf(string(LDAPConfBytes))
if err != nil {
return false, LDAPconf, fmt.Errorf("parsing LDAP conf '%v': %v", LDAPConfBytes, err)
}
if strings.TrimSpace(LDAPconf.AdminPass) == "" {
return false, LDAPconf, fmt.Errorf("LDAP conf missing admin_pass field")
}
if strings.TrimSpace(LDAPconf.SearchBase) == "" {
return false, LDAPconf, fmt.Errorf("LDAP conf missing search_base field")
}
if strings.TrimSpace(LDAPconf.AdminDN) == "" {
return false, LDAPconf, fmt.Errorf("LDAP conf missing admin_dn field")
}
if strings.TrimSpace(LDAPconf.Host) == "" {
return false, LDAPconf, fmt.Errorf("LDAP conf missing host field")
}
if strings.TrimSpace(LDAPconf.SearchQuery) == "" {
return false, LDAPconf, fmt.Errorf("LDAP conf missing search_query field")
}
return true, LDAPconf, nil
}
func GetInfluxConfig(path string) (bool, *ConfigInflux, error) {
raw, err := ioutil.ReadFile(path)
if err != nil {
return false, nil, fmt.Errorf("reading InfluxDB configuration from '%s': %v", path, err)
}
c := ConfigInflux{}
if err := json.Unmarshal(raw, &c); err != nil {
return false, nil, fmt.Errorf("parsing InfluxDB configuration from '%s': %v", path, err)
}
if c.User == "" {
return false, &c, errors.New("InfluxDB configuration missing a username")
}
if c.Password == "" {
return false, &c, errors.New("InfluxDB configuration missing a password")
}
if c.DSDBName == "" {
log.Warnln("InfluxDB configuration does not specify a DS stats DB name - falling back on 'deliveryservice_stats'")
c.DSDBName = "deliveryservice_stats"
}
if c.CacheDBName == "" {
log.Warnln("InfluxDB configuration does not specify a Cache Stats DB name - falling back on 'cache_stats'")
c.CacheDBName = "cache_stats"
}
if c.Secure == nil {
log.Warnln("InfluxDB configuration does not specify 'secure', defaulting to 'false'")
c.Secure = util.BoolPtr(false)
}
return true, &c, nil
}
func getLDAPConf(s string) (*ConfigLDAP, error) {
ldapConf := ConfigLDAP{LDAPTimeoutSecs: DefaultLDAPTimeoutSecs} //if the field is not set in the config we use the default instead of 0
err := json.Unmarshal([]byte(s), &ldapConf)
return &ldapConf, err
}