Format Go with gofmt, then golines -m 77 (closes #215)
check / check (push) Waiting to run

script/fmt now runs golines -m 77 after gofmt, repeating golines until
it changes nothing, since one pass can leave a line the next splits.
script/fmt-check fails when either would change a file.
script/bootstrap installs golines v0.13.0 with go install, pinned to
its commit, and checks the installed version with go version -m.

The tree is reformatted. The longer lines this makes are fixed by
hand: nolint directives moved onto their own line above the statement,
one string split, and 20 functions over funlen's 80 lines shortened or
split, RunWAF and limitPercentages among them.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 14:50:46 +00:00
1 parent f9bec5146b
commit a8b24a843b
117 files changed
+9441 -2670

No files matched your search

+238 -108
View File
@@ -374,13 +374,16 @@ var (
"is not a header name such as accept-language")
errHeaderTakenOut = errors.New(
"is taken out of every request by Go's HTTP server, so it can never " +
"be logged")
"be logged",
)
errOnBothLists = errors.New("is in SWW_TAG_DENY too")
errNotLookupSource = errors.New("is not geojs, file or off")
errNeedsLookups = errors.New("it needs each client looked up")
errNeedsDBPath = errors.New("it names the file to look clients up in")
errDBPathUnused = errors.New("only file reads it")
errOver64K = errors.New(
errNeedsDBPath = errors.New(
"it names the file to look clients up in",
)
errDBPathUnused = errors.New("only file reads it")
errOver64K = errors.New(
"is more than 64K, the most waf gets of a body")
errNotCommand = errors.New("is not rproxy or waf")
errNotDurationAboveZero = errors.New(
@@ -414,16 +417,20 @@ var (
)
errSetupRuleID = errors.New(
"is from 900000 to 900999, the ids of the rules that set the Core Rule Set " +
"up and of sww's own, which cannot be switched off")
"up and of sww's own, which cannot be switched off",
)
errNotBoolean = errors.New("is not true or false")
errNotLogRemoteURL = errors.New(
"is not syslog+udp, syslog+tcp or syslog+tls with a host and a port, " +
"and nothing more, such as syslog+tls://logs.example:6514",
)
errNoCertificate = errors.New("holds no PEM certificate")
errNotFacility = errors.New("is not a syslog facility such as local0 or daemon")
errNotAppName = errors.New(
"is not 1 to 48 printable ASCII characters without a space, such as blog")
errNotFacility = errors.New(
"is not a syslog facility such as local0 or daemon",
)
errNotAppName = errors.New(
"is not 1 to 48 printable ASCII characters without a space, such as blog",
)
errSetTwice = errors.New("set only one of them")
errNotWebhookURL = errors.New(
"is not an http or https URL without a user or a fragment, " +
@@ -436,7 +443,8 @@ var (
)
errNotAlertEvent = errors.New(
"is not ban, permanent_ban, core_rule_set_match, anomaly, reputation_hit, " +
"source_failure or file_error")
"source_failure or file_error",
)
errNotNumberOrOff = errors.New(
"is not a whole number above zero, such as 60, or off",
)
@@ -452,8 +460,10 @@ var (
"is not an http or https URL without a user or a fragment, " +
"such as https://www.spamhaus.org/drop/drop.txt")
errInBlocklistURLs = errors.New("is in SWW_BLOCKLIST_URLS too")
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
errNotAction = errors.New(
errNotAnHourOrMore = errors.New(
"is not a duration of 1h or more, such as 24h",
)
errNotAction = errors.New(
"is not deny, limit:<percent> such as limit:25, or log")
errNotZone = errors.New(
"is not a DNS zone such as dnsbl.dronebl.org",
@@ -473,8 +483,10 @@ var (
"is not an http or https URL without a user or a fragment, " +
"such as https://routewatch.example")
errNeedsLAPIKey = errors.New("the engine answers no request without it")
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
errAnotherList = errors.New(
errLAPIKeyUnused = errors.New(
"it is sent only to the engine at that URL",
)
errAnotherList = errors.New(
"is in SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too")
errFetchedOtherwise = errors.New("is in SWW_BLOCKLIST_URLS, is " +
"SWW_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too")
@@ -484,8 +496,10 @@ var (
errNotTagPercentItem = errors.New(
"is not a tag, : and a percentage, such as vpn:50 or asn=14061:25")
errNotTagPercent = errors.New("is not a percentage from 1 to 99")
errNotASNTag = errors.New("is not asn= and a number, such as asn=14061")
errNotPrefixTag = errors.New(
errNotASNTag = errors.New(
"is not asn= and a number, such as asn=14061",
)
errNotPrefixTag = errors.New(
"is not prefix= and a netblock, such as prefix=192.0.2.0/24")
errExploitTag = errors.New(
"cannot lower limits: the rate limits count a request before its " +
@@ -497,7 +511,8 @@ var (
"asn= or country= tag, a dnsbl= tag of a zone in SWW_DNSBL_ZONES, " +
"a prefix= tag with SWW_ROUTEWATCH_URL set, abuseipdb with " +
"SWW_ABUSEIPDB_KEY set, or exploitsignature with rule files or the " +
"Core Rule Set on")
"Core Rule Set on",
)
)
// FromEnvironment reads the settings with lookupEnv, normally
@@ -512,83 +527,158 @@ func FromEnvironment(
) (*Config, error) {
env := &environment{lookupEnv: lookupEnv}
cfg := &Config{
ListenAddr: env.address("SWW_LISTEN_ADDR"),
InstanceName: env.instanceName(),
Observe: env.observe("SWW_MODE", "enforce"),
IPv6GroupPrefix: env.ipv6GroupPrefix("SWW_IPV6_GROUP_PREFIX", "64"),
MaxTrackedClients: env.numberNotOff("SWW_MAX_TRACKED_CLIENTS", "20000"),
AllowNets: env.netblocks("SWW_ALLOW_NETS", ""),
RateLimitExemptNets: env.netblocks("SWW_RATE_LIMIT_EXEMPT_NETS", ""),
DenyNets: env.netblocks("SWW_DENY_NETS", ""),
RateLimitPerMinute: env.count("SWW_RATE_LIMIT_PER_MINUTE", "120"),
RateLimitPerHour: env.count("SWW_RATE_LIMIT_PER_HOUR", "1500"),
RateLimitPerDay: env.count("SWW_RATE_LIMIT_PER_DAY", "6000"),
RateLimitExemptPaths: env.pathPrefixes("SWW_RATE_LIMIT_EXEMPT_PATHS", ""),
BytesLimitPerMinute: env.size("SWW_BYTES_LIMIT_PER_MINUTE", "off"),
BytesLimitPerHour: env.size("SWW_BYTES_LIMIT_PER_HOUR", "50G"),
BytesLimitPerDay: env.size("SWW_BYTES_LIMIT_PER_DAY", "100G"),
BytesCount: env.bytesCount("SWW_BYTES_COUNT", "both"),
LookupSource: env.lookupSource("SWW_LOOKUP_SOURCE", "geojs"),
LookupDBPath: env.value("SWW_LOOKUP_DB_PATH", ""),
LookupTimeout: env.durationNotOff("SWW_LOOKUP_TIMEOUT", "800ms"),
AddLookupHeaders: env.boolean("SWW_ADD_LOOKUP_HEADERS", "false"),
ListenAddr: env.address("SWW_LISTEN_ADDR"),
InstanceName: env.instanceName(),
Observe: env.observe("SWW_MODE", "enforce"),
IPv6GroupPrefix: env.ipv6GroupPrefix(
"SWW_IPV6_GROUP_PREFIX",
"64",
),
MaxTrackedClients: env.numberNotOff(
"SWW_MAX_TRACKED_CLIENTS",
"20000",
),
AllowNets: env.netblocks("SWW_ALLOW_NETS", ""),
RateLimitExemptNets: env.netblocks(
"SWW_RATE_LIMIT_EXEMPT_NETS",
"",
),
DenyNets: env.netblocks("SWW_DENY_NETS", ""),
RateLimitPerMinute: env.count("SWW_RATE_LIMIT_PER_MINUTE", "120"),
RateLimitPerHour: env.count("SWW_RATE_LIMIT_PER_HOUR", "1500"),
RateLimitPerDay: env.count("SWW_RATE_LIMIT_PER_DAY", "6000"),
RateLimitExemptPaths: env.pathPrefixes(
"SWW_RATE_LIMIT_EXEMPT_PATHS",
"",
),
BytesLimitPerMinute: env.size("SWW_BYTES_LIMIT_PER_MINUTE", "off"),
BytesLimitPerHour: env.size("SWW_BYTES_LIMIT_PER_HOUR", "50G"),
BytesLimitPerDay: env.size("SWW_BYTES_LIMIT_PER_DAY", "100G"),
BytesCount: env.bytesCount("SWW_BYTES_COUNT", "both"),
LookupSource: env.lookupSource("SWW_LOOKUP_SOURCE", "geojs"),
LookupDBPath: env.value("SWW_LOOKUP_DB_PATH", ""),
LookupTimeout: env.durationNotOff(
"SWW_LOOKUP_TIMEOUT",
"800ms",
),
AddLookupHeaders: env.boolean("SWW_ADD_LOOKUP_HEADERS", "false"),
ExclusivelyAllowedCountries: env.countries(
"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
TagLists: env.tagLists("SWW_TAG_LISTS"),
TagDeny: env.tags("SWW_TAG_DENY"),
TagLimitPercent: env.tagPercents("SWW_TAG_LIMIT_PERCENT"),
RouteWatchURL: env.routeWatchURL("SWW_ROUTEWATCH_URL"),
ASNBytesPercent: env.percents("SWW_ASN_BYTES_PERCENT", ParseASN),
CountryBytesPercent: env.percents("SWW_COUNTRY_BYTES_PERCENT", parseCountry),
UnknownLimitPercent: env.percent("SWW_UNKNOWN_LIMIT_PERCENT", "100"),
ASNLimitPercentURL: env.listURL("SWW_ASN_LIMIT_PERCENT_URL"),
BlocklistURLs: env.listURLs("SWW_BLOCKLIST_URLS"),
BlocklistRefresh: env.refresh("SWW_BLOCKLIST_REFRESH", "24h"),
DNSBLZones: env.zones("SWW_DNSBL_ZONES"),
DNSBLResolver: env.resolver("SWW_DNSBL_RESOLVER"),
AbuseIPDBKey: env.secret("SWW_ABUSEIPDB_KEY"),
AbuseIPDBMinScore: env.percent("SWW_ABUSEIPDB_MIN_SCORE", "75"),
AbuseIPDBDailyBudget: env.numberNotOff("SWW_ABUSEIPDB_DAILY_BUDGET", "900"),
ReputationCacheTTL: env.durationNotOff("SWW_REPUTATION_CACHE_TTL", "24h"),
ReputationTimeout: env.durationNotOff("SWW_REPUTATION_TIMEOUT", "2s"),
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWW_CROWDSEC_LAPI_URL"),
CrowdSecKey: env.secret("SWW_CROWDSEC_LAPI_KEY"),
BanResponse: env.banResponse("SWW_BAN_RESPONSE", "403"),
LimitBanDuration: env.durationNotOff("SWW_LIMIT_BAN_DURATION", "1h"),
LimitBanRepeatWindow: env.durationNotOff("SWW_LIMIT_BAN_REPEAT_WINDOW", "24h"),
MaxBanDuration: env.durationNotOff("SWW_MAX_BAN_DURATION", "7d"),
AttackBanDuration: env.durationNotOff("SWW_ATTACK_BAN_DURATION", "7d"),
MaxBans: env.numberNotOff("SWW_MAX_BANS", "5000"),
BanScopeV4Prefix: env.v4Prefix("SWW_BAN_SCOPE_V4_PREFIX", "32"),
StateDir: env.absolutePath("SWW_STATE_DIR", "/var/lib/sww"),
StateWriteDelay: env.durationNotOff("SWW_STATE_WRITE_DELAY", "10s"),
StateCounterInterval: env.durationNotOff("SWW_STATE_COUNTER_INTERVAL", "15m"),
CacheDir: env.absolutePath("SWW_CACHE_DIR", "/var/cache/sww"),
LogRequestHeaders: env.headerNames("SWW_LOG_REQUEST_HEADERS",
"accept,accept-language,accept-encoding,content-type,origin,range"),
LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"),
AdminToken: env.token("SWW_ADMIN_TOKEN"),
MetricsToken: env.token("SWW_METRICS_TOKEN"),
MetricsTopN: env.numberNotOff("SWW_METRICS_TOP_N", "50"),
RulesDir: env.value("SWW_RULES_DIR", "/etc/sww/rules.d"),
RulesEnabled: env.boolean("SWW_RULES_ENABLED", "true"),
WAFMode: env.wafMode("SWW_WAF_MODE", WAFModeBlock),
WAFParanoiaLevel: env.paranoiaLevel("SWW_WAF_PARANOIA_LEVEL", "1"),
WAFAnomalyThreshold: env.numberOrOff("SWW_WAF_ANOMALY_THRESHOLD", "5"),
TagLists: env.tagLists("SWW_TAG_LISTS"),
TagDeny: env.tags("SWW_TAG_DENY"),
TagLimitPercent: env.tagPercents("SWW_TAG_LIMIT_PERCENT"),
RouteWatchURL: env.routeWatchURL("SWW_ROUTEWATCH_URL"),
ASNBytesPercent: env.percents(
"SWW_ASN_BYTES_PERCENT",
ParseASN,
),
CountryBytesPercent: env.percents(
"SWW_COUNTRY_BYTES_PERCENT",
parseCountry,
),
UnknownLimitPercent: env.percent(
"SWW_UNKNOWN_LIMIT_PERCENT",
"100",
),
ASNLimitPercentURL: env.listURL("SWW_ASN_LIMIT_PERCENT_URL"),
BlocklistURLs: env.listURLs("SWW_BLOCKLIST_URLS"),
BlocklistRefresh: env.refresh("SWW_BLOCKLIST_REFRESH", "24h"),
DNSBLZones: env.zones("SWW_DNSBL_ZONES"),
DNSBLResolver: env.resolver("SWW_DNSBL_RESOLVER"),
AbuseIPDBKey: env.secret("SWW_ABUSEIPDB_KEY"),
AbuseIPDBMinScore: env.percent("SWW_ABUSEIPDB_MIN_SCORE", "75"),
AbuseIPDBDailyBudget: env.numberNotOff(
"SWW_ABUSEIPDB_DAILY_BUDGET",
"900",
),
ReputationCacheTTL: env.durationNotOff(
"SWW_REPUTATION_CACHE_TTL",
"24h",
),
ReputationTimeout: env.durationNotOff(
"SWW_REPUTATION_TIMEOUT",
"2s",
),
CrowdSecDecisionsURL: env.crowdSecDecisionsURL(
"SWW_CROWDSEC_LAPI_URL",
),
CrowdSecKey: env.secret("SWW_CROWDSEC_LAPI_KEY"),
BanResponse: env.banResponse("SWW_BAN_RESPONSE", "403"),
LimitBanDuration: env.durationNotOff(
"SWW_LIMIT_BAN_DURATION",
"1h",
),
LimitBanRepeatWindow: env.durationNotOff(
"SWW_LIMIT_BAN_REPEAT_WINDOW",
"24h",
),
MaxBanDuration: env.durationNotOff(
"SWW_MAX_BAN_DURATION",
"7d",
),
AttackBanDuration: env.durationNotOff(
"SWW_ATTACK_BAN_DURATION",
"7d",
),
MaxBans: env.numberNotOff("SWW_MAX_BANS", "5000"),
BanScopeV4Prefix: env.v4Prefix("SWW_BAN_SCOPE_V4_PREFIX", "32"),
StateDir: env.absolutePath(
"SWW_STATE_DIR",
"/var/lib/sww",
),
StateWriteDelay: env.durationNotOff(
"SWW_STATE_WRITE_DELAY",
"10s",
),
StateCounterInterval: env.durationNotOff(
"SWW_STATE_COUNTER_INTERVAL",
"15m",
),
CacheDir: env.absolutePath(
"SWW_CACHE_DIR",
"/var/cache/sww",
),
LogRequestHeaders: env.headerNames(
"SWW_LOG_REQUEST_HEADERS",
"accept,accept-language,accept-encoding,content-type,origin,range",
),
LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"),
AdminToken: env.token("SWW_ADMIN_TOKEN"),
MetricsToken: env.token("SWW_METRICS_TOKEN"),
MetricsTopN: env.numberNotOff("SWW_METRICS_TOP_N", "50"),
RulesDir: env.value("SWW_RULES_DIR", "/etc/sww/rules.d"),
RulesEnabled: env.boolean("SWW_RULES_ENABLED", "true"),
WAFMode: env.wafMode("SWW_WAF_MODE", WAFModeBlock),
WAFParanoiaLevel: env.paranoiaLevel(
"SWW_WAF_PARANOIA_LEVEL",
"1",
),
WAFAnomalyThreshold: env.numberOrOff(
"SWW_WAF_ANOMALY_THRESHOLD",
"5",
),
WAFDisabledRules: env.ruleIDs("SWW_WAF_DISABLED_RULES",
"920340,920420,920440,920640,930130,930140"),
WAFExemptPaths: env.pathPrefixes("SWW_WAF_EXEMPT_PATHS", ""),
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
CoreRuleSetExclusionsFile: env.filePath(
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
LogRemoteTLSCAs: env.certificates("SWW_LOG_REMOTE_TLS_CA_FILE"),
LogRemoteBuffer: env.numberNotOff("SWW_LOG_REMOTE_BUFFER", "10000"),
LogRemoteFacility: env.facility("SWW_LOG_REMOTE_FACILITY", "local0"),
AlertWebhookURL: env.webhookURL("SWW_ALERT_WEBHOOK_URL"),
AlertWebhookHeaders: env.webhookHeaders("SWW_ALERT_WEBHOOK_HEADERS"),
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
LogRemoteTLSCAs: env.certificates("SWW_LOG_REMOTE_TLS_CA_FILE"),
LogRemoteBuffer: env.numberNotOff(
"SWW_LOG_REMOTE_BUFFER",
"10000",
),
LogRemoteFacility: env.facility(
"SWW_LOG_REMOTE_FACILITY",
"local0",
),
AlertWebhookURL: env.webhookURL("SWW_ALERT_WEBHOOK_URL"),
AlertWebhookHeaders: env.webhookHeaders(
"SWW_ALERT_WEBHOOK_HEADERS",
),
AlertSlackWebhookURL: env.webhookURL("SWW_ALERT_SLACK_WEBHOOK_URL"),
AlertNtfyURL: env.webhookURL("SWW_ALERT_NTFY_URL"),
AlertNtfyToken: env.secret("SWW_ALERT_NTFY_TOKEN"),
@@ -721,7 +811,8 @@ func (e *environment) lookup(name string) (string, bool) {
e.settings = append(e.settings, slog.String(fileName, path))
contents, err := os.ReadFile(path) //nolint:gosec // a file the admin names
//nolint:gosec // a file the admin names
contents, err := os.ReadFile(path)
if err != nil {
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
@@ -1071,7 +1162,10 @@ func (e *environment) checkLookupDBPath(cfg *Config) {
"is file while SWW_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
e.check("SWW_LOOKUP_DB_PATH", fmt.Errorf(
"is set while SWW_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
"is set while SWW_LOOKUP_SOURCE is %s; %w",
cfg.LookupSource,
errDBPathUnused,
))
}
}
@@ -1120,8 +1214,11 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
{"SWW_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
} {
if setting.set {
e.check(setting.name, fmt.Errorf("is set while SWW_LOOKUP_SOURCE is off; %w",
errNeedsLookups))
e.check(
setting.name,
fmt.Errorf("is set while SWW_LOOKUP_SOURCE is off; %w",
errNeedsLookups),
)
}
}
}
@@ -1144,14 +1241,21 @@ func (e *environment) checkCrowdSec(cfg *Config) {
switch {
case decisionsURL != "" && cfg.CrowdSecKey == "":
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf(
"is set while SWW_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
"is set while SWW_CROWDSEC_LAPI_KEY is unset; %w",
errNeedsLAPIKey,
))
case decisionsURL == "" && cfg.CrowdSecKey != "":
e.check("SWW_CROWDSEC_LAPI_KEY", fmt.Errorf(
"is set while SWW_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
"is set while SWW_CROWDSEC_LAPI_URL is unset; %w",
errLAPIKeyUnused,
))
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
decisionsURL == cfg.ASNLimitPercentURL):
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
decisionsURL, errAnotherList))
e.check(
"SWW_CROWDSEC_LAPI_URL",
fmt.Errorf("gives the decision list %q, which %w",
decisionsURL, errAnotherList),
)
}
}
@@ -1161,7 +1265,10 @@ func (e *environment) checkTagLists(cfg *Config) {
for _, list := range cfg.TagLists {
if slices.Contains(cfg.BlocklistURLs, list.URL) ||
list.URL == cfg.ASNLimitPercentURL || list.URL == cfg.CrowdSecDecisionsURL {
e.check("SWW_TAG_LISTS", fmt.Errorf("%q %w", list.URL, errFetchedOtherwise))
e.check(
"SWW_TAG_LISTS",
fmt.Errorf("%q %w", list.URL, errFetchedOtherwise),
)
}
}
}
@@ -1200,7 +1307,8 @@ func requestCanGetTag(cfg *Config, tag string) bool {
}
if tag == ExploitSignatureTag {
coreRuleSetOn := cfg.WAFMode != WAFModeOff && cfg.WAFAnomalyThreshold != 0
coreRuleSetOn := cfg.WAFMode != WAFModeOff &&
cfg.WAFAnomalyThreshold != 0
return cfg.RulesEnabled || coreRuleSetOn
}
@@ -1215,15 +1323,17 @@ func requestCanGetTag(cfg *Config, tag string) bool {
zone, isZoneTag := strings.CutPrefix(tag, DNSBLTagPrefix)
return isZoneTag && slices.ContainsFunc(cfg.DNSBLZones, func(each string) bool {
return strings.ToLower(MaskZoneKey(each)) == zone
})
return isZoneTag &&
slices.ContainsFunc(cfg.DNSBLZones, func(each string) bool {
return strings.ToLower(MaskZoneKey(each)) == zone
})
}
// TagsNeedLookup reports whether SWW_TAG_DENY or SWW_TAG_LIMIT_PERCENT
// names an asn= or country= tag.
func (c *Config) TagsNeedLookup() bool {
return slices.ContainsFunc(c.TagDeny, isLookupTag) || tagLimitNeedsLookups(c)
return slices.ContainsFunc(c.TagDeny, isLookupTag) ||
tagLimitNeedsLookups(c)
}
// tagLimitNeedsLookups reports whether SWW_TAG_LIMIT_PERCENT names an asn=
@@ -1240,7 +1350,8 @@ func tagLimitNeedsLookups(cfg *Config) bool {
// isLookupTag reports whether tag is an asn= or country= tag.
func isLookupTag(tag string) bool {
return strings.HasPrefix(tag, ASNTagPrefix) || strings.HasPrefix(tag, CountryTagPrefix)
return strings.HasPrefix(tag, ASNTagPrefix) ||
strings.HasPrefix(tag, CountryTagPrefix)
}
// headerNames reads a setting that is a list of header names, and
@@ -1471,8 +1582,11 @@ func (e *environment) appName(
case set:
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
case sending:
e.check(name, fmt.Errorf("is unset, and SWW_INSTANCE_NAME %q, its default, %w",
value, errNotAppName))
e.check(
name,
fmt.Errorf("is unset, and SWW_INSTANCE_NAME %q, its default, %w",
value, errNotAppName),
)
}
return value
@@ -1488,7 +1602,9 @@ func (e *environment) checkInstanceNameForNtfy(
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
e.check("SWW_INSTANCE_NAME", fmt.Errorf(
"%q %w, and is sent to ntfy in a header while SWW_ALERT_NTFY_URL is set",
instanceName, errControlCharacter))
instanceName,
errControlCharacter,
))
}
}
@@ -2031,7 +2147,11 @@ func parseTags(value string) ([]string, error) {
}
if slices.Contains(tags, tag) {
return nil, fmt.Errorf("%q %w", MaskZoneKey(item), errListedTwice)
return nil, fmt.Errorf(
"%q %w",
MaskZoneKey(item),
errListedTwice,
)
}
tags = append(tags, tag)
@@ -2096,7 +2216,11 @@ func parseTagPercents(value string) (map[string]int64, error) {
for _, item := range items {
colon := strings.LastIndex(item, ":")
if colon <= 0 {
return nil, fmt.Errorf("%q %w", MaskZoneKey(item), errNotTagPercentItem)
return nil, fmt.Errorf(
"%q %w",
MaskZoneKey(item),
errNotTagPercentItem,
)
}
tag, err := parseTag(item[:colon])
@@ -2106,7 +2230,11 @@ func parseTagPercents(value string) (map[string]int64, error) {
percent, err := strconv.ParseInt(item[colon+1:], 10, 64)
if err != nil || percent < 1 || percent > 99 {
return nil, fmt.Errorf("%q %w", MaskZoneKey(item[colon+1:]), errNotTagPercent)
return nil, fmt.Errorf(
"%q %w",
MaskZoneKey(item[colon+1:]),
errNotTagPercent,
)
}
if tag == ExploitSignatureTag {
@@ -2257,7 +2385,8 @@ func parseUpstreamURL(value string) (*url.URL, error) {
}
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
upstream.Hostname() != "" && upstream.User == nil &&
upstream.Hostname() != "" &&
upstream.User == nil &&
upstream.Opaque == "" &&
(upstream.Path == "" || upstream.Path == "/") &&
upstream.RawQuery == "" &&
@@ -2350,7 +2479,8 @@ func parseWebhookURL(value string) (*url.URL, string, error) {
func isHTTPURL(u *url.URL) bool {
port, err := strconv.ParseUint(u.Port(), 10, 16)
return (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" &&
return (u.Scheme == "http" || u.Scheme == "https") &&
u.Hostname() != "" &&
(u.Port() == "" || (err == nil && port != 0)) &&
u.User == nil &&
u.Opaque == "" &&
File diff suppressed because it is too large. Load diff
+33 -13
View File
@@ -55,7 +55,8 @@ var (
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errNotMillisecondsOrOff = errors.New(
"is not a whole number of milliseconds above zero, such as 60000, or off")
"is not a whole number of milliseconds above zero, such as 60000, or off",
)
)
// RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
@@ -63,19 +64,36 @@ var (
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
// invalid is an error that names it. Since rproxy never touches disk, its
// settings have no _FILE form.
func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
func RProxyFromEnvironment(
lookupEnv func(string) (string, bool),
) (*RProxy, error) {
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
cfg := &RProxy{
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
TrustedProxies: env.netblocks("SWRP_TRUSTED_PROXIES", privateRanges),
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
DefaultDisposition: env.disposition("SWRP_DEFAULT_DISPOSITION", "allow"),
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
MaxRequestTime: env.millisecondsOrOff("SWRP_MAX_REQUEST_TIME_MS", "60000"),
MaxResponseTime: env.millisecondsOrOff("SWRP_MAX_RESPONSE_TIME_MS", "1800000"),
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
UpstreamURL: env.appURL(
"SWRP_UPSTREAM_URL",
defaultUpstreamURL,
),
TrustedProxies: env.netblocks(
"SWRP_TRUSTED_PROXIES",
privateRanges,
),
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
DefaultDisposition: env.disposition(
"SWRP_DEFAULT_DISPOSITION",
"allow",
),
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
MaxRequestTime: env.millisecondsOrOff(
"SWRP_MAX_REQUEST_TIME_MS",
"60000",
),
MaxResponseTime: env.millisecondsOrOff(
"SWRP_MAX_RESPONSE_TIME_MS",
"1800000",
),
}
if env.err != nil {
@@ -166,7 +184,9 @@ func (e *environment) milliseconds(name, defaultValue string) time.Duration {
// millisecondsOrOff reads a setting that is a whole number of milliseconds
// above zero, or off, which is 0.
func (e *environment) millisecondsOrOff(name, defaultValue string) time.Duration {
func (e *environment) millisecondsOrOff(
name, defaultValue string,
) time.Duration {
value := e.value(name, defaultValue)
if value == off {
return 0
+29 -10
View File
@@ -50,18 +50,28 @@ func TestRProxyDefaults(t *testing.T) {
if cfg.ListenAddr != rproxyDefaultAddr ||
cfg.UpstreamURL.String() != rproxyDefaultApp ||
cfg.WAFBackend.String() != backendURL {
t.Errorf("addresses %q, %v, %v", cfg.ListenAddr, cfg.UpstreamURL, cfg.WAFBackend)
t.Errorf(
"addresses %q, %v, %v",
cfg.ListenAddr,
cfg.UpstreamURL,
cfg.WAFBackend,
)
}
if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" {
t.Errorf("timeout %v, default disposition %q", cfg.Timeout, cfg.DefaultDisposition)
t.Errorf(
"timeout %v, default disposition %q",
cfg.Timeout,
cfg.DefaultDisposition,
)
}
if cfg.MaxRequestSize != 100<<20 || cfg.MaxResponseSize != 1<<30 {
t.Errorf("sizes %d and %d", cfg.MaxRequestSize, cfg.MaxResponseSize)
}
if cfg.MaxRequestTime != time.Minute || cfg.MaxResponseTime != 30*time.Minute {
if cfg.MaxRequestTime != time.Minute ||
cfg.MaxResponseTime != 30*time.Minute {
t.Errorf("times %v and %v", cfg.MaxRequestTime, cfg.MaxResponseTime)
}
@@ -100,9 +110,12 @@ func TestRProxySettingsAreRead(t *testing.T) {
t.Errorf("addresses %+v", cfg)
}
if cfg.Timeout != 150*time.Millisecond || cfg.DefaultDisposition != "deny" ||
cfg.MaxRequestSize != 1024 || cfg.MaxResponseSize != 0 ||
cfg.MaxRequestTime != 0 || cfg.MaxResponseTime != 250*time.Millisecond {
if cfg.Timeout != 150*time.Millisecond ||
cfg.DefaultDisposition != "deny" ||
cfg.MaxRequestSize != 1024 ||
cfg.MaxResponseSize != 0 ||
cfg.MaxRequestTime != 0 ||
cfg.MaxResponseTime != 250*time.Millisecond {
t.Errorf("limits %+v", cfg)
}
}
@@ -147,7 +160,9 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel()
_, err := rproxyConfig(map[string]string{wafBackend: backendURL, tc.name: tc.value})
_, err := rproxyConfig(
map[string]string{wafBackend: backendURL, tc.name: tc.value},
)
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
t.Errorf("error %v, want one naming %s", err, tc.name)
}
@@ -158,7 +173,9 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
func TestRProxyTrustedProxiesSetButEmptyTrustNothing(t *testing.T) {
t.Parallel()
cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL, rproxyTrusted: ""})
cfg, err := rproxyConfig(
map[string]string{wafBackend: backendURL, rproxyTrusted: ""},
)
if err != nil {
t.Fatalf("settings: %v", err)
}
@@ -172,7 +189,8 @@ func TestRProxyNeedsItsBackend(t *testing.T) {
t.Parallel()
_, err := rproxyConfig(nil)
if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") {
if err == nil ||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
t.Errorf("error %v, want one saying %s is required", err, wafBackend)
}
}
@@ -188,7 +206,8 @@ func TestRProxyReadsNoFile(t *testing.T) {
}
_, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path})
if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") {
if err == nil ||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
t.Errorf("error %v, want %s still required", err, wafBackend)
}