blob: 028ee7e9fc26f88c3a38d69f3ec561121cf333b0 [file] [log] [blame]
package crconfig
/*
* 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 (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/lib/pq"
)
const CDNSOAMinimum = 30 * time.Second
const CDNSOAExpire = 604800 * time.Second
const CDNSOARetry = 7200 * time.Second
const CDNSOARefresh = 28800 * time.Second
const CDNSOAAdmin = "traffic_ops"
const DefaultTLDTTLSOA = 86400 * time.Second
const DefaultTLDTTLNS = 3600 * time.Second
const GeoProviderMaxmindStr = "maxmindGeolocationService"
const GeoProviderNeustarStr = "neustarGeolocationService"
func makeDSes(cdn string, domain string, tx *sql.Tx) (map[string]tc.CRConfigDeliveryService, error) {
dses := map[string]tc.CRConfigDeliveryService{}
admin := CDNSOAAdmin
expireSecondsStr := strconv.Itoa(int(CDNSOAExpire / time.Second))
minimumSecondsStr := strconv.Itoa(int(CDNSOAMinimum / time.Second))
refreshSecondsStr := strconv.Itoa(int(CDNSOARefresh / time.Second))
retrySecondsStr := strconv.Itoa(int(CDNSOARetry / time.Second))
cdnSOA := &tc.SOA{
Admin: &admin,
ExpireSeconds: &expireSecondsStr,
MinimumSeconds: &minimumSecondsStr,
RefreshSeconds: &refreshSecondsStr,
RetrySeconds: &retrySecondsStr,
}
// Note the CRConfig omits acceptHTTP if it's true
falsePtr := false
protocol0 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: false, RedirectOnHTTPS: false}
protocol1 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTP: &falsePtr, AcceptHTTPS: true, RedirectOnHTTPS: false}
protocol2 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: true, RedirectOnHTTPS: false}
protocol3 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: true, RedirectOnHTTPS: true}
protocolDefault := protocol0
geoProvider0 := GeoProviderMaxmindStr
geoProvider1 := GeoProviderNeustarStr
geoProviderDefault := geoProvider0
serverParams, err := getServerProfileParams(cdn, tx)
if err != nil {
return nil, errors.New("getting deliveryservice parameters: " + err.Error())
}
dsParams, err := getDSParams(serverParams)
if err != nil {
return nil, errors.New("getting deliveryservice server parameters: " + err.Error())
}
dsmatchsets, dsdomains, err := getDSRegexesDomains(cdn, domain, tx)
if err != nil {
return nil, errors.New("getting regex matchsets: " + err.Error())
}
staticDNSEntries, err := getStaticDNSEntries(cdn, tx)
if err != nil {
return nil, errors.New("getting static DNS entries: " + err.Error())
}
q := `
SELECT d.anonymous_blocking_enabled,
d.consistent_hash_regex,
d.deep_caching_type,
d.initial_dispersion,
d.dns_bypass_cname,
d.dns_bypass_ip,
d.dns_bypass_ip6,
d.dns_bypass_ttl,
(SELECT ARRAY_AGG(name ORDER BY name)
FROM deliveryservice_consistent_hash_query_param
WHERE deliveryservice_id = d.id) AS query_keys,
d.routing_name,
d.ccr_dns_ttl AS ttl,
d.ecs_enabled,
d.regional_geo_blocking,
d.geo_limit,
d.geo_limit_countries,
d.geolimit_redirect_url,
d.geo_provider,
d.http_bypass_fqdn,
d.ipv6_routing_enabled,
d.max_dns_answers,
d.miss_lat,
d.miss_long,
p.name AS profile,
d.protocol,
(SELECT ARRAY_AGG(required_capability ORDER BY required_capability)
FROM deliveryservices_required_capability
WHERE deliveryservice_id = d.id) AS required_capabilities,
d.topology,
d.tr_request_headers,
d.tr_response_headers,
d.tr_response_headers,
t.name AS type,
d.xml_id
FROM deliveryservice AS d
INNER JOIN type AS t ON t.id = d.type
LEFT OUTER JOIN profile AS p ON p.id = d.profile
WHERE d.cdn_id = (select id FROM cdn WHERE name = $1)
AND d.active = true
`
q += fmt.Sprintf(" and t.name != '%s'", tc.DSTypeAnyMap)
rows, err := tx.Query(q, cdn)
if err != nil {
return nil, errors.New("querying deliveryservices: " + err.Error())
}
defer rows.Close()
for rows.Next() {
ds := tc.CRConfigDeliveryService{
ConsistentHashQueryParams: []string{},
Protocol: &tc.CRConfigDeliveryServiceProtocol{},
ResponseHeaders: map[string]string{},
Soa: cdnSOA,
TTLs: &tc.CRConfigTTL{},
}
missLat := sql.NullFloat64{}
missLon := sql.NullFloat64{}
protocol := sql.NullInt64{}
ttl := sql.NullInt64{}
geoProvider := sql.NullInt64{}
ttype := ""
geoLimit := sql.NullInt64{}
geoLimitCountries := sql.NullString{}
geoLimitRedirectURL := sql.NullString{}
dispersion := sql.NullInt64{}
geoBlocking := false
trRespHdrsStr := sql.NullString{}
xmlID := ""
maxDNSAnswers := sql.NullInt64{}
profile := sql.NullString{}
dnsBypassIP := sql.NullString{}
dnsBypassIP6 := sql.NullString{}
dnsBypassTTL := sql.NullInt64{}
dnsBypassCName := sql.NullString{}
httpBypassFQDN := sql.NullString{}
ip6RoutingEnabled := sql.NullBool{}
ecsEnabled := sql.NullBool{}
deepCachingType := sql.NullString{}
trRequestHeaders := sql.NullString{}
trResponseHeaders := sql.NullString{}
anonymousBlocking := false
consistentHashRegex := sql.NullString{}
err := rows.Scan(
&anonymousBlocking,
&consistentHashRegex,
&deepCachingType,
&dispersion,
&dnsBypassCName,
&dnsBypassIP,
&dnsBypassIP6,
&dnsBypassTTL,
pq.Array(&ds.ConsistentHashQueryParams),
&ds.RoutingName,
&ds.TTL,
&ecsEnabled,
&geoBlocking,
&geoLimit,
&geoLimitCountries,
&geoLimitRedirectURL,
&geoProvider,
&httpBypassFQDN,
&ip6RoutingEnabled,
&maxDNSAnswers,
&missLat,
&missLon,
&profile,
&protocol,
pq.Array(&ds.RequiredCapabilities),
&ds.Topology,
&trRequestHeaders,
&trRespHdrsStr,
&trResponseHeaders,
&ttype,
&xmlID,
)
if err != nil {
return nil, errors.New("scanning deliveryservice: " + err.Error())
}
// TODO prevent (lat XOR lon) in the Tx and UI
if missLat.Valid && missLon.Valid {
ds.MissLocation = &tc.CRConfigLatitudeLongitudeShort{Lat: missLat.Float64, Lon: missLon.Float64}
} else if missLat.Valid {
log.Warnln("delivery service " + xmlID + " has miss latitude but not longitude: omitting miss lat-lon from CRConfig")
} else if missLon.Valid {
log.Warnln("delivery service " + xmlID + " has miss longitude but not latitude: omitting miss lat-lon from CRConfig")
}
if ttl.Valid {
ttl := int(ttl.Int64)
ds.TTL = &ttl
}
protocolStr := getProtocolStr(ttype)
ds.Protocol = protocolDefault
if protocol.Valid {
switch protocol.Int64 {
case 0:
ds.Protocol = protocol0
case 1:
ds.Protocol = protocol1
case 2:
ds.Protocol = protocol2
case 3:
ds.Protocol = protocol3
}
}
ds.GeoLocationProvider = &geoProviderDefault
if geoProvider.Valid {
switch geoProvider.Int64 {
case 0:
ds.GeoLocationProvider = &geoProvider0
case 1:
ds.GeoLocationProvider = &geoProvider1
}
}
if ds.Protocol.AcceptHTTPS {
ds.SSLEnabled = true
}
if deepCachingType.Valid {
// TODO change to omit Valid check, default to the default DeepCachingType (NEVER). I'm pretty sure that's what should happen, but the Valid check emulates the old Perl CRConfig generation
t := tc.DeepCachingTypeFromString(deepCachingType.String)
ds.DeepCachingType = &t
}
ds.GeoLocationProvider = &geoProviderDefault
if matchsets, ok := dsmatchsets[xmlID]; ok {
ds.MatchSets = matchsets
} else {
log.Warnln("no regex matchsets for delivery service: " + xmlID)
}
if domains, ok := dsdomains[xmlID]; ok {
ds.Domains = domains
} else {
log.Warnln("no host regex for delivery service: " + xmlID)
}
switch geoLimit.Int64 { // No Valid check - default false and set countries, if null
case 0:
ds.CoverageZoneOnly = false
case 1:
ds.CoverageZoneOnly = true
if protocolStr == "HTTP" {
ds.GeoLimitRedirectURL = &geoLimitRedirectURL.String // No Valid check - empty string, if null
}
default:
ds.CoverageZoneOnly = false
if protocolStr == "HTTP" {
ds.GeoLimitRedirectURL = &geoLimitRedirectURL.String // No Valid check - empty string, if null
}
if geoLimitCountries.Valid {
for _, code := range strings.Split(geoLimitCountries.String, ",") {
ds.GeoEnabled = append(ds.GeoEnabled, tc.CRConfigGeoEnabled{CountryCode: strings.TrimSpace(code)})
}
}
}
nsSeconds := DefaultTLDTTLNS
soaSeconds := DefaultTLDTTLSOA
if profile.Valid {
if sval, ok := dsParams["tld.ttls.SOA"]; ok {
if val, err := strconv.Atoi(sval); err == nil {
soaSeconds = time.Duration(val) * time.Second
} else {
log.Errorln("delivery service " + xmlID + " profile " + profile.String + " param tld.ttls.SOA '" + sval + "' not a number - skipping")
}
}
if sval, ok := dsParams["tld.ttls.NS"]; ok {
if val, err := strconv.Atoi(sval); err == nil {
nsSeconds = time.Duration(val) * time.Second
} else {
log.Errorln("delivery service " + xmlID + " profile " + profile.String + " param tld.ttls.NS '" + sval + "' not a number - skipping")
}
}
}
nsSecondsStr := strconv.Itoa(int(nsSeconds / time.Second))
soaSecondsStr := strconv.Itoa(int(soaSeconds / time.Second))
ttlStr := ""
if ds.TTL != nil {
ttlStr = strconv.Itoa(*ds.TTL)
}
ds.TTLs = &tc.CRConfigTTL{
ASeconds: &ttlStr,
AAAASeconds: &ttlStr,
NSSeconds: &nsSecondsStr,
SOASeconds: &soaSecondsStr,
}
if protocolStr == "DNS" {
bypassDest := &tc.CRConfigBypassDestination{}
if dnsBypassIP.String != "" {
bypassDest.IP = &dnsBypassIP.String
}
if dnsBypassIP6.String != "" {
bypassDest.IP6 = &dnsBypassIP6.String
}
if dnsBypassTTL.Valid {
i := int(dnsBypassTTL.Int64)
bypassDest.TTL = &i
}
if dnsBypassCName.Valid && dnsBypassCName.String != "" {
bypassDest.CName = &dnsBypassCName.String
}
if *bypassDest != (tc.CRConfigBypassDestination{}) {
if ds.BypassDestination == nil {
ds.BypassDestination = map[string]*tc.CRConfigBypassDestination{}
}
ds.BypassDestination["DNS"] = bypassDest
}
if maxDNSAnswers.Valid {
i := int(maxDNSAnswers.Int64)
ds.MaxDNSIPsForLocation = &i
}
} else if protocolStr == "HTTP" {
if httpBypassFQDN.String != "" {
if ds.BypassDestination == nil {
ds.BypassDestination = map[string]*tc.CRConfigBypassDestination{}
}
hostPort := strings.Split(httpBypassFQDN.String, ":")
bypass := &tc.CRConfigBypassDestination{FQDN: &hostPort[0]}
if len(hostPort) > 1 {
bypass.Port = &hostPort[1]
}
ds.BypassDestination["HTTP"] = bypass
}
geoBlockingStr := "false"
if geoBlocking {
geoBlockingStr = "true"
}
ds.RegionalGeoBlocking = &geoBlockingStr
anonymousBlockingStr := "false"
if anonymousBlocking {
anonymousBlockingStr = "true"
}
ds.AnonymousBlockingEnabled = &anonymousBlockingStr
if dispersion.Valid {
ds.Dispersion = &tc.CRConfigDispersion{Limit: int(dispersion.Int64), Shuffled: true}
}
}
if consistentHashRegex.Valid && consistentHashRegex.String != "" {
ds.ConsistentHashRegex = &consistentHashRegex.String
}
ds.IP6RoutingEnabled = &ip6RoutingEnabled.Bool // No Valid check, false if null
ds.EcsEnabled = &ecsEnabled.Bool // No Valid check, false if null
if trResponseHeaders.Valid && trResponseHeaders.String != "" {
trResponseHeaders.String = strings.Replace(trResponseHeaders.String, "__RETURN__", "\n", -1)
hdrs := strings.Split(trResponseHeaders.String, "\n")
for _, hdr := range hdrs {
nameVal := strings.Split(hdr, `:`)
name := strings.TrimSpace(nameVal[0])
val := ""
if len(nameVal) > 1 {
val = strings.Trim(nameVal[1], " \n\"")
}
ds.ResponseHeaders[name] = val
}
}
if trRequestHeaders.Valid && trRequestHeaders.String != "" {
trRequestHeaders.String = strings.Replace(trRequestHeaders.String, "__RETURN__", "\n", -1)
hdrs := strings.Split(trRequestHeaders.String, "\n")
for _, hdr := range hdrs {
nameVal := strings.Split(hdr, `:`)
name := strings.TrimSpace(nameVal[0])
ds.RequestHeaders = append(ds.RequestHeaders, name)
}
}
ds.StaticDNSEntries = staticDNSEntries[tc.DeliveryServiceName(xmlID)]
dses[xmlID] = ds
}
if err := rows.Err(); err != nil {
return nil, errors.New("iterating deliveryservice rows: " + err.Error())
}
return dses, nil
}
func getStaticDNSEntries(cdn string, tx *sql.Tx) (map[tc.DeliveryServiceName][]tc.CRConfigStaticDNSEntry, error) {
entries := map[tc.DeliveryServiceName][]tc.CRConfigStaticDNSEntry{}
q := `
select d.xml_id as ds, e.host as name, e.ttl, e.address as value, t.name as type
from staticdnsentry as e
inner join deliveryservice as d on d.id = e.deliveryservice
inner join type as t on t.id = e.type
where d.cdn_id = (select id from cdn where name = $1)
and d.active = true
`
rows, err := tx.Query(q, cdn)
if err != nil {
return nil, errors.New("querying static DNS entries: " + err.Error())
}
defer rows.Close()
for rows.Next() {
ds := ""
name := ""
ttl := 0
value := ""
ttype := ""
if err := rows.Scan(&ds, &name, &ttl, &value, &ttype); err != nil {
return nil, errors.New("scanning static DNS entries: " + err.Error())
}
ttype = strings.Replace(ttype, "_RECORD", "", -1)
entries[tc.DeliveryServiceName(ds)] = append(entries[tc.DeliveryServiceName(ds)], tc.CRConfigStaticDNSEntry{Name: name, TTL: ttl, Value: value, Type: ttype})
}
return entries, nil
}
func getProtocolStr(dsType string) string {
if strings.HasPrefix(dsType, "DNS") {
return "DNS"
}
return "HTTP"
}
func getDSRegexesDomains(cdn string, domain string, tx *sql.Tx) (map[string][]*tc.MatchSet, map[string][]string, error) {
dsmatchsets := map[string][]*tc.MatchSet{}
domains := map[string][]string{}
patternToHostReplacer := strings.NewReplacer(`\`, ``, `.*`, ``, `.`, ``)
q := `
select r.pattern, t.name as type, dt.name as dstype, COALESCE(dr.set_number, 0), d.xml_id as dsname
from regex as r
inner join deliveryservice_regex as dr on r.id = dr.regex
inner join deliveryservice as d on d.id = dr.deliveryservice
inner join type as t on t.id = r.type
inner join type as dt on dt.id = d.type
where d.cdn_id = (select id from cdn where name = $1)
and d.active = true
order by dr.set_number asc
`
rows, err := tx.Query(q, cdn)
if err != nil {
return nil, nil, errors.New("querying deliveryservices: " + err.Error())
}
defer rows.Close()
// a map to keep track of the ds name and the last order of the regex for that ds
dsNameOrderMap := make(map[string]int)
for rows.Next() {
pattern := ""
ttype := ""
dstype := ""
setnum := 0
dsname := ""
if err := rows.Scan(&pattern, &ttype, &dstype, &setnum, &dsname); err != nil {
return nil, nil, errors.New("scanning deliveryservice regexes: " + err.Error())
}
if _, ok := dsNameOrderMap[dsname]; !ok {
dsNameOrderMap[dsname] = 0
} else {
dsNameOrderMap[dsname] = dsNameOrderMap[dsname] + 1
}
protocolStr := getProtocolStr(dstype)
dsmatchsets[dsname] = append(dsmatchsets[dsname], nil)
matchType := ""
switch ttype {
case "HOST_REGEXP":
matchType = "HOST"
case "PATH_REGEXP":
matchType = "PATH"
case "HEADER_REGEXP":
matchType = "HEADER"
default:
log.Infoln("unknown delivery service '" + dsname + "' regex type: " + ttype + " - skipping") // info, not warn or err, because this is normal for STEERING_REGEXP (and maybe others in the future)
continue
}
// If there are gaps between two or more DS regex orders, do not add these missing orders in the final list.
// Instead, skip over them and add the next regex with a valid order.
if dsmatchsets[dsname][dsNameOrderMap[dsname]] == nil {
dsmatchsets[dsname][dsNameOrderMap[dsname]] = &tc.MatchSet{}
}
matchset := dsmatchsets[dsname][dsNameOrderMap[dsname]]
matchset.Protocol = protocolStr
matchset.MatchList = append(matchset.MatchList, tc.MatchList{MatchType: matchType, Regex: pattern})
if ttype == "HOST_REGEXP" && setnum == 0 {
domains[dsname] = append(domains[dsname], patternToHostReplacer.Replace(pattern)+"."+domain)
}
}
return dsmatchsets, domains, nil
}
// getDSParams takes a map[serverProfile][paramName]paramVal and returns a map[paramName]paramVal.
// The returned map of parameter values is used for DS settings for the current CDN.
// If any profiles have conflicting parameters, an error is returned.
func getDSParams(serverParams map[string]map[string]string) (map[string]string, error) {
dsParamNames := map[string]struct{}{
"tld.soa.admin": struct{}{},
"tld.soa.expire": struct{}{},
"tld.soa.minimum": struct{}{},
"tld.soa.refresh": struct{}{},
"tld.soa.retry": struct{}{},
"tld.ttls.SOA": struct{}{},
"tld.ttls.NS": struct{}{},
"LogRequestHeaders": struct{}{},
}
dsParams := map[string]string{}
dsParamsOriginalProfile := map[string]string{} // map[paramName]profile - used exclusively for the error message
for profile, profileParams := range serverParams {
for paramName, _ := range dsParamNames {
paramVal, profileHasParam := profileParams[paramName]
if !profileHasParam {
continue
}
if dsParamVal, ok := dsParams[paramName]; ok && dsParamVal != paramVal {
return nil, errors.New("profiles " + profile + " and " + dsParamsOriginalProfile[paramName] + " have conflicting values '" + paramVal + "' and '" + dsParamVal + "'")
}
dsParams[paramName] = paramVal
dsParamsOriginalProfile[paramName] = profile
}
}
return dsParams, nil
}
// getDSProfileParams returns a map[dsname]map[paramname]paramvalue
func getServerProfileParams(cdn string, tx *sql.Tx) (map[string]map[string]string, error) {
q := `
select parameter.name, parameter.value, profile.name as profile
from profile
inner join profile_parameter as pp on pp.profile = profile.id
inner join parameter on parameter.id = pp.parameter
where profile.id in (select profile from server where server.cdn_id = (select id from cdn where name = $1))
`
rows, err := tx.Query(q, cdn)
if err != nil {
return nil, errors.New("querying deliveryservices: " + err.Error())
}
defer rows.Close()
params := map[string]map[string]string{}
debugCount := 0
for rows.Next() {
debugCount++
name := ""
val := ""
profile := ""
if err := rows.Scan(&name, &val, &profile); err != nil {
return nil, errors.New("scanning deliveryservice parameters: " + err.Error())
}
if _, ok := params[profile]; !ok {
params[profile] = map[string]string{}
}
params[profile][name] = val
}
if err := rows.Err(); err != nil {
return nil, errors.New("iterating deliveryservice parameter rows: " + err.Error())
}
return params, nil
}