590 lines
22 KiB
Go
590 lines
22 KiB
Go
// Package apt implements the persistent Debian-family APT Target.
|
|
package apt
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"proxyctl/internal/execsafe"
|
|
"proxyctl/internal/profile"
|
|
"proxyctl/internal/securefs"
|
|
"proxyctl/internal/target"
|
|
)
|
|
|
|
const (
|
|
// overlayName sorts after digit-prefixed and common letter-named fragments
|
|
// (such as a hand-written proxy.conf) because APT applies apt.conf.d entries
|
|
// in lexical order with later values winning; the overlay must be effective
|
|
// for Enabled to be truthful. legacyOverlayName is the pre-rename overlay
|
|
// removed on the next mutation so upgrades never leave stale fragments.
|
|
overlayName = "zz-proxyctl.conf"
|
|
legacyOverlayName = "99proxyctl"
|
|
overlayHeader = "// Generated by proxyctl. Do not edit.\n"
|
|
)
|
|
|
|
// Configuration names the Debian marker and the isolated APT configuration
|
|
// fragment. TrustedRoot anchors all mutations and is injectable for tests.
|
|
type Configuration struct {
|
|
DebianVersionFile string
|
|
ConfigDirectory string
|
|
OverlayFile string
|
|
TrustedRoot string
|
|
}
|
|
|
|
func DefaultConfiguration() Configuration {
|
|
return Configuration{
|
|
DebianVersionFile: "/etc/debian_version",
|
|
ConfigDirectory: "/etc/apt/apt.conf.d",
|
|
OverlayFile: "/etc/apt/apt.conf.d/" + overlayName,
|
|
TrustedRoot: "/",
|
|
}
|
|
}
|
|
|
|
// FileSystem is the mutation seam used by the Adapter. Availability probes are
|
|
// read-only os.Stat calls, while writes remain descriptor-anchored in securefs.
|
|
type FileSystem interface {
|
|
ReadFile(string) ([]byte, error)
|
|
AtomicWrite(string, []byte, os.FileMode) error
|
|
Remove(string) error
|
|
}
|
|
|
|
type OSFileSystem struct{ TrustedRoot string }
|
|
|
|
func NewOSFileSystem(trustedRoot string) OSFileSystem { return OSFileSystem{TrustedRoot: trustedRoot} }
|
|
func (f OSFileSystem) store() securefs.Store { return securefs.Store{TrustedRoot: f.TrustedRoot} }
|
|
func (f OSFileSystem) ReadFile(path string) ([]byte, error) {
|
|
return f.store().ReadFile(path)
|
|
}
|
|
func (f OSFileSystem) AtomicWrite(path string, data []byte, mode os.FileMode) error {
|
|
return f.store().AtomicWrite(path, data, mode)
|
|
}
|
|
func (f OSFileSystem) Remove(path string) error { return f.store().Remove(path) }
|
|
|
|
// Runner isolates apt-config discovery and execution from adapter tests.
|
|
type Runner interface {
|
|
LookPath(string) (string, error)
|
|
Output(context.Context, string, ...string) ([]byte, error)
|
|
}
|
|
|
|
type systemRunner struct{}
|
|
|
|
func (systemRunner) LookPath(name string) (string, error) { return execsafe.LookPath(name) }
|
|
func (systemRunner) Output(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
return exec.CommandContext(ctx, name, args...).Output()
|
|
}
|
|
|
|
type Adapter struct {
|
|
configuration Configuration
|
|
files FileSystem
|
|
runner Runner
|
|
}
|
|
|
|
var _ target.Diagnosable = (*Adapter)(nil)
|
|
|
|
func New(configuration Configuration, files FileSystem, runner Runner) *Adapter {
|
|
defaults := DefaultConfiguration()
|
|
if configuration.DebianVersionFile == "" {
|
|
configuration.DebianVersionFile = defaults.DebianVersionFile
|
|
}
|
|
if configuration.ConfigDirectory == "" {
|
|
configuration.ConfigDirectory = defaults.ConfigDirectory
|
|
}
|
|
if configuration.OverlayFile == "" {
|
|
configuration.OverlayFile = filepath.Join(configuration.ConfigDirectory, overlayName)
|
|
}
|
|
if configuration.TrustedRoot == "" {
|
|
configuration.TrustedRoot = defaults.TrustedRoot
|
|
}
|
|
if files == nil {
|
|
files = NewOSFileSystem(configuration.TrustedRoot)
|
|
}
|
|
if runner == nil {
|
|
runner = systemRunner{}
|
|
}
|
|
return &Adapter{configuration: configuration, files: files, runner: runner}
|
|
}
|
|
|
|
func (a *Adapter) ID() string { return "apt" }
|
|
func (a *Adapter) Description() target.Description {
|
|
return target.Description{
|
|
Name: "APT",
|
|
Summary: "Persistently configures APT proxying across Shell and privilege boundaries.",
|
|
Scope: "APT package operations; not Shell, desktop, Docker, or other applications",
|
|
Permissions: "pre-existing authority to write /etc/apt; proxyctl never escalates privileges",
|
|
Ownership: "isolated apt.conf.d overlay; disabling reveals any earlier External configuration",
|
|
RestartRisk: "none",
|
|
Details: "APT supports proxy environment variables; this Target affects persistent APT package operations across Shell and privilege boundaries; it does not change Shell, desktop, Docker, or other application proxy settings. proxyctl owns only an isolated apt.conf.d overlay, leaving earlier configuration underneath so disabling restores any External setting. Writing /etc/apt normally requires pre-existing root authority; proxyctl never escalates privileges, and no service restart is performed.",
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) Confirmation(bool) string { return "" }
|
|
|
|
// DiagnosticChecks observes the APT command, configuration schema, and current
|
|
// filesystem authority without changing the overlay or effective settings.
|
|
func (a *Adapter) MutationAuthority(context.Context) target.Check {
|
|
check := target.ReadOnlyWriteAuthority(a.configuration.OverlayFile)
|
|
if !check.OK {
|
|
check.Evidence += "; run proxyctl with sufficient authority (normally sudo proxyctl)"
|
|
}
|
|
return check
|
|
}
|
|
|
|
func (a *Adapter) DiagnosticChecks(ctx context.Context, _ *profile.Profile) []target.Check {
|
|
commandCheck := target.Check{Name: "required command", OK: true, Evidence: "apt-config is available"}
|
|
command, err := a.runner.LookPath("apt-config")
|
|
if err != nil {
|
|
commandCheck.OK = false
|
|
commandCheck.Evidence = "apt-config is unavailable: " + profile.RedactText(err.Error())
|
|
} else if _, err := a.runner.Output(ctx, command, "dump"); err != nil {
|
|
commandCheck.OK = false
|
|
commandCheck.Evidence = "apt-config dump failed: " + profile.RedactText(err.Error())
|
|
}
|
|
marker, markerErr := os.Stat(a.configuration.DebianVersionFile)
|
|
directory, directoryErr := os.Stat(a.configuration.ConfigDirectory)
|
|
schemaOK := markerErr == nil && marker.Mode().IsRegular() && directoryErr == nil && directory.IsDir()
|
|
schemaEvidence := "Debian marker and apt.conf.d configuration directory are present"
|
|
if !schemaOK {
|
|
schemaEvidence = profile.RedactText(fmt.Sprintf("Debian marker or apt.conf.d configuration directory is unusable: marker=%v directory=%v", markerErr, directoryErr))
|
|
}
|
|
return []target.Check{
|
|
commandCheck,
|
|
{Name: "configuration schema", OK: schemaOK, Evidence: schemaEvidence},
|
|
target.ReadOnlyWriteAuthority(a.configuration.OverlayFile),
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) Availability(ctx context.Context) target.Availability {
|
|
availability, _, _ := a.detectAvailability(ctx)
|
|
return availability
|
|
}
|
|
|
|
// detectAvailability returns the effective dump used to prove capability so a
|
|
// progressive Detect can classify state without running apt-config twice.
|
|
func (a *Adapter) detectAvailability(ctx context.Context) (target.Availability, []byte, error) {
|
|
marker, err := os.Stat(a.configuration.DebianVersionFile)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return target.Availability{Evidence: "Debian-family marker /etc/debian_version was not detected"}, nil, nil
|
|
}
|
|
return target.Availability{Evidence: "Debian-family marker /etc/debian_version could not be inspected"}, nil, fmt.Errorf("inspect Debian-family marker: %w", err)
|
|
}
|
|
if !marker.Mode().IsRegular() {
|
|
return target.Availability{Evidence: "Debian-family marker /etc/debian_version was not detected"}, nil, nil
|
|
}
|
|
directory, err := os.Stat(a.configuration.ConfigDirectory)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return target.Availability{Evidence: "APT configuration directory apt.conf.d was not detected"}, nil, nil
|
|
}
|
|
return target.Availability{Evidence: "APT configuration directory apt.conf.d could not be inspected"}, nil, fmt.Errorf("inspect APT configuration directory: %w", err)
|
|
}
|
|
if !directory.IsDir() {
|
|
return target.Availability{Evidence: "APT configuration directory apt.conf.d was not detected"}, nil, nil
|
|
}
|
|
command, err := a.runner.LookPath("apt-config")
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, exec.ErrNotFound) {
|
|
return target.Availability{Evidence: "Debian APT tool apt-config was not detected"}, nil, nil
|
|
}
|
|
return target.Availability{Evidence: "Debian APT tool apt-config could not be inspected"}, nil, fmt.Errorf("locate apt-config: %w", err)
|
|
}
|
|
effective, err := a.runner.Output(ctx, command, "dump")
|
|
if err != nil {
|
|
return target.Availability{Evidence: "apt-config could not inspect effective APT configuration"}, nil, fmt.Errorf("inspect effective APT configuration: %w", err)
|
|
}
|
|
return target.Availability{Available: true, Evidence: "Debian marker, apt-config, and apt.conf.d configuration capability detected"}, effective, nil
|
|
}
|
|
|
|
func (a *Adapter) Detect(ctx context.Context, active *profile.Profile) target.Detection {
|
|
availability, effective, availabilityErr := a.detectAvailability(ctx)
|
|
if !availability.Available && availabilityErr == nil {
|
|
return target.Detection{Availability: availability}
|
|
}
|
|
overlay, overlayErr := a.files.ReadFile(a.configuration.OverlayFile)
|
|
inspection, inspectionErr := a.inspectObserved(active, overlay, overlayErr, effective, availabilityErr)
|
|
return target.Detection{Availability: availability, Inspection: inspection, Err: errors.Join(availabilityErr, inspectionErr)}
|
|
}
|
|
|
|
func (a *Adapter) OverlayPath() string { return a.configuration.OverlayFile }
|
|
|
|
func (a *Adapter) Inspect(ctx context.Context, active *profile.Profile) target.Inspection {
|
|
inspection, _ := a.inspect(ctx, active)
|
|
return inspection
|
|
}
|
|
|
|
func (a *Adapter) inspect(ctx context.Context, active *profile.Profile) (target.Inspection, error) {
|
|
overlay, overlayErr := a.files.ReadFile(a.configuration.OverlayFile)
|
|
effective, dumpErr := a.dump(ctx)
|
|
return a.inspectObserved(active, overlay, overlayErr, effective, dumpErr)
|
|
}
|
|
|
|
func (a *Adapter) inspectObserved(active *profile.Profile, overlay []byte, overlayErr error, effective []byte, dumpErr error) (target.Inspection, error) {
|
|
managed := overlayErr == nil && isOwnedOverlay(overlay)
|
|
if overlayErr != nil && !errors.Is(overlayErr, fs.ErrNotExist) {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay cannot be read"}, fmt.Errorf("read managed APT overlay: %w", overlayErr)
|
|
}
|
|
if dumpErr != nil {
|
|
if managed {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay exists but effective configuration cannot be inspected"}, dumpErr
|
|
}
|
|
return target.Inspection{State: target.Disabled, Evidence: "effective APT configuration could not be positively inspected"}, dumpErr
|
|
}
|
|
effectiveValues := parseConfiguration(effective)
|
|
if managed {
|
|
managedValues := parseConfiguration(overlay)
|
|
if len(managedValues) == 0 {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay contains no recognized proxy settings"}, nil
|
|
}
|
|
if active == nil {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay exists without an Active Profile"}, nil
|
|
}
|
|
desired, err := translate(*active)
|
|
if err == nil {
|
|
coverExistingHostRules(desired.settings, effectiveValues)
|
|
}
|
|
if err == nil && equalSettings(managedValues, desired.settings) && equalSettings(effectiveValues, desired.settings) {
|
|
return target.Inspection{State: target.Enabled, Evidence: "effective managed APT values match the Active Profile"}, nil
|
|
}
|
|
return target.Inspection{State: target.Drifted, Evidence: "effective managed APT values do not match the Active Profile"}, nil
|
|
}
|
|
if key, ok := recognizedProxy(effectiveValues); ok {
|
|
return target.Inspection{State: target.External, Evidence: "recognized non-managed APT proxy setting " + key + " is effective"}, nil
|
|
}
|
|
return target.Inspection{State: target.Disabled, Evidence: "no recognized effective APT proxy configuration detected"}, nil
|
|
}
|
|
|
|
func (a *Adapter) SetDesired(ctx context.Context, enabled bool, active *profile.Profile) target.OperationResult {
|
|
if !a.Availability(ctx).Available {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("APT Target is unavailable: Debian APT tooling or configuration capability was not detected")}
|
|
}
|
|
if enabled && active == nil {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("no Active Profile; open Profile management to create and activate one")}
|
|
}
|
|
var mutationErr error
|
|
if enabled {
|
|
if existing, err := a.files.ReadFile(a.configuration.OverlayFile); err == nil && !isOwnedOverlay(existing) {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("APT overlay path already contains configuration not owned by proxyctl; no changes made")}
|
|
} else if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(fmt.Errorf("inspect managed APT overlay before writing: %w", err), active)}
|
|
}
|
|
translated, err := translate(*active)
|
|
if err != nil {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(err, active)}
|
|
}
|
|
effective, err := a.dump(ctx)
|
|
if err != nil {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(fmt.Errorf("inspect effective APT configuration before mutation: %w", err), active)}
|
|
}
|
|
coverExistingHostRules(translated.settings, parseConfiguration(effective))
|
|
translated.rendered = render(translated.settings)
|
|
mutationErr = a.files.AtomicWrite(a.configuration.OverlayFile, translated.rendered, 0o600)
|
|
if mutationErr != nil {
|
|
mutationErr = fmt.Errorf("write managed APT overlay: %w", mutationErr)
|
|
} else {
|
|
mutationErr = a.removeLegacyOverlay()
|
|
}
|
|
} else {
|
|
existing, err := a.files.ReadFile(a.configuration.OverlayFile)
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
case err != nil:
|
|
mutationErr = fmt.Errorf("inspect managed APT overlay before removal: %w", err)
|
|
case !isOwnedOverlay(existing):
|
|
// A path collision is user-owned configuration, not managed state.
|
|
default:
|
|
if err := a.files.Remove(a.configuration.OverlayFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
mutationErr = fmt.Errorf("remove managed APT overlay: %w", err)
|
|
}
|
|
}
|
|
if mutationErr == nil {
|
|
mutationErr = a.removeLegacyOverlay()
|
|
}
|
|
}
|
|
inspection, inspectErr := a.inspect(ctx, active)
|
|
if mutationErr == nil && inspectErr != nil {
|
|
mutationErr = fmt.Errorf("re-inspect effective APT configuration after mutation: %w", inspectErr)
|
|
}
|
|
if mutationErr == nil && enabled && inspection.State != target.Enabled {
|
|
mutationErr = errors.New("managed APT overlay was written but did not become the effective Active Profile configuration")
|
|
}
|
|
if mutationErr == nil && !enabled && inspection.State == target.Drifted {
|
|
mutationErr = errors.New("managed APT overlay removal did not complete")
|
|
}
|
|
return target.OperationResult{Inspection: inspection, Err: redactError(mutationErr, active)}
|
|
}
|
|
|
|
// removeLegacyOverlay deletes the pre-rename overlay when proxyctl owns it, so
|
|
// upgrades never leave a stale credential-bearing fragment behind. A legacy
|
|
// file without the managed header is user-owned and stays untouched.
|
|
func (a *Adapter) removeLegacyOverlay() error {
|
|
legacy := filepath.Join(a.configuration.ConfigDirectory, legacyOverlayName)
|
|
if legacy == a.configuration.OverlayFile {
|
|
return nil
|
|
}
|
|
data, err := a.files.ReadFile(legacy)
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
return nil
|
|
case err != nil:
|
|
return fmt.Errorf("inspect legacy APT overlay: %w", err)
|
|
case !isOwnedOverlay(data):
|
|
return nil
|
|
}
|
|
if err := a.files.Remove(legacy); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove legacy APT overlay: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Adapter) dump(ctx context.Context) ([]byte, error) {
|
|
command, err := a.runner.LookPath("apt-config")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locate apt-config: %w", err)
|
|
}
|
|
output, err := a.runner.Output(ctx, command, "dump")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("run apt-config dump: %w", err)
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
type translation struct {
|
|
settings map[string]string
|
|
rendered []byte
|
|
}
|
|
|
|
func translate(input profile.Profile) (translation, error) {
|
|
p, err := profile.ValidateAndNormalize(input)
|
|
if err != nil {
|
|
return translation{}, err
|
|
}
|
|
httpValue, httpsValue := p.HTTPURL, p.HTTPSURL
|
|
if httpValue == "" {
|
|
httpValue = p.AllURL
|
|
}
|
|
if httpsValue == "" {
|
|
httpsValue = p.AllURL
|
|
}
|
|
for _, candidate := range []struct {
|
|
field, value string
|
|
}{{"effective HTTP proxy", httpValue}, {"effective HTTPS proxy", httpsValue}} {
|
|
if candidate.value == "" {
|
|
continue
|
|
}
|
|
parsed, _ := url.Parse(candidate.value)
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" && parsed.Scheme != "socks5h" {
|
|
return translation{}, fmt.Errorf("APT cannot translate %s value %q: proxy scheme %q is incompatible", candidate.field, profile.ObscureURL(candidate.value), parsed.Scheme)
|
|
}
|
|
}
|
|
settings := map[string]string{
|
|
"Acquire::http::Proxy": valueOrDirect(httpValue),
|
|
"Acquire::https::Proxy": valueOrDirect(httpsValue),
|
|
}
|
|
for _, bypass := range p.Bypass {
|
|
if strings.HasPrefix(bypass, ".") || strings.Contains(bypass, "/") || strings.Contains(bypass, ":") {
|
|
return translation{}, fmt.Errorf("APT cannot translate Bypass entry %q; only exact hostnames and IPv4 addresses are supported", bypass)
|
|
}
|
|
if net.ParseIP(bypass) == nil && strings.ContainsAny(bypass, " \t\r\n;\"") {
|
|
return translation{}, fmt.Errorf("APT cannot translate Bypass entry %q", bypass)
|
|
}
|
|
settings["Acquire::http::Proxy::"+bypass] = "DIRECT"
|
|
settings["Acquire::https::Proxy::"+bypass] = "DIRECT"
|
|
}
|
|
return translation{settings: settings, rendered: render(settings)}, nil
|
|
}
|
|
|
|
func valueOrDirect(value string) string {
|
|
if value == "" {
|
|
return "DIRECT"
|
|
}
|
|
return value
|
|
}
|
|
|
|
var settingOrder = []string{"Acquire::http::Proxy", "Acquire::https::Proxy"}
|
|
|
|
func render(settings map[string]string) []byte {
|
|
var output strings.Builder
|
|
output.WriteString(overlayHeader)
|
|
for _, key := range settingOrder {
|
|
if value, ok := settings[key]; ok {
|
|
fmt.Fprintf(&output, "%s \"%s\";\n", key, aptQuote(value))
|
|
}
|
|
}
|
|
for _, protocol := range []string{"http", "https"} {
|
|
prefix := "Acquire::" + protocol + "::Proxy::"
|
|
var keys []string
|
|
for key := range settings {
|
|
if strings.HasPrefix(key, prefix) {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
fmt.Fprintf(&output, "%s \"%s\";\n", key, aptQuote(settings[key]))
|
|
}
|
|
}
|
|
return []byte(output.String())
|
|
}
|
|
|
|
func isOwnedOverlay(data []byte) bool { return strings.HasPrefix(string(data), overlayHeader) }
|
|
|
|
func aptQuote(value string) string {
|
|
return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value)
|
|
}
|
|
|
|
func parseConfiguration(data []byte) map[string]string {
|
|
settings := make(map[string]string)
|
|
for _, raw := range strings.Split(string(data), "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, rest, ok := strings.Cut(line, " ")
|
|
if !ok {
|
|
key, rest, ok = strings.Cut(line, "\t")
|
|
}
|
|
key, recognized := canonicalKey(key)
|
|
if !ok || !recognized {
|
|
continue
|
|
}
|
|
rest = strings.TrimSpace(rest)
|
|
if len(rest) < 3 || rest[0] != '"' || rest[len(rest)-1] != ';' {
|
|
continue
|
|
}
|
|
quoted := strings.TrimSpace(rest[:len(rest)-1])
|
|
if len(quoted) < 2 || quoted[len(quoted)-1] != '"' {
|
|
continue
|
|
}
|
|
settings[key] = aptUnquote(quoted[1 : len(quoted)-1])
|
|
}
|
|
return settings
|
|
}
|
|
|
|
func canonicalKey(key string) (string, bool) {
|
|
parts := strings.Split(key, "::")
|
|
if len(parts) < 3 || !strings.EqualFold(parts[0], "Acquire") ||
|
|
(!strings.EqualFold(parts[1], "http") && !strings.EqualFold(parts[1], "https")) ||
|
|
!strings.EqualFold(parts[2], "Proxy") {
|
|
return "", false
|
|
}
|
|
canonical := "Acquire::" + strings.ToLower(parts[1]) + "::Proxy"
|
|
if len(parts) == 3 {
|
|
return canonical, true
|
|
}
|
|
host := strings.Join(parts[3:], "::")
|
|
if host == "" {
|
|
return "", false
|
|
}
|
|
return canonical + "::" + strings.ToLower(host), true
|
|
}
|
|
|
|
func recognizedProxy(settings map[string]string) (string, bool) {
|
|
keys := make([]string, 0, len(settings))
|
|
for key := range settings {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
value := settings[key]
|
|
if value != "" && !strings.EqualFold(value, "DIRECT") && !strings.EqualFold(value, "false") {
|
|
return key, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func aptUnquote(value string) string {
|
|
var output strings.Builder
|
|
escaped := false
|
|
for _, r := range value {
|
|
if escaped {
|
|
output.WriteRune(r)
|
|
escaped = false
|
|
} else if r == '\\' {
|
|
escaped = true
|
|
} else {
|
|
output.WriteRune(r)
|
|
}
|
|
}
|
|
if escaped {
|
|
output.WriteByte('\\')
|
|
}
|
|
return output.String()
|
|
}
|
|
|
|
func coverExistingHostRules(desired, effective map[string]string) {
|
|
for key := range effective {
|
|
var protocol, prefix string
|
|
switch {
|
|
case strings.HasPrefix(key, "Acquire::http::Proxy::"):
|
|
protocol, prefix = "http", "Acquire::http::Proxy::"
|
|
case strings.HasPrefix(key, "Acquire::https::Proxy::"):
|
|
protocol, prefix = "https", "Acquire::https::Proxy::"
|
|
default:
|
|
continue
|
|
}
|
|
if _, alreadyCovered := desired[key]; alreadyCovered {
|
|
continue
|
|
}
|
|
value := desired["Acquire::"+protocol+"::Proxy"]
|
|
if value == "" {
|
|
value = "DIRECT"
|
|
}
|
|
// A later fragment can shadow a more-specific earlier host rule only by
|
|
// repeating that host key. The earlier file itself remains untouched and
|
|
// becomes effective again when the owned overlay is removed.
|
|
desired[prefix+strings.TrimPrefix(key, prefix)] = value
|
|
}
|
|
}
|
|
|
|
func equalSettings(left, right map[string]string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for key, value := range left {
|
|
if right[key] != value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type redactedError struct {
|
|
message string
|
|
class error
|
|
}
|
|
|
|
func (e *redactedError) Error() string { return e.message }
|
|
func (e *redactedError) Is(other error) bool { return e.class != nil && errors.Is(e.class, other) }
|
|
|
|
func redactError(err error, active *profile.Profile) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
message := profile.RedactText(err.Error())
|
|
if active != nil {
|
|
for _, raw := range []string{active.SingleURL, active.HTTPURL, active.HTTPSURL, active.AllURL} {
|
|
if raw != "" {
|
|
message = strings.ReplaceAll(message, raw, profile.ObscureURL(raw))
|
|
}
|
|
}
|
|
}
|
|
var class error
|
|
for _, safe := range []error{os.ErrPermission, fs.ErrNotExist, fs.ErrExist, context.Canceled, context.DeadlineExceeded} {
|
|
if errors.Is(err, safe) {
|
|
class = safe
|
|
break
|
|
}
|
|
}
|
|
return &redactedError{message: message, class: class}
|
|
}
|