Commit 42e5ddf7 authored by Pavle Batuta's avatar Pavle Batuta Committed by GitHub

ENS Resolver client (#615)

Add ens resolver client and integrate resolver into the api package
parent 6c981a3a
...@@ -43,6 +43,10 @@ vet: ...@@ -43,6 +43,10 @@ vet:
test-race: test-race:
$(GO) test -race -v ./... $(GO) test -race -v ./...
.PHONY: test-integration
test-integration:
$(GO) test -tags=integration -v ./...
.PHONY: test .PHONY: test
test: test:
$(GO) test -v ./... $(GO) test -v ./...
......
...@@ -40,6 +40,7 @@ const ( ...@@ -40,6 +40,7 @@ const (
optionNameGlobalPinningEnabled = "global-pinning-enable" optionNameGlobalPinningEnabled = "global-pinning-enable"
optionNamePaymentThreshold = "payment-threshold" optionNamePaymentThreshold = "payment-threshold"
optionNamePaymentTolerance = "payment-tolerance" optionNamePaymentTolerance = "payment-tolerance"
optionNameResolverEndpoints = "resolver-options"
) )
func init() { func init() {
...@@ -182,4 +183,5 @@ func (c *command) setAllFlags(cmd *cobra.Command) { ...@@ -182,4 +183,5 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Bool(optionNameGlobalPinningEnabled, false, "enable global pinning") cmd.Flags().Bool(optionNameGlobalPinningEnabled, false, "enable global pinning")
cmd.Flags().Uint64(optionNamePaymentThreshold, 100000, "threshold in BZZ where you expect to get paid from your peers") cmd.Flags().Uint64(optionNamePaymentThreshold, 100000, "threshold in BZZ where you expect to get paid from your peers")
cmd.Flags().Uint64(optionNamePaymentTolerance, 10000, "excess debt above payment threshold in BZZ where you disconnect from your peer") cmd.Flags().Uint64(optionNamePaymentTolerance, 10000, "excess debt above payment threshold in BZZ where you disconnect from your peer")
cmd.Flags().StringSlice(optionNameResolverEndpoints, []string{}, "resolver connection string, see help for format")
} }
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
memkeystore "github.com/ethersphere/bee/pkg/keystore/mem" memkeystore "github.com/ethersphere/bee/pkg/keystore/mem"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/node" "github.com/ethersphere/bee/pkg/node"
"github.com/ethersphere/bee/pkg/resolver"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
...@@ -53,6 +54,18 @@ func (c *command) initStartCmd() (err error) { ...@@ -53,6 +54,18 @@ func (c *command) initStartCmd() (err error) {
default: default:
return fmt.Errorf("unknown verbosity level %q", v) return fmt.Errorf("unknown verbosity level %q", v)
} }
// If the resolver is specified, resolve all connection strings
// and fail on any errors.
var resolverCfgs []*resolver.ConnectionConfig
resolverEndpoints := c.config.GetStringSlice(optionNameResolverEndpoints)
if len(resolverEndpoints) > 0 {
resolverCfgs, err = resolver.ParseConnectionStrings(resolverEndpoints)
if err != nil {
return err
}
}
bee := ` bee := `
Welcome to the Swarm.... Bzzz Bzzzz Bzzzz Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
\ / \ /
...@@ -127,26 +140,27 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz ...@@ -127,26 +140,27 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
} }
b, err := node.NewBee(c.config.GetString(optionNameP2PAddr), address, keystore, swarmPrivateKey, c.config.GetUint64(optionNameNetworkID), logger, node.Options{ b, err := node.NewBee(c.config.GetString(optionNameP2PAddr), address, keystore, swarmPrivateKey, c.config.GetUint64(optionNameNetworkID), logger, node.Options{
DataDir: c.config.GetString(optionNameDataDir), DataDir: c.config.GetString(optionNameDataDir),
DBCapacity: c.config.GetUint64(optionNameDBCapacity), DBCapacity: c.config.GetUint64(optionNameDBCapacity),
Password: password, Password: password,
APIAddr: c.config.GetString(optionNameAPIAddr), APIAddr: c.config.GetString(optionNameAPIAddr),
DebugAPIAddr: debugAPIAddr, DebugAPIAddr: debugAPIAddr,
Addr: c.config.GetString(optionNameP2PAddr), Addr: c.config.GetString(optionNameP2PAddr),
NATAddr: c.config.GetString(optionNameNATAddr), NATAddr: c.config.GetString(optionNameNATAddr),
EnableWS: c.config.GetBool(optionNameP2PWSEnable), EnableWS: c.config.GetBool(optionNameP2PWSEnable),
EnableQUIC: c.config.GetBool(optionNameP2PQUICEnable), EnableQUIC: c.config.GetBool(optionNameP2PQUICEnable),
WelcomeMessage: c.config.GetString(optionWelcomeMessage), WelcomeMessage: c.config.GetString(optionWelcomeMessage),
Bootnodes: c.config.GetStringSlice(optionNameBootnodes), Bootnodes: c.config.GetStringSlice(optionNameBootnodes),
CORSAllowedOrigins: c.config.GetStringSlice(optionCORSAllowedOrigins), CORSAllowedOrigins: c.config.GetStringSlice(optionCORSAllowedOrigins),
Standalone: c.config.GetBool(optionNameStandalone), Standalone: c.config.GetBool(optionNameStandalone),
TracingEnabled: c.config.GetBool(optionNameTracingEnabled), TracingEnabled: c.config.GetBool(optionNameTracingEnabled),
TracingEndpoint: c.config.GetString(optionNameTracingEndpoint), TracingEndpoint: c.config.GetString(optionNameTracingEndpoint),
TracingServiceName: c.config.GetString(optionNameTracingServiceName), TracingServiceName: c.config.GetString(optionNameTracingServiceName),
Logger: logger, Logger: logger,
GlobalPinningEnabled: c.config.GetBool(optionNameGlobalPinningEnabled), GlobalPinningEnabled: c.config.GetBool(optionNameGlobalPinningEnabled),
PaymentThreshold: c.config.GetUint64(optionNamePaymentThreshold), PaymentThreshold: c.config.GetUint64(optionNamePaymentThreshold),
PaymentTolerance: c.config.GetUint64(optionNamePaymentTolerance), PaymentTolerance: c.config.GetUint64(optionNamePaymentTolerance),
ResolverConnectionCfgs: resolverCfgs,
}) })
if err != nil { if err != nil {
return err return err
...@@ -192,7 +206,6 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz ...@@ -192,7 +206,6 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
} }
c.setAllFlags(cmd) c.setAllFlags(cmd)
c.root.AddCommand(cmd) c.root.AddCommand(cmd)
return nil return nil
} }
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
cmdfile "github.com/ethersphere/bee/cmd/internal/file" cmdfile "github.com/ethersphere/bee/cmd/internal/file"
"github.com/ethersphere/bee/pkg/api" "github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
resolverMock "github.com/ethersphere/bee/pkg/resolver/mock"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/storage/mock" "github.com/ethersphere/bee/pkg/storage/mock"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
...@@ -153,7 +154,7 @@ func TestLimitWriter(t *testing.T) { ...@@ -153,7 +154,7 @@ func TestLimitWriter(t *testing.T) {
// newTestServer creates an http server to serve the bee http api endpoints. // newTestServer creates an http server to serve the bee http api endpoints.
func newTestServer(t *testing.T, storer storage.Storer) *url.URL { func newTestServer(t *testing.T, storer storage.Storer) *url.URL {
t.Helper() t.Helper()
s := api.New(tags.NewTags(), storer, nil, logging.New(ioutil.Discard, 0), nil) s := api.New(tags.NewTags(), storer, resolverMock.NewResolver(), nil, logging.New(ioutil.Discard, 0), nil)
ts := httptest.NewServer(s) ts := httptest.NewServer(s)
srvUrl, err := url.Parse(ts.URL) srvUrl, err := url.Parse(ts.URL)
if err != nil { if err != nil {
......
...@@ -7,6 +7,7 @@ require ( ...@@ -7,6 +7,7 @@ require (
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
github.com/coreos/go-semver v0.3.0 github.com/coreos/go-semver v0.3.0
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/ethereum/go-ethereum v1.9.20
github.com/ethersphere/bmt v0.1.2 github.com/ethersphere/bmt v0.1.2
github.com/ethersphere/manifest v0.2.0 github.com/ethersphere/manifest v0.2.0
github.com/gogo/protobuf v1.3.1 github.com/gogo/protobuf v1.3.1
...@@ -48,6 +49,7 @@ require ( ...@@ -48,6 +49,7 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d
github.com/uber/jaeger-client-go v2.24.0+incompatible github.com/uber/jaeger-client-go v2.24.0+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible // indirect github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
github.com/wealdtech/go-ens/v3 v3.4.3
gitlab.com/nolash/go-mockbytes v0.0.7 gitlab.com/nolash/go-mockbytes v0.0.7
go.opencensus.io v0.22.4 // indirect go.opencensus.io v0.22.4 // indirect
go.uber.org/zap v1.15.0 // indirect go.uber.org/zap v1.15.0 // indirect
......
This diff is collapsed.
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
...@@ -13,7 +14,9 @@ import ( ...@@ -13,7 +14,9 @@ import (
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
m "github.com/ethersphere/bee/pkg/metrics" m "github.com/ethersphere/bee/pkg/metrics"
"github.com/ethersphere/bee/pkg/resolver"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags" "github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/tracing" "github.com/ethersphere/bee/pkg/tracing"
) )
...@@ -24,6 +27,12 @@ const ( ...@@ -24,6 +27,12 @@ const (
SwarmEncryptHeader = "Swarm-Encrypt" SwarmEncryptHeader = "Swarm-Encrypt"
) )
var (
errInvalidNameOrAddress = errors.New("invalid name or bzz address")
errNoResolver = errors.New("no resolver connected")
)
// Service is the API service interface.
type Service interface { type Service interface {
http.Handler http.Handler
m.Collector m.Collector
...@@ -32,6 +41,7 @@ type Service interface { ...@@ -32,6 +41,7 @@ type Service interface {
type server struct { type server struct {
Tags *tags.Tags Tags *tags.Tags
Storer storage.Storer Storer storage.Storer
Resolver resolver.Interface
CORSAllowedOrigins []string CORSAllowedOrigins []string
Logger logging.Logger Logger logging.Logger
Tracer *tracing.Tracer Tracer *tracing.Tracer
...@@ -44,10 +54,12 @@ const ( ...@@ -44,10 +54,12 @@ const (
TargetsRecoveryHeader = "swarm-recovery-targets" TargetsRecoveryHeader = "swarm-recovery-targets"
) )
func New(tags *tags.Tags, storer storage.Storer, corsAllowedOrigins []string, logger logging.Logger, tracer *tracing.Tracer) Service { // New will create a and initialize a new API service.
func New(tags *tags.Tags, storer storage.Storer, resolver resolver.Interface, corsAllowedOrigins []string, logger logging.Logger, tracer *tracing.Tracer) Service {
s := &server{ s := &server{
Tags: tags, Tags: tags,
Storer: storer, Storer: storer,
Resolver: resolver,
CORSAllowedOrigins: corsAllowedOrigins, CORSAllowedOrigins: corsAllowedOrigins,
Logger: logger, Logger: logger,
Tracer: tracer, Tracer: tracer,
...@@ -80,6 +92,32 @@ func (s *server) getOrCreateTag(tagUid string) (*tags.Tag, bool, error) { ...@@ -80,6 +92,32 @@ func (s *server) getOrCreateTag(tagUid string) (*tags.Tag, bool, error) {
return t, false, err return t, false, err
} }
func (s *server) resolveNameOrAddress(str string) (swarm.Address, error) {
log := s.Logger
// Try and parse the name as a bzz address.
addr, err := swarm.ParseHexAddress(str)
if err == nil {
log.Tracef("name resolve: valid bzz address %q", str)
return addr, nil
}
// If no resolver is not available, return an error.
if s.Resolver == nil {
return swarm.ZeroAddress, errNoResolver
}
// Try and resolve the name using the provided resolver.
log.Debugf("name resolve: attempting to resolve %s to bzz address", str)
addr, err = s.Resolver.Resolve(str)
if err == nil {
log.Tracef("name resolve: resolved name %s to %s", str, addr)
return addr, nil
}
return swarm.ZeroAddress, fmt.Errorf("%w: %v", errInvalidNameOrAddress, err)
}
// requestModePut returns the desired storage.ModePut for this request based on the request headers. // requestModePut returns the desired storage.ModePut for this request based on the request headers.
func requestModePut(r *http.Request) storage.ModePut { func requestModePut(r *http.Request) storage.ModePut {
if h := strings.ToLower(r.Header.Get(SwarmPinHeader)); h == "true" { if h := strings.ToLower(r.Header.Get(SwarmPinHeader)); h == "true" {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package api_test package api_test
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
...@@ -14,7 +15,10 @@ import ( ...@@ -14,7 +15,10 @@ import (
"github.com/ethersphere/bee/pkg/api" "github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/pingpong" "github.com/ethersphere/bee/pkg/pingpong"
"github.com/ethersphere/bee/pkg/resolver"
resolverMock "github.com/ethersphere/bee/pkg/resolver/mock"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags" "github.com/ethersphere/bee/pkg/tags"
"resenje.org/web" "resenje.org/web"
) )
...@@ -22,6 +26,7 @@ import ( ...@@ -22,6 +26,7 @@ import (
type testServerOptions struct { type testServerOptions struct {
Pingpong pingpong.Interface Pingpong pingpong.Interface
Storer storage.Storer Storer storage.Storer
Resolver resolver.Interface
Tags *tags.Tags Tags *tags.Tags
Logger logging.Logger Logger logging.Logger
} }
...@@ -30,7 +35,10 @@ func newTestServer(t *testing.T, o testServerOptions) *http.Client { ...@@ -30,7 +35,10 @@ func newTestServer(t *testing.T, o testServerOptions) *http.Client {
if o.Logger == nil { if o.Logger == nil {
o.Logger = logging.New(ioutil.Discard, 0) o.Logger = logging.New(ioutil.Discard, 0)
} }
s := api.New(o.Tags, o.Storer, nil, o.Logger, nil) if o.Resolver == nil {
o.Resolver = resolverMock.NewResolver()
}
s := api.New(o.Tags, o.Storer, o.Resolver, nil, o.Logger, nil)
ts := httptest.NewServer(s) ts := httptest.NewServer(s)
t.Cleanup(ts.Close) t.Cleanup(ts.Close)
...@@ -45,3 +53,79 @@ func newTestServer(t *testing.T, o testServerOptions) *http.Client { ...@@ -45,3 +53,79 @@ func newTestServer(t *testing.T, o testServerOptions) *http.Client {
}), }),
} }
} }
func TestParseName(t *testing.T) {
const bzzHash = "89c17d0d8018a19057314aa035e61c9d23c47581a61dd3a79a7839692c617e4d"
testCases := []struct {
desc string
name string
log logging.Logger
res resolver.Interface
noResolver bool
wantAdr swarm.Address
wantErr error
}{
{
desc: "empty name",
name: "",
wantErr: api.ErrInvalidNameOrAddress,
},
{
desc: "bzz hash",
name: bzzHash,
wantAdr: swarm.MustParseHexAddress(bzzHash),
},
{
desc: "no resolver connected with bzz hash",
name: bzzHash,
noResolver: true,
wantAdr: swarm.MustParseHexAddress(bzzHash),
},
{
desc: "no resolver connected with name",
name: "itdoesntmatter.eth",
noResolver: true,
wantErr: api.ErrNoResolver,
},
{
desc: "name not resolved",
name: "not.good",
res: resolverMock.NewResolver(
resolverMock.WithResolveFunc(func(string) (swarm.Address, error) {
return swarm.ZeroAddress, errors.New("failed to resolve")
}),
),
wantErr: api.ErrInvalidNameOrAddress,
},
{
desc: "name resolved",
name: "everything.okay",
wantAdr: swarm.MustParseHexAddress("89c17d0d8018a19057314aa035e61c9d23c47581a61dd3a79a7839692c617e4d"),
},
}
for _, tC := range testCases {
if tC.log == nil {
tC.log = logging.New(ioutil.Discard, 0)
}
if tC.res == nil && !tC.noResolver {
tC.res = resolverMock.NewResolver(
resolverMock.WithResolveFunc(func(string) (swarm.Address, error) {
return tC.wantAdr, nil
}))
}
s := api.New(nil, nil, tC.res, nil, tC.log, nil).(*api.Server)
t.Run(tC.desc, func(t *testing.T) {
got, err := s.ResolveNameOrAddress(tC.name)
if err != nil && !errors.Is(err, tC.wantErr) {
t.Fatalf("bad error: %v", err)
}
if !got.Equal(tC.wantAdr) {
t.Errorf("got %s, want %s", got, tC.wantAdr)
}
})
}
}
...@@ -52,11 +52,11 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -52,11 +52,11 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
// bytesGetHandler handles retrieval of raw binary data of arbitrary length. // bytesGetHandler handles retrieval of raw binary data of arbitrary length.
func (s *server) bytesGetHandler(w http.ResponseWriter, r *http.Request) { func (s *server) bytesGetHandler(w http.ResponseWriter, r *http.Request) {
addressHex := mux.Vars(r)["address"] nameOrHex := mux.Vars(r)["address"]
address, err := swarm.ParseHexAddress(addressHex) address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
s.Logger.Debugf("bytes: parse address %s: %v", addressHex, err) s.Logger.Debugf("bytes: parse address %s: %v", nameOrHex, err)
s.Logger.Error("bytes: parse address error") s.Logger.Error("bytes: parse address error")
jsonhttp.BadRequest(w, "invalid address") jsonhttp.BadRequest(w, "invalid address")
return return
......
...@@ -19,7 +19,6 @@ import ( ...@@ -19,7 +19,6 @@ import (
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/manifest" "github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/sctx" "github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/swarm"
) )
func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
...@@ -27,12 +26,12 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -27,12 +26,12 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(sctx.SetTargets(r.Context(), targets)) r = r.WithContext(sctx.SetTargets(r.Context(), targets))
ctx := r.Context() ctx := r.Context()
addressHex := mux.Vars(r)["address"] nameOrHex := mux.Vars(r)["address"]
path := mux.Vars(r)["path"] path := mux.Vars(r)["path"]
address, err := swarm.ParseHexAddress(addressHex) address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
s.Logger.Debugf("bzz download: parse address %s: %v", addressHex, err) s.Logger.Debugf("bzz download: parse address %s: %v", nameOrHex, err)
s.Logger.Error("bzz download: parse address") s.Logger.Error("bzz download: parse address")
jsonhttp.BadRequest(w, "invalid address") jsonhttp.BadRequest(w, "invalid address")
return return
......
...@@ -23,10 +23,11 @@ import ( ...@@ -23,10 +23,11 @@ import (
) )
func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) {
addr := mux.Vars(r)["addr"] nameOrHex := mux.Vars(r)["addr"]
address, err := swarm.ParseHexAddress(addr)
address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
s.Logger.Debugf("chunk upload: parse chunk address %s: %v", addr, err) s.Logger.Debugf("chunk upload: parse chunk address %s: %v", nameOrHex, err)
s.Logger.Error("chunk upload: parse chunk address") s.Logger.Error("chunk upload: parse chunk address")
jsonhttp.BadRequest(w, "invalid chunk address") jsonhttp.BadRequest(w, "invalid chunk address")
return return
...@@ -79,12 +80,12 @@ func (s *server) chunkGetHandler(w http.ResponseWriter, r *http.Request) { ...@@ -79,12 +80,12 @@ func (s *server) chunkGetHandler(w http.ResponseWriter, r *http.Request) {
targets := r.URL.Query().Get("targets") targets := r.URL.Query().Get("targets")
r = r.WithContext(sctx.SetTargets(r.Context(), targets)) r = r.WithContext(sctx.SetTargets(r.Context(), targets))
addr := mux.Vars(r)["addr"] nameOrHex := mux.Vars(r)["addr"]
ctx := r.Context() ctx := r.Context()
address, err := swarm.ParseHexAddress(addr) address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
s.Logger.Debugf("chunk: parse chunk address %s: %v", addr, err) s.Logger.Debugf("chunk: parse chunk address %s: %v", nameOrHex, err)
s.Logger.Error("chunk: parse chunk address error") s.Logger.Error("chunk: parse chunk address error")
jsonhttp.BadRequest(w, "invalid chunk address") jsonhttp.BadRequest(w, "invalid chunk address")
return return
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
package api package api
import "github.com/ethersphere/bee/pkg/swarm"
type Server = server
type ( type (
BytesPostResponse = bytesPostResponse BytesPostResponse = bytesPostResponse
FileUploadResponse = fileUploadResponse FileUploadResponse = fileUploadResponse
...@@ -14,3 +18,12 @@ type ( ...@@ -14,3 +18,12 @@ type (
var ( var (
ContentTypeTar = contentTypeTar ContentTypeTar = contentTypeTar
) )
var (
ErrNoResolver = errNoResolver
ErrInvalidNameOrAddress = errInvalidNameOrAddress
)
func (s *Server) ResolveNameOrAddress(str string) (swarm.Address, error) {
return s.resolveNameOrAddress(str)
}
...@@ -221,11 +221,12 @@ type fileUploadInfo struct { ...@@ -221,11 +221,12 @@ type fileUploadInfo struct {
// fileDownloadHandler downloads the file given the entry's reference. // fileDownloadHandler downloads the file given the entry's reference.
func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) {
addr := mux.Vars(r)["addr"] nameOrHex := mux.Vars(r)["addr"]
address, err := swarm.ParseHexAddress(addr)
address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
s.Logger.Debugf("file download: parse file address %s: %v", addr, err) s.Logger.Debugf("file download: parse file address %s: %v", nameOrHex, err)
s.Logger.Errorf("file download: parse file address %s", addr) s.Logger.Errorf("file download: parse file address %s", nameOrHex)
jsonhttp.BadRequest(w, "invalid file address") jsonhttp.BadRequest(w, "invalid file address")
return return
} }
...@@ -238,16 +239,16 @@ func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -238,16 +239,16 @@ func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
_, err = file.JoinReadAll(r.Context(), j, address, buf) _, err = file.JoinReadAll(r.Context(), j, address, buf)
if err != nil { if err != nil {
s.Logger.Debugf("file download: read entry %s: %v", addr, err) s.Logger.Debugf("file download: read entry %s: %v", address, err)
s.Logger.Errorf("file download: read entry %s", addr) s.Logger.Errorf("file download: read entry %s", address)
jsonhttp.NotFound(w, nil) jsonhttp.NotFound(w, nil)
return return
} }
e := &entry.Entry{} e := &entry.Entry{}
err = e.UnmarshalBinary(buf.Bytes()) err = e.UnmarshalBinary(buf.Bytes())
if err != nil { if err != nil {
s.Logger.Debugf("file download: unmarshal entry %s: %v", addr, err) s.Logger.Debugf("file download: unmarshal entry %s: %v", address, err)
s.Logger.Errorf("file download: unmarshal entry %s", addr) s.Logger.Errorf("file download: unmarshal entry %s", address)
jsonhttp.InternalServerError(w, "error unmarshaling entry") jsonhttp.InternalServerError(w, "error unmarshaling entry")
return return
} }
...@@ -266,16 +267,16 @@ func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -266,16 +267,16 @@ func (s *server) fileDownloadHandler(w http.ResponseWriter, r *http.Request) {
buf = bytes.NewBuffer(nil) buf = bytes.NewBuffer(nil)
_, err = file.JoinReadAll(r.Context(), j, e.Metadata(), buf) _, err = file.JoinReadAll(r.Context(), j, e.Metadata(), buf)
if err != nil { if err != nil {
s.Logger.Debugf("file download: read metadata %s: %v", addr, err) s.Logger.Debugf("file download: read metadata %s: %v", nameOrHex, err)
s.Logger.Errorf("file download: read metadata %s", addr) s.Logger.Errorf("file download: read metadata %s", nameOrHex)
jsonhttp.NotFound(w, nil) jsonhttp.NotFound(w, nil)
return return
} }
metaData := &entry.Metadata{} metaData := &entry.Metadata{}
err = json.Unmarshal(buf.Bytes(), metaData) err = json.Unmarshal(buf.Bytes(), metaData)
if err != nil { if err != nil {
s.Logger.Debugf("file download: unmarshal metadata %s: %v", addr, err) s.Logger.Debugf("file download: unmarshal metadata %s: %v", nameOrHex, err)
s.Logger.Errorf("file download: unmarshal metadata %s", addr) s.Logger.Errorf("file download: unmarshal metadata %s", nameOrHex)
jsonhttp.InternalServerError(w, "error unmarshaling metadata") jsonhttp.InternalServerError(w, "error unmarshaling metadata")
return return
} }
......
...@@ -17,6 +17,8 @@ import ( ...@@ -17,6 +17,8 @@ import (
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
p2pmock "github.com/ethersphere/bee/pkg/p2p/mock" p2pmock "github.com/ethersphere/bee/pkg/p2p/mock"
"github.com/ethersphere/bee/pkg/pingpong" "github.com/ethersphere/bee/pkg/pingpong"
"github.com/ethersphere/bee/pkg/resolver"
resolverMock "github.com/ethersphere/bee/pkg/resolver/mock"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags" "github.com/ethersphere/bee/pkg/tags"
...@@ -30,6 +32,7 @@ type testServerOptions struct { ...@@ -30,6 +32,7 @@ type testServerOptions struct {
P2P *p2pmock.Service P2P *p2pmock.Service
Pingpong pingpong.Interface Pingpong pingpong.Interface
Storer storage.Storer Storer storage.Storer
Resolver resolver.Interface
TopologyOpts []topologymock.Option TopologyOpts []topologymock.Option
Tags *tags.Tags Tags *tags.Tags
AccountingOpts []accountingmock.Option AccountingOpts []accountingmock.Option
...@@ -65,7 +68,10 @@ func newTestServer(t *testing.T, o testServerOptions) *testServer { ...@@ -65,7 +68,10 @@ func newTestServer(t *testing.T, o testServerOptions) *testServer {
} }
func newBZZTestServer(t *testing.T, o testServerOptions) *http.Client { func newBZZTestServer(t *testing.T, o testServerOptions) *http.Client {
s := api.New(o.Tags, o.Storer, nil, logging.New(ioutil.Discard, 0), nil) if o.Resolver == nil {
o.Resolver = resolverMock.NewResolver()
}
s := api.New(o.Tags, o.Storer, o.Resolver, nil, logging.New(ioutil.Discard, 0), nil)
ts := httptest.NewServer(s) ts := httptest.NewServer(s)
t.Cleanup(ts.Close) t.Cleanup(ts.Close)
......
...@@ -37,6 +37,8 @@ import ( ...@@ -37,6 +37,8 @@ import (
"github.com/ethersphere/bee/pkg/pusher" "github.com/ethersphere/bee/pkg/pusher"
"github.com/ethersphere/bee/pkg/pushsync" "github.com/ethersphere/bee/pkg/pushsync"
"github.com/ethersphere/bee/pkg/recovery" "github.com/ethersphere/bee/pkg/recovery"
"github.com/ethersphere/bee/pkg/resolver"
resolverSvc "github.com/ethersphere/bee/pkg/resolver/service"
"github.com/ethersphere/bee/pkg/retrieval" "github.com/ethersphere/bee/pkg/retrieval"
"github.com/ethersphere/bee/pkg/settlement/pseudosettle" "github.com/ethersphere/bee/pkg/settlement/pseudosettle"
"github.com/ethersphere/bee/pkg/soc" "github.com/ethersphere/bee/pkg/soc"
...@@ -56,6 +58,7 @@ type Bee struct { ...@@ -56,6 +58,7 @@ type Bee struct {
p2pCancel context.CancelFunc p2pCancel context.CancelFunc
apiServer *http.Server apiServer *http.Server
debugAPIServer *http.Server debugAPIServer *http.Server
resolverCloser io.Closer
errorLogWriter *io.PipeWriter errorLogWriter *io.PipeWriter
tracerCloser io.Closer tracerCloser io.Closer
stateStoreCloser io.Closer stateStoreCloser io.Closer
...@@ -67,26 +70,27 @@ type Bee struct { ...@@ -67,26 +70,27 @@ type Bee struct {
} }
type Options struct { type Options struct {
DataDir string DataDir string
DBCapacity uint64 DBCapacity uint64
Password string Password string
APIAddr string APIAddr string
DebugAPIAddr string DebugAPIAddr string
Addr string Addr string
NATAddr string NATAddr string
EnableWS bool EnableWS bool
EnableQUIC bool EnableQUIC bool
WelcomeMessage string WelcomeMessage string
Bootnodes []string Bootnodes []string
CORSAllowedOrigins []string CORSAllowedOrigins []string
Logger logging.Logger Logger logging.Logger
Standalone bool Standalone bool
TracingEnabled bool TracingEnabled bool
TracingEndpoint string TracingEndpoint string
TracingServiceName string TracingServiceName string
GlobalPinningEnabled bool GlobalPinningEnabled bool
PaymentThreshold uint64 PaymentThreshold uint64
PaymentTolerance uint64 PaymentTolerance uint64
ResolverConnectionCfgs []*resolver.ConnectionConfig
} }
func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, swarmPrivateKey *ecdsa.PrivateKey, networkID uint64, logger logging.Logger, o Options) (*Bee, error) { func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, swarmPrivateKey *ecdsa.PrivateKey, networkID uint64, logger logging.Logger, o Options) (*Bee, error) {
...@@ -289,10 +293,13 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, ...@@ -289,10 +293,13 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
b.pullerCloser = puller b.pullerCloser = puller
multiResolver := resolverSvc.InitMultiResolver(logger, o.ResolverConnectionCfgs)
b.resolverCloser = multiResolver
var apiService api.Service var apiService api.Service
if o.APIAddr != "" { if o.APIAddr != "" {
// API server // API server
apiService = api.New(tagg, ns, o.CORSAllowedOrigins, logger, tracer) apiService = api.New(tagg, ns, multiResolver, o.CORSAllowedOrigins, logger, tracer)
apiListener, err := net.Listen("tcp", o.APIAddr) apiListener, err := net.Listen("tcp", o.APIAddr)
if err != nil { if err != nil {
return nil, fmt.Errorf("api listener: %w", err) return nil, fmt.Errorf("api listener: %w", err)
...@@ -380,6 +387,7 @@ func (b *Bee) Shutdown(ctx context.Context) error { ...@@ -380,6 +387,7 @@ func (b *Bee) Shutdown(ctx context.Context) error {
return nil return nil
}) })
} }
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {
errs.add(err) errs.add(err)
} }
...@@ -421,6 +429,13 @@ func (b *Bee) Shutdown(ctx context.Context) error { ...@@ -421,6 +429,13 @@ func (b *Bee) Shutdown(ctx context.Context) error {
errs.add(fmt.Errorf("error log writer: %w", err)) errs.add(fmt.Errorf("error log writer: %w", err))
} }
// Shutdown the resolver service only if it has been initialized.
if b.resolverCloser != nil {
if err := b.resolverCloser.Close(); err != nil {
errs.add(fmt.Errorf("resolver service: %w", err))
}
}
if errs.hasErrors() { if errs.hasErrors() {
return errs return errs
} }
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"github.com/ethersphere/bee/pkg/resolver"
)
// Interface is a resolver client that can connect/disconnect to an external
// Name Resolution Service via an edpoint.
type Interface interface {
resolver.Interface
Connect(endpoint string) error
IsConnected() bool
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ens
import (
"fmt"
"strings"
"sync"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethersphere/bee/pkg/resolver/client"
"github.com/ethersphere/bee/pkg/swarm"
)
// Address is the swarm bzz address.
type Address = swarm.Address
// Make sure Client implements the resolver.Client interface.
var _ client.Interface = (*Client)(nil)
type dialType func(string) (*ethclient.Client, error)
type resolveType func(bind.ContractBackend, string) (string, error)
// Client is a name resolution client that can connect to ENS via an
// Ethereum endpoint.
type Client struct {
mu sync.Mutex
Endpoint string
ethCl *ethclient.Client
dialFn dialType
resolveFn resolveType
}
// Option is a function that applies an option to a Client.
type Option func(*Client)
// NewClient will return a new Client.
func NewClient(opts ...Option) *Client {
c := &Client{
dialFn: wrapDial,
resolveFn: wrapResolve,
}
// Apply all options to the Client.
for _, o := range opts {
o(c)
}
return c
}
// Connect implements the resolver.Client interface.
func (c *Client) Connect(ep string) error {
if c.dialFn == nil {
return fmt.Errorf("dialFn: %w", errNotImplemented)
}
ethCl, err := c.dialFn(ep)
if err != nil {
return err
}
// Lock and set the parameters.
c.mu.Lock()
c.ethCl = ethCl
c.Endpoint = ep
c.mu.Unlock()
return nil
}
// IsConnected returns true if there is an active RPC connection with an
// Ethereum node at the configured endpoint.
// Function obtains a write lock while interacting with the Ethereum client.
func (c *Client) IsConnected() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.ethCl != nil
}
// Resolve implements the resolver.Client interface.
// Function obtains a read lock while interacting with the Ethereum client.
func (c *Client) Resolve(name string) (Address, error) {
if c.resolveFn == nil {
return swarm.ZeroAddress, fmt.Errorf("resolveFn: %w", errNotImplemented)
}
// Obtain our copy of the client under lock.
c.mu.Lock()
ethCl := c.ethCl
c.mu.Unlock()
hash, err := c.resolveFn(ethCl, name)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("%v: %w", err, ErrResolveFailed)
}
// In case the implementation returns a zero address return an NameNotFound
// error.
if hash == "" {
return swarm.ZeroAddress, fmt.Errorf("name %s: %w", name, ErrNameNotFound)
}
// Ensure that the content hash string is in a valid format, eg.
// "/swarm/<address>".
if !strings.HasPrefix(hash, "/swarm/") {
return swarm.ZeroAddress, fmt.Errorf("contenthash %s: %w", hash, ErrInvalidContentHash)
}
// Trim the prefix and try to parse the result as a bzz address.
return swarm.ParseHexAddress(strings.TrimPrefix(hash, "/swarm/"))
}
// Close closes the RPC connection with the client, terminating all unfinished
// requests.
// Function obtains a write lock while interacting with the Ethereum client.
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.ethCl != nil {
c.ethCl.Close() // TODO: consider mocking out the eth client.
}
c.ethCl = nil
return nil
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build integration
package ens_test
import (
"strings"
"testing"
"github.com/ethersphere/bee/pkg/resolver/client/ens"
)
func TestENSntegration(t *testing.T) {
// TODO: consider using a stable gateway instead of INFURA.
defaultEndpoint := "https://goerli.infura.io/v3/59d83a5a4be74f86b9851190c802297b"
testCases := []struct {
desc string
endpoint string
name string
wantAdr string
wantFailConnect bool
wantFailResolve bool
}{
{
desc: "bad ethclient endpoint",
endpoint: "fail",
wantFailConnect: true,
},
{
desc: "no domain",
name: "idonthaveadomain",
wantFailResolve: true,
},
{
desc: "no eth domain",
name: "centralized.com",
wantFailResolve: true,
},
{
desc: "not registered",
name: "unused.test.swarm.eth",
wantFailResolve: true,
},
{
desc: "no content hash",
name: "nocontent.resolver.test.swarm.eth",
wantFailResolve: true,
},
{
desc: "ok",
name: "example.resolver.test.swarm.eth",
wantAdr: "00cb23598c2e520b6a6aae3ddc94fed4435a2909690bdd709bf9d9e7c2aadfad",
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
if tC.endpoint == "" {
tC.endpoint = defaultEndpoint
}
eC := ens.NewClient()
defer eC.Close()
err := eC.Connect(tC.endpoint)
if err != nil {
if !tC.wantFailConnect {
t.Fatalf("failed to connect: %v", err)
}
return
}
addr, err := eC.Resolve(tC.name)
if err != nil {
if !tC.wantFailResolve {
t.Fatalf("failed to resolve name: %v", err)
}
return
}
want := strings.ToLower(tC.wantAdr)
got := strings.ToLower(addr.String())
if got != want {
t.Errorf("bad addr: got %q, want %q", got, want)
}
eC.Close()
if eC.IsConnected() {
t.Errorf("IsConnected: got true, want false")
}
})
}
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ens_test
import (
"errors"
"testing"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethersphere/bee/pkg/resolver/client/ens"
"github.com/ethersphere/bee/pkg/swarm"
)
func TestNewClient(t *testing.T) {
cl := ens.NewClient()
if cl.Endpoint != "" {
t.Errorf("expected no endpoint set")
}
}
func TestConnect(t *testing.T) {
ep := "test"
t.Run("no dial func error", func(t *testing.T) {
c := ens.NewClient(
ens.WithDialFunc(nil),
)
err := c.Connect(ep)
defer c.Close()
if !errors.Is(err, ens.ErrNotImplemented) {
t.Fatal("expected correct error")
}
})
t.Run("connect error", func(t *testing.T) {
c := ens.NewClient(
ens.WithErrorDialFunc(errors.New("failed to connect")),
)
if err := c.Connect("test"); err == nil {
t.Fatal("expected error")
}
c.Close()
})
t.Run("ok", func(t *testing.T) {
c := ens.NewClient(
ens.WithNoopDialFunc(),
)
if err := c.Connect(ep); err != nil {
t.Fatal(err)
}
// Override the eth client to test connection.
ens.SetEthClient(c, &ethclient.Client{})
if c.Endpoint != ep {
t.Errorf("bad endpoint: got %q, want %q", c.Endpoint, ep)
}
if !c.IsConnected() {
t.Error("IsConnected: got false, want true")
}
// We are not really connected, so clear the client to prevent panic.
ens.SetEthClient(c, nil)
c.Close()
if c.IsConnected() {
t.Error("IsConnected: got true, want false")
}
})
}
func TestResolve(t *testing.T) {
name := "hello"
bzzAddress := swarm.MustParseHexAddress(
"6f4eeb99d0a144d78ac33cf97091a59a6291aa78929938defcf967e74326e08b",
)
t.Run("no resolve func error", func(t *testing.T) {
c := ens.NewClient(
ens.WithResolveFunc(nil),
)
_, err := c.Resolve("test")
if !errors.Is(err, ens.ErrNotImplemented) {
t.Fatal("expected correct error")
}
})
t.Run("resolve error", func(t *testing.T) {
c := ens.NewClient(
ens.WithNoopDialFunc(),
ens.WithErrorResolveFunc(errors.New("resolve error")),
)
if err := c.Connect(name); err != nil {
t.Fatal(err)
}
defer c.Close()
_, err := c.Resolve(name)
if !errors.Is(err, ens.ErrResolveFailed) {
t.Error("expected resolve error")
}
})
t.Run("zero address returned", func(t *testing.T) {
c := ens.NewClient(
ens.WithNoopDialFunc(),
ens.WithZeroAdrResolveFunc(),
)
if err := c.Connect(name); err != nil {
t.Fatal(err)
}
defer c.Close()
_, err := c.Resolve(name)
if !errors.Is(err, ens.ErrNameNotFound) {
t.Error("expected name not found error")
}
})
t.Run("resolved without address prefix error", func(t *testing.T) {
c := ens.NewClient(
ens.WithNoopDialFunc(),
ens.WithNoprefixAdrResolveFunc(bzzAddress),
)
if err := c.Connect(name); err != nil {
t.Fatal(err)
}
defer c.Close()
_, err := c.Resolve(name)
if err == nil {
t.Error("expected error")
}
})
t.Run("ok", func(t *testing.T) {
c := ens.NewClient(
ens.WithNoopDialFunc(),
ens.WithValidAdrResolveFunc(bzzAddress),
)
if err := c.Connect(name); err != nil {
t.Fatal(err)
}
defer c.Close()
addr, err := c.Resolve(name)
if err != nil {
t.Error(err)
}
want := bzzAddress.String()
got := addr.String()
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ens
import (
"errors"
)
var (
// ErrInvalidContentHash denotes that the value of the contenthash record is
// not valid.
ErrInvalidContentHash = errors.New("invalid swarm content hash")
// ErrResolveFailed is returned when a name could not be resolved.
ErrResolveFailed = errors.New("resolve failed")
// ErrNameNotFound is returned when a name resolves to an empty contenthash
// record.
ErrNameNotFound = errors.New("name not found")
)
var (
// errNotImplemented denotes that the function has not been implemented.
errNotImplemented = errors.New("function not implemented")
)
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ens
import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethersphere/bee/pkg/resolver"
"github.com/ethersphere/bee/pkg/swarm"
)
var (
ErrNotImplemented = errNotImplemented
)
func SetEthClient(c *Client, ethCl *ethclient.Client) {
c.ethCl = ethCl
}
// WithDialFunc will set the Dial function implementaton.
func WithDialFunc(fn func(ep string) (*ethclient.Client, error)) Option {
return func(c *Client) {
c.dialFn = fn
}
}
func WithErrorDialFunc(err error) Option {
return WithDialFunc(func(ep string) (*ethclient.Client, error) {
return nil, err
})
}
func WithNoopDialFunc() Option {
return WithDialFunc(func(ep string) (*ethclient.Client, error) {
return nil, nil
})
}
// WithResolveFunc will set the Resolve function implementation.
func WithResolveFunc(fn func(backend bind.ContractBackend, input string) (string, error)) Option {
return func(c *Client) {
c.resolveFn = fn
}
}
func WithErrorResolveFunc(err error) Option {
return WithResolveFunc(func(backend bind.ContractBackend, input string) (string, error) {
return "", err
})
}
func WithZeroAdrResolveFunc() Option {
return WithResolveFunc(func(backend bind.ContractBackend, input string) (string, error) {
return swarm.ZeroAddress.String(), nil
})
}
func WithNoprefixAdrResolveFunc(addr resolver.Address) Option {
return WithResolveFunc(func(backend bind.ContractBackend, input string) (string, error) {
return addr.String(), nil
})
}
func WithValidAdrResolveFunc(addr resolver.Address) Option {
return WithResolveFunc(func(backend bind.ContractBackend, input string) (string, error) {
return "/swarm/" + addr.String(), nil
})
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ens
import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
goens "github.com/wealdtech/go-ens/v3"
)
func wrapDial(ep string) (*ethclient.Client, error) {
// Open a connection to the ethereum node through the endpoint.
cl, err := ethclient.Dial(ep)
if err != nil {
return nil, err
}
// Ensure the ENS resolver contract is deployed on the network we are now
// connected to.
if _, err := goens.PublicResolverAddress(cl); err != nil {
return nil, err
}
return cl, nil
}
func wrapResolve(backend bind.ContractBackend, name string) (string, error) {
// Connect to the ENS resolver for the provided name.
ensR, err := goens.NewResolver(backend, name)
if err != nil {
return "", err
}
// Try and read out the content hash record.
ch, err := ensR.Contenthash()
if err != nil {
return "", err
}
addr, err := goens.ContenthashToString(ch)
if err != nil {
return "", err
}
return addr, nil
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver
import (
"fmt"
"strings"
"unicode"
"github.com/ethereum/go-ethereum/common"
)
// Defined as per RFC 1034. For reference, see:
// https://en.wikipedia.org/wiki/Domain_Name_System#cite_note-rfc1034-1
const maxTLDLength = 63
// ConnectionConfig contains the TLD, endpoint and contract address used to
// establish to a resolver.
type ConnectionConfig struct {
TLD string
Address string
Endpoint string
}
// ParseConnectionString will try to parse a connection string used to connect
// the Resolver to a name resolution service. The resulting config can be
// used to initialize a resovler Service.
func parseConnectionString(cs string) (*ConnectionConfig, error) {
isAllUnicodeLetters := func(s string) bool {
for _, r := range s {
if !unicode.IsLetter(r) {
return false
}
}
return true
}
endpoint := cs
var tld string
var addr string
// Split TLD and Endpoint strings.
if i := strings.Index(endpoint, ":"); i > 0 {
// Make sure not to grab the protocol, as it contains "://"!
// Eg. in http://... the "http" is NOT a tld.
if isAllUnicodeLetters(endpoint[:i]) && len(endpoint) > i+2 && endpoint[i+1:i+3] != "//" {
tld = endpoint[:i]
if len(tld) > maxTLDLength {
return nil, fmt.Errorf("%w: %s", ErrTLDTooLong, tld)
}
endpoint = endpoint[i+1:]
}
}
// Split the address string.
if i := strings.Index(endpoint, "@"); i > 0 {
addr = common.HexToAddress(endpoint[:i]).String()
endpoint = endpoint[i+1:]
}
return &ConnectionConfig{
Endpoint: endpoint,
Address: addr,
TLD: tld,
}, nil
}
// ParseConnectionStrings will apply ParseConnectionString to each connection
// string. Returns first error found.
func ParseConnectionStrings(cstrs []string) ([]*ConnectionConfig, error) {
var res []*ConnectionConfig
for _, cs := range cstrs {
cfg, err := parseConnectionString(cs)
if err != nil {
return nil, err
}
res = append(res, cfg)
}
return res, nil
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver_test
import (
"errors"
"testing"
"github.com/ethersphere/bee/pkg/resolver"
)
func TestParseConnectionStrings(t *testing.T) {
testCases := []struct {
desc string
conStrings []string
wantCfg []resolver.ConnectionConfig
wantErr error
}{
{
// Defined as per RFC 1034. For reference, see:
// https://en.wikipedia.org/wiki/Domain_Name_System#cite_note-rfc1034-1
desc: "tld too long",
conStrings: []string{
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:example.com",
},
wantErr: resolver.ErrTLDTooLong,
},
{
desc: "single endpoint default tld",
conStrings: []string{
"https://example.com",
},
wantCfg: []resolver.ConnectionConfig{
{
TLD: "",
Endpoint: "https://example.com",
},
},
},
{
desc: "single endpoint explicit tld",
conStrings: []string{
"tld:https://example.com",
},
wantCfg: []resolver.ConnectionConfig{
{
TLD: "tld",
Endpoint: "https://example.com",
},
},
},
{
desc: "single endpoint with address default tld",
conStrings: []string{
"0x314159265dD8dbb310642f98f50C066173C1259b@https://example.com",
},
wantCfg: []resolver.ConnectionConfig{
{
TLD: "",
Address: "0x314159265dD8dbb310642f98f50C066173C1259b",
Endpoint: "https://example.com",
},
},
},
{
desc: "single endpoint with address explicit tld",
conStrings: []string{
"tld:0x314159265dD8dbb310642f98f50C066173C1259b@https://example.com",
},
wantCfg: []resolver.ConnectionConfig{
{
TLD: "tld",
Address: "0x314159265dD8dbb310642f98f50C066173C1259b",
Endpoint: "https://example.com",
},
},
},
{
desc: "mixed",
conStrings: []string{
"tld:https://example.com",
"testdomain:wowzers.map",
"yesyesyes:0x314159265dD8dbb310642f98f50C066173C1259b@2.2.2.2",
"cloudflare-ethereum.org",
},
wantCfg: []resolver.ConnectionConfig{
{
TLD: "tld",
Endpoint: "https://example.com",
},
{
TLD: "testdomain",
Endpoint: "wowzers.map",
},
{
TLD: "yesyesyes",
Address: "0x314159265dD8dbb310642f98f50C066173C1259b",
Endpoint: "2.2.2.2",
},
{
TLD: "",
Endpoint: "cloudflare-ethereum.org",
},
},
},
{
desc: "mixed with error",
conStrings: []string{
"tld:https://example.com",
"testdomain:wowzers.map",
"nonononononononononononononononononononononononononononononononononono:yes",
},
wantErr: resolver.ErrTLDTooLong,
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
got, err := resolver.ParseConnectionStrings(tC.conStrings)
if err != nil {
if !errors.Is(err, tC.wantErr) {
t.Errorf("got error %v", err)
}
return
}
for i, el := range got {
want := tC.wantCfg[i]
got := el
if got.TLD != want.TLD {
t.Errorf("got %q, want %q", got.TLD, want.TLD)
}
if got.Address != want.Address {
t.Errorf("got %q, want %q", got.Address, want.Address)
}
if got.Endpoint != want.Endpoint {
t.Errorf("got %q, want %q", got.Endpoint, want.Endpoint)
}
}
})
}
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver
import (
"errors"
"fmt"
"strings"
)
var (
// ErrTLDTooLong denotes when a TLD in a name exceeds maximum length.
ErrTLDTooLong = fmt.Errorf("TLD exceeds maximum length of %d characters", maxTLDLength)
// ErrInvalidTLD denotes passing an invalid TLD to the MultiResolver.
ErrInvalidTLD = errors.New("invalid TLD")
// ErrResolverChainEmpty denotes trying to pop an empty resolver chain.
ErrResolverChainEmpty = errors.New("resolver chain empty")
)
// CloseError denotes that at least one resolver in the MultiResolver has
// had an error when Close was called.
type CloseError struct {
errs []error
}
func (me CloseError) add(err error) {
if err != nil {
me.errs = append(me.errs, err)
}
}
func (me CloseError) errorOrNil() error {
if len(me.errs) > 0 {
return me
}
return nil
}
// Error returns a formatted multi close error.
func (me CloseError) Error() string {
if len(me.errs) == 0 {
return ""
}
var b strings.Builder
b.WriteString("multiresolver failed to close: ")
for _, e := range me.errs {
b.WriteString(e.Error())
b.WriteString("; ")
}
return b.String()
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package mock package mock
import ( import (
"errors"
"fmt" "fmt"
"github.com/ethersphere/bee/pkg/resolver" "github.com/ethersphere/bee/pkg/resolver"
...@@ -13,8 +14,12 @@ import ( ...@@ -13,8 +14,12 @@ import (
// Assure mock Resolver implements the Resolver interface. // Assure mock Resolver implements the Resolver interface.
var _ resolver.Interface = (*Resolver)(nil) var _ resolver.Interface = (*Resolver)(nil)
// ErrNotImplemented denotes a function has not been implemented.
var ErrNotImplemented = errors.New("not implemented")
// Resolver is the mock Resolver implementation. // Resolver is the mock Resolver implementation.
type Resolver struct { type Resolver struct {
IsClosed bool
resolveFunc func(string) (resolver.Address, error) resolveFunc func(string) (resolver.Address, error)
} }
...@@ -45,5 +50,12 @@ func (r *Resolver) Resolve(name string) (resolver.Address, error) { ...@@ -45,5 +50,12 @@ func (r *Resolver) Resolve(name string) (resolver.Address, error) {
if r.resolveFunc != nil { if r.resolveFunc != nil {
return r.resolveFunc(name) return r.resolveFunc(name)
} }
return resolver.Address{}, fmt.Errorf("not implemented") return resolver.Address{}, fmt.Errorf("resolveFunc: %w", ErrNotImplemented)
}
// Close implements the Resolver interface.
func (r *Resolver) Close() error {
r.IsClosed = true
return nil
} }
...@@ -5,20 +5,19 @@ ...@@ -5,20 +5,19 @@
package resolver package resolver
import ( import (
"errors" "fmt"
"path" "path"
"strings" "strings"
)
// MultiResolver errors. "github.com/ethersphere/bee/pkg/swarm"
var (
ErrInvalidTLD = errors.New("invalid TLD")
ErrResolverChainEmpty = errors.New("resolver chain empty")
) )
// Ensure MultiResolver implements Resolver interface.
var _ Interface = (*MultiResolver)(nil)
type resolverMap map[string][]Interface type resolverMap map[string][]Interface
// MultiResolver performs name resolutions based on the TLD of the URL. // MultiResolver performs name resolutions based on the TLD label in the name.
type MultiResolver struct { type MultiResolver struct {
resolvers resolverMap resolvers resolverMap
// ForceDefault will force all names to be resolved by the default // ForceDefault will force all names to be resolved by the default
...@@ -43,8 +42,7 @@ func NewMultiResolver(opts ...Option) *MultiResolver { ...@@ -43,8 +42,7 @@ func NewMultiResolver(opts ...Option) *MultiResolver {
return mr return mr
} }
// WithForceDefault will force resolution using the default resolver // WithForceDefault will force resolution using the default resolver chain.
// chain.
func WithForceDefault() Option { func WithForceDefault() Option {
return func(mr *MultiResolver) { return func(mr *MultiResolver) {
mr.ForceDefault = true mr.ForceDefault = true
...@@ -57,7 +55,7 @@ func WithForceDefault() Option { ...@@ -57,7 +55,7 @@ func WithForceDefault() Option {
// to the default resolver chain. // to the default resolver chain.
func (mr *MultiResolver) PushResolver(tld string, r Interface) error { func (mr *MultiResolver) PushResolver(tld string, r Interface) error {
if tld != "" && !isTLD(tld) { if tld != "" && !isTLD(tld) {
return ErrInvalidTLD return fmt.Errorf("tld %s: %w", tld, ErrInvalidTLD)
} }
mr.resolvers[tld] = append(mr.resolvers[tld], r) mr.resolvers[tld] = append(mr.resolvers[tld], r)
...@@ -70,12 +68,12 @@ func (mr *MultiResolver) PushResolver(tld string, r Interface) error { ...@@ -70,12 +68,12 @@ func (mr *MultiResolver) PushResolver(tld string, r Interface) error {
// from the default resolver chain. // from the default resolver chain.
func (mr *MultiResolver) PopResolver(tld string) error { func (mr *MultiResolver) PopResolver(tld string) error {
if tld != "" && !isTLD(tld) { if tld != "" && !isTLD(tld) {
return ErrInvalidTLD return fmt.Errorf("tld %s: %w", tld, ErrInvalidTLD)
} }
l := len(mr.resolvers[tld]) l := len(mr.resolvers[tld])
if l == 0 { if l == 0 {
return ErrResolverChainEmpty return fmt.Errorf("tld %s: %w", tld, ErrResolverChainEmpty)
} }
mr.resolvers[tld] = mr.resolvers[tld][:l-1] mr.resolvers[tld] = mr.resolvers[tld][:l-1]
return nil return nil
...@@ -103,26 +101,41 @@ func (mr *MultiResolver) GetChain(tld string) []Interface { ...@@ -103,26 +101,41 @@ func (mr *MultiResolver) GetChain(tld string) []Interface {
// returning the result of the first Resolver that succeeds. If all resolvers // returning the result of the first Resolver that succeeds. If all resolvers
// in the chain return an error, the function will return an ErrResolveFailed. // in the chain return an error, the function will return an ErrResolveFailed.
func (mr *MultiResolver) Resolve(name string) (Address, error) { func (mr *MultiResolver) Resolve(name string) (Address, error) {
tld := "" tld := ""
if !mr.ForceDefault { if !mr.ForceDefault {
tld = getTLD(name) tld = getTLD(name)
} }
chain := mr.resolvers[tld] chain := mr.resolvers[tld]
// If no resolver chain is found, switch to the default chain.
if len(chain) == 0 { if len(chain) == 0 {
return Address{}, ErrResolverChainEmpty chain = mr.resolvers[""]
} }
addr := swarm.ZeroAddress
var err error var err error
for _, res := range chain { for _, res := range chain {
adr, err := res.Resolve(name) addr, err = res.Resolve(name)
if err == nil { if err == nil {
return adr, nil return addr, nil
}
}
return addr, err
}
// Close all will call Close on all resolvers in all resolver chains.
func (mr *MultiResolver) Close() error {
errs := new(CloseError)
for _, chain := range mr.resolvers {
for _, r := range chain {
errs.add(r.Close())
} }
} }
// TODO: consider wrapping errors from the resolver chain. return errs.errorOrNil()
return Address{}, err
} }
func isTLD(tld string) bool { func isTLD(tld string) bool {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package resolver_test package resolver_test
import ( import (
"errors"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
...@@ -62,7 +63,7 @@ func TestPushResolver(t *testing.T) { ...@@ -62,7 +63,7 @@ func TestPushResolver(t *testing.T) {
want := mock.NewResolver() want := mock.NewResolver()
err := mr.PushResolver(tC.tld, want) err := mr.PushResolver(tC.tld, want)
if err != nil { if err != nil {
if err != tC.wantErr { if !errors.Is(err, tC.wantErr) {
t.Fatal(err) t.Fatal(err)
} }
return return
...@@ -87,30 +88,27 @@ func TestPopResolver(t *testing.T) { ...@@ -87,30 +88,27 @@ func TestPopResolver(t *testing.T) {
mr := resolver.NewMultiResolver() mr := resolver.NewMultiResolver()
t.Run("error on bad tld", func(t *testing.T) { t.Run("error on bad tld", func(t *testing.T) {
err := mr.PopResolver("invalid") if err := mr.PopResolver("invalid"); !errors.Is(err, resolver.ErrInvalidTLD) {
want := resolver.ErrInvalidTLD t.Fatal("invalid error type")
if err != want {
t.Fatalf("bad error: got %v, want %v", err, want)
} }
}) })
t.Run("error on empty", func(t *testing.T) { t.Run("error on empty", func(t *testing.T) {
err := mr.PopResolver(".tld") if err := mr.PopResolver(".tld"); !errors.Is(err, resolver.ErrResolverChainEmpty) {
want := resolver.ErrResolverChainEmpty t.Fatal("invalid error type")
if err != want {
t.Fatalf("bad error: got %v, want %v", err, want)
} }
}) })
} }
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
testAdr := newAddr("aaaabbbbccccdddd") addr := newAddr("aaaabbbbccccdddd")
testAdrAlt := newAddr("ddddccccbbbbaaaa") addrAlt := newAddr("ddddccccbbbbaaaa")
errUnregisteredName := fmt.Errorf("unregistered name")
newOKResolver := func(adr Address) resolver.Interface { newOKResolver := func(addr Address) resolver.Interface {
return mock.NewResolver( return mock.NewResolver(
mock.WithResolveFunc(func(_ string) (Address, error) { mock.WithResolveFunc(func(_ string) (Address, error) {
return adr, nil return addr, nil
}), }),
) )
} }
...@@ -118,7 +116,14 @@ func TestResolve(t *testing.T) { ...@@ -118,7 +116,14 @@ func TestResolve(t *testing.T) {
return mock.NewResolver( return mock.NewResolver(
mock.WithResolveFunc(func(name string) (Address, error) { mock.WithResolveFunc(func(name string) (Address, error) {
err := fmt.Errorf("name resolution failed for %q", name) err := fmt.Errorf("name resolution failed for %q", name)
return Address{}, err return swarm.ZeroAddress, err
}),
)
}
newUnregisteredNameResolver := func() resolver.Interface {
return mock.NewResolver(
mock.WithResolveFunc(func(name string) (Address, error) {
return swarm.ZeroAddress, errUnregisteredName
}), }),
) )
} }
...@@ -132,37 +137,43 @@ func TestResolve(t *testing.T) { ...@@ -132,37 +137,43 @@ func TestResolve(t *testing.T) {
// Default chain: // Default chain:
tld: "", tld: "",
res: []resolver.Interface{ res: []resolver.Interface{
newOKResolver(testAdr), newOKResolver(addr),
}, },
expectAdr: testAdr, expectAdr: addr,
}, },
{ {
tld: ".tld", tld: ".tld",
res: []resolver.Interface{ res: []resolver.Interface{
newErrResolver(), newErrResolver(),
newErrResolver(), newErrResolver(),
newOKResolver(testAdr), newOKResolver(addr),
}, },
expectAdr: testAdr, expectAdr: addr,
}, },
{ {
tld: ".good", tld: ".good",
res: []resolver.Interface{ res: []resolver.Interface{
newOKResolver(testAdr), newOKResolver(addr),
newOKResolver(testAdrAlt), newOKResolver(addrAlt),
}, },
expectAdr: testAdr, expectAdr: addr,
}, },
{ {
tld: ".empty", tld: ".empty",
}, },
{ {
tld: ".errors", tld: ".dies",
res: []resolver.Interface{ res: []resolver.Interface{
newErrResolver(), newErrResolver(),
newErrResolver(), newErrResolver(),
}, },
}, },
{
tld: ".unregistered",
res: []resolver.Interface{
newUnregisteredNameResolver(),
},
},
} }
testCases := []struct { testCases := []struct {
...@@ -170,33 +181,39 @@ func TestResolve(t *testing.T) { ...@@ -170,33 +181,39 @@ func TestResolve(t *testing.T) {
wantAdr Address wantAdr Address
wantErr error wantErr error
}{ }{
// {
// name: "",
// wantAdr: testAdr,
// },
// {
// name: "hello",
// wantAdr: testAdr,
// },
// {
// name: "example.tld",
// wantAdr: testAdr,
// },
// {
// name: ".tld",
// wantAdr: testAdr,
// },
// {
// name: "get.good",
// wantAdr: testAdr,
// },
// {
// // Switch to the default chain:
// name: "this.empty",
// wantAdr: testAdr,
// },
// {
// name: "this.dies",
// wantErr: fmt.Errorf("Failed to resolve name %q", "this.dies"),
// },
{ {
name: "", name: "iam.unregistered",
wantAdr: testAdr, wantAdr: swarm.ZeroAddress,
}, wantErr: errUnregisteredName,
{
name: "hello",
wantAdr: testAdr,
},
{
name: "example.tld",
wantAdr: testAdr,
},
{
name: ".tld",
wantAdr: testAdr,
},
{
name: "get.good",
wantAdr: testAdr,
},
{
name: "this.empty",
wantErr: resolver.ErrResolverChainEmpty,
},
{
name: "this.errors",
wantErr: fmt.Errorf("name resolution failed for %q", "this.errors"),
}, },
} }
...@@ -212,18 +229,31 @@ func TestResolve(t *testing.T) { ...@@ -212,18 +229,31 @@ func TestResolve(t *testing.T) {
for _, tC := range testCases { for _, tC := range testCases {
t.Run(tC.name, func(t *testing.T) { t.Run(tC.name, func(t *testing.T) {
adr, err := mr.Resolve(tC.name) addr, err := mr.Resolve(tC.name)
if err != nil { if err != nil {
if tC.wantErr == nil { if tC.wantErr == nil {
t.Fatalf("unexpected error: got %v", err) t.Fatalf("unexpected error: got %v", err)
} }
if err.Error() != tC.wantErr.Error() { if !errors.Is(err, tC.wantErr) {
t.Fatalf("got %v, want %v", err, tC.wantErr) t.Fatalf("got %v, want %v", err, tC.wantErr)
} }
} }
if !adr.Equal(tC.wantAdr) { if !addr.Equal(tC.wantAdr) {
t.Errorf("got %q, want %q", adr, tC.wantAdr) t.Errorf("got %q, want %q", addr, tC.wantAdr)
} }
}) })
} }
t.Run("close all", func(t *testing.T) {
if err := mr.Close(); err != nil {
t.Fatal(err)
}
for _, tE := range testFixture {
for _, r := range mr.GetChain(tE.tld) {
if !r.(*mock.Resolver).IsClosed {
t.Errorf("expected %q resolver closed", tE.tld)
}
}
}
})
} }
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
package resolver package resolver
import ( import (
"io"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
) )
...@@ -14,4 +16,5 @@ type Address = swarm.Address ...@@ -14,4 +16,5 @@ type Address = swarm.Address
// Interface can resolve an URL into an associated Ethereum address. // Interface can resolve an URL into an associated Ethereum address.
type Interface interface { type Interface interface {
Resolve(url string) (Address, error) Resolve(url string) (Address, error)
io.Closer
} }
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package service
import (
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/resolver"
"github.com/ethersphere/bee/pkg/resolver/client/ens"
)
// InitMultiResolver will create a new MultiResolver, create the appropriate
// resolvers, push them to the resolver chains and attempt to connect.
func InitMultiResolver(logger logging.Logger, cfgs []*resolver.ConnectionConfig) resolver.Interface {
if len(cfgs) == 0 {
logger.Info("name resolver: no name resolution service provided")
return nil
}
// Create a new MultiResolver.
mr := resolver.NewMultiResolver()
connectENS := func(tld string, ep string) {
ensCl := ens.NewClient()
logger.Debugf("name resolver: resolver for %q: connecting to endpoint %s", tld, ep)
if err := ensCl.Connect(ep); err != nil {
logger.Errorf("name resolver: resolver for %q domain: failed to connect to %q: %v", tld, ep, err)
} else {
logger.Infof("name resolver: resolver for %q domain: connected to %s", tld, ep)
if err := mr.PushResolver(tld, ensCl); err != nil {
logger.Errorf("name resolver: failed to push resolver to %q resolver chain: %v", tld, err)
}
}
}
// Attempt to conect to each resolver using the connection string.
for _, c := range cfgs {
// Warn user that the resolver address field is not used.
if c.Address != "" {
logger.Warningf("name resolver: connection string %q contains resolver address field, which is currently unused", c.Address)
}
// Select the appropriate resolver.
switch c.TLD {
case "eth":
// FIXME: MultiResolver expects "." in front of the TLD label.
connectENS("."+c.TLD, c.Endpoint)
case "":
connectENS("", c.Endpoint)
default:
logger.Errorf("default domain resolution not supported")
}
}
return mr
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment