mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-17 06:57:40 +08:00

* Feat: http retrieval as experimental feature This introduces the http-retrieval capability as an experimental feature. It can be enabled in the configuration `Experimental.HTTPRetrieval.Enabled = true`. Documentation and changelog to be added later. * refactor: HTTPRetrieval.Enabled as Flag * docs(config): HTTPRetrieval section * refactor: reusable MockHTTPContentRouter * feat: HTTPRetrieval.TLSInsecureSkipVerify allows self-signed certificates in tests * feat(config): HTTPRetrieval.MaxBlockSize * test: end-to-end HTTPRetrieval.Enabled this spawns two http services on localhost: 1. HTTP router that returns HTTP provider when /routing/v1/providers/cid i queried 2. HTTP provider that returns a block when /ipfs/cid is queried 3. Configures Kubo to use (1) instead of cid.contact this seems to work (running test with DEBUG=true shows (1) was queried for the test CID and returned multiaddr of (2), but Kubo never requested test CID block from (2) – needs investigation * fix: enable /routing/v1/peers for non-cid.contact we artificially limited every delegated routing endpoint because of cid.contact being limited to one endpoint * feat: Routing.DelegatedRouters make it easy to override the hardcoded implicit HTTP routeur URL without having to set the entire custom Router.Routers and Router.Methods (http_retrieval_client_test.go still needs to be fixed in future commit) * test: flag remaining work * docs: review feedback * refactor: providerQueryMgr with bitswapNetworks this fixes two regressions: (1) introduced in https://github.com/ipfs/kubo/issues/10717 where we only used bitswapLib2p query manager (this is why E2E did not act on http provider) (2) introduced in https://github.com/ipfs/kubo/pull/10765 where it was not possible to set binary peerID in IgnoreProviders (we changed to []string) * refactor: Bitswap.Libp2pEnabled replaces Bitswap.Enabled with Bitswap.Libp2pEnabled adds tests that confirm it is possible to disable libp2p bitswap fully and only keep http in client mode also, removes the need for passing empty blockstore in client-only mode * docs: changelog --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>
259 lines
6.9 KiB
Go
259 lines
6.9 KiB
Go
// package config implements the ipfs config file datastructures and utilities.
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/ipfs/kubo/misc/fsutil"
|
|
)
|
|
|
|
// Config is used to load ipfs config files.
|
|
type Config struct {
|
|
Identity Identity // local node's peer identity
|
|
Datastore Datastore // local node's storage
|
|
Addresses Addresses // local node's addresses
|
|
Mounts Mounts // local node's mount points
|
|
Discovery Discovery // local node's discovery mechanisms
|
|
Routing Routing // local node's routing settings
|
|
Ipns Ipns // Ipns settings
|
|
Bootstrap []string // local nodes's bootstrap peer addresses
|
|
Gateway Gateway // local node's gateway server options
|
|
API API // local node's API settings
|
|
Swarm SwarmConfig
|
|
AutoNAT AutoNATConfig
|
|
AutoTLS AutoTLS
|
|
Pubsub PubsubConfig
|
|
Peering Peering
|
|
DNS DNS
|
|
Migration Migration
|
|
|
|
Provider Provider
|
|
Reprovider Reprovider
|
|
HTTPRetrieval HTTPRetrieval
|
|
Experimental Experiments
|
|
Plugins Plugins
|
|
Pinning Pinning
|
|
Import Import
|
|
Version Version
|
|
|
|
Internal Internal // experimental/unstable options
|
|
|
|
Bitswap Bitswap `json:",omitempty"`
|
|
}
|
|
|
|
const (
|
|
// DefaultPathName is the default config dir name.
|
|
DefaultPathName = ".ipfs"
|
|
// DefaultPathRoot is the path to the default config dir location.
|
|
DefaultPathRoot = "~/" + DefaultPathName
|
|
// DefaultConfigFile is the filename of the configuration file.
|
|
DefaultConfigFile = "config"
|
|
// EnvDir is the environment variable used to change the path root.
|
|
EnvDir = "IPFS_PATH"
|
|
)
|
|
|
|
// PathRoot returns the default configuration root directory.
|
|
func PathRoot() (string, error) {
|
|
dir := os.Getenv(EnvDir)
|
|
var err error
|
|
if len(dir) == 0 {
|
|
dir, err = fsutil.ExpandHome(DefaultPathRoot)
|
|
}
|
|
return dir, err
|
|
}
|
|
|
|
// Path returns the path `extension` relative to the configuration root. If an
|
|
// empty string is provided for `configroot`, the default root is used.
|
|
func Path(configroot, extension string) (string, error) {
|
|
if len(configroot) == 0 {
|
|
dir, err := PathRoot()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, extension), nil
|
|
|
|
}
|
|
return filepath.Join(configroot, extension), nil
|
|
}
|
|
|
|
// Filename returns the configuration file path given a configuration root
|
|
// directory and a user-provided configuration file path argument with the
|
|
// following rules:
|
|
// - If the user-provided configuration file path is empty, use the default one.
|
|
// - If the configuration root directory is empty, use the default one.
|
|
// - If the user-provided configuration file path is only a file name, use the
|
|
// configuration root directory, otherwise use only the user-provided path
|
|
// and ignore the configuration root.
|
|
func Filename(configroot, userConfigFile string) (string, error) {
|
|
if userConfigFile == "" {
|
|
return Path(configroot, DefaultConfigFile)
|
|
}
|
|
|
|
if filepath.Dir(userConfigFile) == "." {
|
|
return Path(configroot, userConfigFile)
|
|
}
|
|
|
|
return userConfigFile, nil
|
|
}
|
|
|
|
// HumanOutput gets a config value ready for printing.
|
|
func HumanOutput(value interface{}) ([]byte, error) {
|
|
s, ok := value.(string)
|
|
if ok {
|
|
return []byte(strings.Trim(s, "\n")), nil
|
|
}
|
|
return Marshal(value)
|
|
}
|
|
|
|
// Marshal configuration with JSON.
|
|
func Marshal(value interface{}) ([]byte, error) {
|
|
// need to prettyprint, hence MarshalIndent, instead of Encoder
|
|
return json.MarshalIndent(value, "", " ")
|
|
}
|
|
|
|
func FromMap(v map[string]interface{}) (*Config, error) {
|
|
buf := new(bytes.Buffer)
|
|
if err := json.NewEncoder(buf).Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
var conf Config
|
|
if err := json.NewDecoder(buf).Decode(&conf); err != nil {
|
|
return nil, fmt.Errorf("failure to decode config: %w", err)
|
|
}
|
|
return &conf, nil
|
|
}
|
|
|
|
func ToMap(conf *Config) (map[string]interface{}, error) {
|
|
buf := new(bytes.Buffer)
|
|
if err := json.NewEncoder(buf).Encode(conf); err != nil {
|
|
return nil, err
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.NewDecoder(buf).Decode(&m); err != nil {
|
|
return nil, fmt.Errorf("failure to decode config: %w", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// Convert config to a map, without using encoding/json, since
|
|
// zero/empty/'omitempty' fields are excluded by encoding/json during
|
|
// marshaling.
|
|
func ReflectToMap(conf interface{}) interface{} {
|
|
v := reflect.ValueOf(conf)
|
|
if !v.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
// Handle pointer type
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
// Create a zero value of the pointer's element type
|
|
elemType := v.Type().Elem()
|
|
zero := reflect.Zero(elemType)
|
|
return ReflectToMap(zero.Interface())
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
switch v.Kind() {
|
|
case reflect.Struct:
|
|
result := make(map[string]interface{})
|
|
t := v.Type()
|
|
for i := 0; i < v.NumField(); i++ {
|
|
field := v.Field(i)
|
|
// Only include exported fields
|
|
if field.CanInterface() {
|
|
result[t.Field(i).Name] = ReflectToMap(field.Interface())
|
|
}
|
|
}
|
|
return result
|
|
|
|
case reflect.Map:
|
|
result := make(map[string]interface{})
|
|
iter := v.MapRange()
|
|
for iter.Next() {
|
|
key := iter.Key()
|
|
// Convert map keys to strings for consistency
|
|
keyStr := fmt.Sprint(ReflectToMap(key.Interface()))
|
|
result[keyStr] = ReflectToMap(iter.Value().Interface())
|
|
}
|
|
// Add a sample to differentiate between a map and a struct on validation.
|
|
sample := reflect.Zero(v.Type().Elem())
|
|
if sample.CanInterface() {
|
|
result["*"] = ReflectToMap(sample.Interface())
|
|
}
|
|
return result
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
result := make([]interface{}, v.Len())
|
|
for i := 0; i < v.Len(); i++ {
|
|
result[i] = ReflectToMap(v.Index(i).Interface())
|
|
}
|
|
return result
|
|
|
|
default:
|
|
// For basic types (int, string, etc.), just return the value
|
|
if v.CanInterface() {
|
|
return v.Interface()
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Clone copies the config. Use when updating.
|
|
func (c *Config) Clone() (*Config, error) {
|
|
var newConfig Config
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(c); err != nil {
|
|
return nil, fmt.Errorf("failure to encode config: %w", err)
|
|
}
|
|
|
|
if err := json.NewDecoder(&buf).Decode(&newConfig); err != nil {
|
|
return nil, fmt.Errorf("failure to decode config: %w", err)
|
|
}
|
|
|
|
return &newConfig, nil
|
|
}
|
|
|
|
// Check if the provided key is present in the structure.
|
|
func CheckKey(key string) error {
|
|
conf := Config{}
|
|
|
|
// Convert an empty config to a map without JSON.
|
|
cursor := ReflectToMap(&conf)
|
|
|
|
// Parse the key and verify it's presence in the map.
|
|
var ok bool
|
|
var mapCursor map[string]interface{}
|
|
|
|
parts := strings.Split(key, ".")
|
|
for i, part := range parts {
|
|
mapCursor, ok = cursor.(map[string]interface{})
|
|
if !ok {
|
|
if cursor == nil {
|
|
return nil
|
|
}
|
|
path := strings.Join(parts[:i], ".")
|
|
return fmt.Errorf("%s key is not a map", path)
|
|
}
|
|
|
|
cursor, ok = mapCursor[part]
|
|
if !ok {
|
|
// If the config sections is a map, validate against the default entry.
|
|
if cursor, ok = mapCursor["*"]; ok {
|
|
continue
|
|
}
|
|
path := strings.Join(parts[:i+1], ".")
|
|
return fmt.Errorf("%s not found", path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|