| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "maps" |
| 5 | "math/rand/v2" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/ipfs/boxo/autoconf" |
| 9 | logging "github.com/ipfs/go-log/v2" |
| 10 | peer "github.com/libp2p/go-libp2p/core/peer" |
| 11 | ) |
| 12 | |
| 13 | var log = logging.Logger("config") |
| 14 | |
| 15 | // AutoConf contains the configuration for the autoconf subsystem |
| 16 | type AutoConf struct { |
| 17 | // URL is the HTTP(S) URL to fetch the autoconf.json from |
| 18 | // Default: see boxo/autoconf.MainnetAutoConfURL |
| 19 | URL *OptionalString `json:",omitempty"` |
| 20 | |
| 21 | // Enabled determines whether to use autoconf |
| 22 | // Default: true |
| 23 | Enabled Flag `json:",omitempty"` |
| 24 | |
| 25 | // RefreshInterval is how often to refresh autoconf data |
| 26 | // Default: 24h |
| 27 | RefreshInterval *OptionalDuration `json:",omitempty"` |
| 28 | |
| 29 | // TLSInsecureSkipVerify allows skipping TLS verification (for testing only) |
| 30 | // Default: false |
| 31 | TLSInsecureSkipVerify Flag `json:",omitempty"` |
| 32 | } |
| 33 | |
| 34 | const ( |
| 35 | // AutoPlaceholder is the string used as a placeholder for autoconf values |
| 36 | AutoPlaceholder = "auto" |
| 37 | |
| 38 | // DefaultAutoConfEnabled is the default value for AutoConf.Enabled |
| 39 | DefaultAutoConfEnabled = true |
| 40 | |
| 41 | // DefaultAutoConfURL is the default URL for fetching autoconf |
| 42 | DefaultAutoConfURL = autoconf.MainnetAutoConfURL |
| 43 | |
| 44 | // DefaultAutoConfRefreshInterval is the default interval for refreshing autoconf data |
| 45 | DefaultAutoConfRefreshInterval = autoconf.DefaultRefreshInterval |
| 46 | |
| 47 | // AutoConf client configuration constants |
| 48 | DefaultAutoConfCacheSize = autoconf.DefaultCacheSize |
| 49 | DefaultAutoConfTimeout = autoconf.DefaultTimeout |
| 50 | ) |
| 51 | |
| 52 | // getNativeSystems returns the list of systems that should be used natively based on routing type |
| 53 | func getNativeSystems(routingType string) []string { |
| 54 | switch routingType { |
| 55 | case "dht", "dhtclient", "dhtserver": |
| 56 | return []string{autoconf.SystemAminoDHT} // Only native DHT |
| 57 | case "auto", "autoclient": |
| 58 | return []string{autoconf.SystemAminoDHT} // Native DHT, delegated others |
| 59 | case "delegated": |
| 60 | return []string{} // Everything delegated |
| 61 | case "none": |
| 62 | return []string{} // No native systems |
| 63 | default: |
| 64 | return []string{} // Custom mode |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // selectRandomResolver picks a random resolver from a list for load balancing |
| 69 | func selectRandomResolver(resolvers []string) string { |
| 70 | if len(resolvers) == 0 { |
| 71 | return "" |
| 72 | } |
| 73 | return resolvers[rand.IntN(len(resolvers))] |
| 74 | } |
| 75 | |
| 76 | // DNSResolversWithAutoConf returns DNS resolvers with "auto" values replaced by autoconf values |
| 77 | func (c *Config) DNSResolversWithAutoConf() map[string]string { |
| 78 | if c.DNS.Resolvers == nil { |
| 79 | return nil |
| 80 | } |
| 81 | |
| 82 | resolved := make(map[string]string) |
| 83 | autoConf := c.getAutoConf() |
| 84 | autoExpanded := 0 |
| 85 | |
| 86 | // Process each configured resolver |
| 87 | for domain, resolver := range c.DNS.Resolvers { |
| 88 | if resolver == AutoPlaceholder { |
| 89 | // Try to resolve from autoconf |
| 90 | if autoConf != nil && autoConf.DNSResolvers != nil { |
| 91 | if resolvers, exists := autoConf.DNSResolvers[domain]; exists && len(resolvers) > 0 { |
| 92 | resolved[domain] = selectRandomResolver(resolvers) |
| 93 | autoExpanded++ |
| 94 | } |
| 95 | } |
| 96 | // If autoConf is disabled or domain not found, skip this "auto" resolver |
| 97 | } else { |
| 98 | // Keep custom resolver as-is |
| 99 | resolved[domain] = resolver |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // Add default resolvers from autoconf that aren't already configured |
| 104 | if autoConf != nil && autoConf.DNSResolvers != nil { |
| 105 | for domain, resolvers := range autoConf.DNSResolvers { |
| 106 | if _, exists := resolved[domain]; !exists && len(resolvers) > 0 { |
| 107 | resolved[domain] = selectRandomResolver(resolvers) |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // Log expansion statistics |
| 113 | if autoExpanded > 0 { |
| 114 | log.Debugf("expanded %d 'auto' DNS.Resolvers from autoconf", autoExpanded) |
| 115 | } |
| 116 | |
| 117 | return resolved |
| 118 | } |
| 119 | |
| 120 | // expandAutoConfSlice is a generic helper for expanding "auto" placeholders in string slices |
| 121 | // It handles the common pattern of: iterate through slice, expand "auto" once, keep custom values |
| 122 | func expandAutoConfSlice(sourceSlice []string, autoConfData []string) []string { |
| 123 | var resolved []string |
| 124 | autoExpanded := false |
| 125 | |
| 126 | for _, item := range sourceSlice { |
| 127 | if item == AutoPlaceholder { |
| 128 | // Replace with autoconf data (only once) |
| 129 | if autoConfData != nil && !autoExpanded { |
| 130 | resolved = append(resolved, autoConfData...) |
| 131 | autoExpanded = true |
| 132 | } |
| 133 | // If autoConfData is nil or already expanded, skip redundant "auto" entries silently |
| 134 | } else { |
| 135 | // Keep custom item |
| 136 | resolved = append(resolved, item) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | return resolved |
| 141 | } |
| 142 | |
| 143 | // BootstrapWithAutoConf returns bootstrap config with "auto" values replaced by autoconf values |
| 144 | func (c *Config) BootstrapWithAutoConf() []string { |
| 145 | autoConf := c.getAutoConf() |
| 146 | var autoConfData []string |
| 147 | |
| 148 | if autoConf != nil { |
| 149 | routingType := c.Routing.Type.WithDefault(DefaultRoutingType) |
| 150 | nativeSystems := getNativeSystems(routingType) |
| 151 | autoConfData = autoConf.GetBootstrapPeers(nativeSystems...) |
| 152 | log.Debugf("BootstrapWithAutoConf: processing with routing type: %s", routingType) |
| 153 | } else { |
| 154 | log.Debugf("BootstrapWithAutoConf: autoConf disabled, using original config") |
| 155 | } |
| 156 | |
| 157 | result := expandAutoConfSlice(c.Bootstrap, autoConfData) |
| 158 | log.Debugf("BootstrapWithAutoConf: final result contains %d peers", len(result)) |
| 159 | return result |
| 160 | } |
| 161 | |
| 162 | // getAutoConf is a helper to get autoconf data with fallbacks |
| 163 | func (c *Config) getAutoConf() *autoconf.Config { |
| 164 | if !c.AutoConf.Enabled.WithDefault(DefaultAutoConfEnabled) { |
| 165 | log.Debugf("getAutoConf: AutoConf disabled, returning nil") |
| 166 | return nil |
| 167 | } |
| 168 | |
| 169 | // Create or get cached client with config |
| 170 | client, err := GetAutoConfClient(c) |
| 171 | if err != nil { |
| 172 | log.Debugf("getAutoConf: client creation failed - %v", err) |
| 173 | return nil |
| 174 | } |
| 175 | |
| 176 | // Use GetCached to avoid network I/O during config operations |
| 177 | // This ensures config retrieval doesn't block on network operations |
| 178 | result := client.GetCached() |
| 179 | |
| 180 | log.Debugf("getAutoConf: returning autoconf data") |
| 181 | return result |
| 182 | } |
| 183 | |
| 184 | // BootstrapPeersWithAutoConf returns bootstrap peers with "auto" values replaced by autoconf values |
| 185 | // and parsed into peer.AddrInfo structures |
| 186 | func (c *Config) BootstrapPeersWithAutoConf() ([]peer.AddrInfo, error) { |
| 187 | bootstrapStrings := c.BootstrapWithAutoConf() |
| 188 | return ParseBootstrapPeers(bootstrapStrings) |
| 189 | } |
| 190 | |
| 191 | // DelegatedRoutersWithAutoConf returns delegated router URLs without trailing slashes |
| 192 | func (c *Config) DelegatedRoutersWithAutoConf() []string { |
| 193 | autoConf := c.getAutoConf() |
| 194 | |
| 195 | // Use autoconf to expand the endpoints with supported paths for read operations |
| 196 | routingType := c.Routing.Type.WithDefault(DefaultRoutingType) |
| 197 | nativeSystems := getNativeSystems(routingType) |
| 198 | return autoconf.ExpandDelegatedEndpoints( |
| 199 | c.Routing.DelegatedRouters, |
| 200 | autoConf, |
| 201 | nativeSystems, |
| 202 | // Kubo supports all read paths |
| 203 | autoconf.RoutingV1ProvidersPath, |
| 204 | autoconf.RoutingV1PeersPath, |
| 205 | autoconf.RoutingV1IPNSPath, |
| 206 | ) |
| 207 | } |
| 208 | |
| 209 | // DelegatedPublishersWithAutoConf returns delegated publisher URLs without trailing slashes |
| 210 | func (c *Config) DelegatedPublishersWithAutoConf() []string { |
| 211 | autoConf := c.getAutoConf() |
| 212 | |
| 213 | // Use autoconf to expand the endpoints with IPNS write path |
| 214 | routingType := c.Routing.Type.WithDefault(DefaultRoutingType) |
| 215 | nativeSystems := getNativeSystems(routingType) |
| 216 | return autoconf.ExpandDelegatedEndpoints( |
| 217 | c.Ipns.DelegatedPublishers, |
| 218 | autoConf, |
| 219 | nativeSystems, |
| 220 | autoconf.RoutingV1IPNSPath, // Only IPNS operations (for write) |
| 221 | ) |
| 222 | } |
| 223 | |
| 224 | // expandConfigField expands a specific config field with autoconf values |
| 225 | // Handles both top-level fields ("Bootstrap") and nested fields ("DNS.Resolvers") |
| 226 | func (c *Config) expandConfigField(expandedCfg map[string]any, fieldPath string) { |
| 227 | // Check if this field supports autoconf expansion |
| 228 | expandFunc, supported := supportedAutoConfFields[fieldPath] |
| 229 | if !supported { |
| 230 | return |
| 231 | } |
| 232 | |
| 233 | // Handle top-level fields (no dot in path) |
| 234 | if !strings.Contains(fieldPath, ".") { |
| 235 | if _, exists := expandedCfg[fieldPath]; exists { |
| 236 | expandedCfg[fieldPath] = expandFunc(c) |
| 237 | } |
| 238 | return |
| 239 | } |
| 240 | |
| 241 | // Handle nested fields (section.field format) |
| 242 | parts := strings.SplitN(fieldPath, ".", 2) |
| 243 | if len(parts) != 2 { |
| 244 | return |
| 245 | } |
| 246 | |
| 247 | sectionName, fieldName := parts[0], parts[1] |
| 248 | if section, exists := expandedCfg[sectionName]; exists { |
| 249 | if sectionMap, ok := section.(map[string]any); ok { |
| 250 | if _, exists := sectionMap[fieldName]; exists { |
| 251 | sectionMap[fieldName] = expandFunc(c) |
| 252 | expandedCfg[sectionName] = sectionMap |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | // ExpandAutoConfValues expands "auto" placeholders in config with their actual values using the same methods as the daemon |
| 259 | func (c *Config) ExpandAutoConfValues(cfg map[string]any) (map[string]any, error) { |
| 260 | // Create a deep copy of the config map to avoid modifying the original |
| 261 | expandedCfg := maps.Clone(cfg) |
| 262 | |
| 263 | // Use the same expansion methods that the daemon uses - ensures runtime consistency |
| 264 | // Unified expansion for all supported autoconf fields |
| 265 | c.expandConfigField(expandedCfg, "Bootstrap") |
| 266 | c.expandConfigField(expandedCfg, "DNS.Resolvers") |
| 267 | c.expandConfigField(expandedCfg, "Routing.DelegatedRouters") |
| 268 | c.expandConfigField(expandedCfg, "Ipns.DelegatedPublishers") |
| 269 | |
| 270 | return expandedCfg, nil |
| 271 | } |
| 272 | |
| 273 | // supportedAutoConfFields maps field keys to their expansion functions |
| 274 | var supportedAutoConfFields = map[string]func(*Config) any{ |
| 275 | "Bootstrap": func(c *Config) any { |
| 276 | expanded := c.BootstrapWithAutoConf() |
| 277 | return stringSliceToInterfaceSlice(expanded) |
| 278 | }, |
| 279 | "DNS.Resolvers": func(c *Config) any { |
| 280 | expanded := c.DNSResolversWithAutoConf() |
| 281 | return stringMapToInterfaceMap(expanded) |
| 282 | }, |
| 283 | "Routing.DelegatedRouters": func(c *Config) any { |
| 284 | expanded := c.DelegatedRoutersWithAutoConf() |
| 285 | return stringSliceToInterfaceSlice(expanded) |
| 286 | }, |
| 287 | "Ipns.DelegatedPublishers": func(c *Config) any { |
| 288 | expanded := c.DelegatedPublishersWithAutoConf() |
| 289 | return stringSliceToInterfaceSlice(expanded) |
| 290 | }, |
| 291 | } |
| 292 | |
| 293 | // ExpandConfigField expands auto values for a specific config field using the same methods as the daemon |
| 294 | func (c *Config) ExpandConfigField(key string, value any) any { |
| 295 | if expandFunc, supported := supportedAutoConfFields[key]; supported { |
| 296 | return expandFunc(c) |
| 297 | } |
| 298 | |
| 299 | // Return original value if no expansion needed (not a field that supports auto values) |
| 300 | return value |
| 301 | } |
| 302 | |
| 303 | // Helper functions for type conversion between string types and any types for JSON compatibility |
| 304 | |
| 305 | func stringSliceToInterfaceSlice(slice []string) []any { |
| 306 | result := make([]any, len(slice)) |
| 307 | for i, v := range slice { |
| 308 | result[i] = v |
| 309 | } |
| 310 | return result |
| 311 | } |
| 312 | |
| 313 | func stringMapToInterfaceMap(m map[string]string) map[string]any { |
| 314 | result := make(map[string]any) |
| 315 | for k, v := range m { |
| 316 | result[k] = v |
| 317 | } |
| 318 | return result |
| 319 | } |