Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add routing filtering to delegated routing server IPIP-484 #671

Merged
merged 21 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The following emojis are used to highlight certain changes:

### Added

- `routing/http`: added support for address and protocol filtering to the delegated routing server ([IPIP-484](https://github.com/ipfs/specs/pull/484)) [#671](https://github.com/ipfs/boxo/pull/671)

### Changed

### Removed
Expand Down
8 changes: 3 additions & 5 deletions routing/http/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,7 @@ func TestClient_FindProviders(t *testing.T) {
}

bitswapRecord := makeBitswapRecord()
bitswapProviders := []iter.Result[types.Record]{
{Val: &bitswapRecord},
}
peerRecordFromBitswapRecord := types.FromBitswapRecord(&bitswapRecord)

cases := []struct {
name string
Expand All @@ -254,8 +252,8 @@ func TestClient_FindProviders(t *testing.T) {
},
{
name: "happy case (with deprecated bitswap schema)",
routerResult: bitswapProviders,
expResult: bitswapProviders,
routerResult: []iter.Result[types.Record]{{Val: &bitswapRecord}},
expResult: []iter.Result[types.Record]{{Val: peerRecordFromBitswapRecord}},
expStreamingResponse: true,
},
{
Expand Down
197 changes: 197 additions & 0 deletions routing/http/server/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package server

import (
"reflect"
"slices"
"strings"

"github.com/ipfs/boxo/routing/http/types"
"github.com/ipfs/boxo/routing/http/types/iter"
"github.com/multiformats/go-multiaddr"
)

// filters implements IPIP-0484

func parseFilter(param string) []string {
if param == "" {
return nil
}
return strings.Split(strings.ToLower(param), ",")
}

// applyFiltersToIter applies the filters to the given iterator and returns a new iterator.
//
// The function iterates over the input iterator, applying the specified filters to each record.
// It supports both positive and negative filters for both addresses and protocols.
//
// Parameters:
// - recordsIter: An iterator of types.Record to be filtered.
// - filterAddrs: A slice of strings representing the address filter criteria.
// - filterProtocols: A slice of strings representing the protocol filter criteria.
func applyFiltersToIter(recordsIter iter.ResultIter[types.Record], filterAddrs, filterProtocols []string) iter.ResultIter[types.Record] {
mappedIter := iter.Map(recordsIter, func(v iter.Result[types.Record]) iter.Result[types.Record] {
if v.Err != nil || v.Val == nil {
gammazero marked this conversation as resolved.
Show resolved Hide resolved
return v
}

Check warning on line 35 in routing/http/server/filters.go

View check run for this annotation

Codecov / codecov/patch

routing/http/server/filters.go#L34-L35

Added lines #L34 - L35 were not covered by tests

switch v.Val.GetSchema() {
case types.SchemaPeer:
record, ok := v.Val.(*types.PeerRecord)
if !ok {
logger.Errorw("problem casting find providers record", "Schema", v.Val.GetSchema(), "Type", reflect.TypeOf(v).String())
// drop failed type assertion
return iter.Result[types.Record]{}
}

Check warning on line 44 in routing/http/server/filters.go

View check run for this annotation

Codecov / codecov/patch

routing/http/server/filters.go#L41-L44

Added lines #L41 - L44 were not covered by tests

record = applyFilters(record, filterAddrs, filterProtocols)
if record == nil {
return iter.Result[types.Record]{}
}
v.Val = record

//lint:ignore SA1019 // ignore staticcheck
case types.SchemaBitswap:
//lint:ignore SA1019 // ignore staticcheck
record, ok := v.Val.(*types.BitswapRecord)
if !ok {
logger.Errorw("problem casting find providers record", "Schema", v.Val.GetSchema(), "Type", reflect.TypeOf(v).String())
// drop failed type assertion
return iter.Result[types.Record]{}
}

Check warning on line 60 in routing/http/server/filters.go

View check run for this annotation

Codecov / codecov/patch

routing/http/server/filters.go#L57-L60

Added lines #L57 - L60 were not covered by tests
peerRecord := types.FromBitswapRecord(record)
peerRecord = applyFilters(peerRecord, filterAddrs, filterProtocols)
if peerRecord == nil {
return iter.Result[types.Record]{}
}

Check warning on line 65 in routing/http/server/filters.go

View check run for this annotation

Codecov / codecov/patch

routing/http/server/filters.go#L64-L65

Added lines #L64 - L65 were not covered by tests
v.Val = peerRecord
}
return v
})

// filter out nil results and errors
filteredIter := iter.Filter(mappedIter, func(v iter.Result[types.Record]) bool {
return v.Err == nil && v.Val != nil
})

return filteredIter
}

// Applies the filters. Returns nil if the provider does not pass the protocols filter
// The address filter is more complicated because it potentially modifies the Addrs slice.
func applyFilters(provider *types.PeerRecord, filterAddrs, filterProtocols []string) *types.PeerRecord {
if len(filterAddrs) == 0 && len(filterProtocols) == 0 {
return provider
}

if !protocolsAllowed(provider.Protocols, filterProtocols) {
// If the provider doesn't match any of the passed protocols, the provider is omitted from the response.
return nil
}

// return untouched if there's no filter or filterAddrsQuery contains "unknown" and provider has no addrs
if len(filterAddrs) == 0 || (len(provider.Addrs) == 0 && slices.Contains(filterAddrs, "unknown")) {
return provider
}

filteredAddrs := applyAddrFilter(provider.Addrs, filterAddrs)

// If filtering resulted in no addrs, omit the provider
if len(filteredAddrs) == 0 {
return nil
}

provider.Addrs = filteredAddrs
return provider
}

// applyAddrFilter filters a list of multiaddresses based on the provided filter query.
//
// Parameters:
// - addrs: A slice of types.Multiaddr to be filtered.
// - filterAddrsQuery: A slice of strings representing the filter criteria.
//
// The function supports both positive and negative filters:
// - Positive filters (e.g., "tcp", "udp") include addresses that match the specified protocols.
// - Negative filters (e.g., "!tcp", "!udp") exclude addresses that match the specified protocols.
//
// If no filters are provided, the original list of addresses is returned unchanged.
// If only negative filters are provided, addresses not matching any negative filter are included.
// If positive filters are provided, only addresses matching at least one positive filter (and no negative filters) are included.
// If both positive and negative filters are provided, the address must match at least one positive filter and no negative filters to be included.
//
// Returns:
// A new slice of types.Multiaddr containing only the addresses that pass the filter criteria.
func applyAddrFilter(addrs []types.Multiaddr, filterAddrsQuery []string) []types.Multiaddr {
if len(filterAddrsQuery) == 0 {
return addrs
}

var filteredAddrs []types.Multiaddr
var positiveFilters, negativeFilters []multiaddr.Protocol

// Separate positive and negative filters
for _, filter := range filterAddrsQuery {
if strings.HasPrefix(filter, "!") {
negativeFilters = append(negativeFilters, multiaddr.ProtocolWithName(filter[1:]))
} else {
positiveFilters = append(positiveFilters, multiaddr.ProtocolWithName(filter))
}
}

for _, addr := range addrs {
2color marked this conversation as resolved.
Show resolved Hide resolved
protocols := addr.Protocols()

// Check negative filters
if containsAny(protocols, negativeFilters) {
continue
}

// If no positive filters or matches a positive filter, include the address
if len(positiveFilters) == 0 || containsAny(protocols, positiveFilters) {
filteredAddrs = append(filteredAddrs, addr)
}
}

return filteredAddrs
}

// Helper function to check if protocols contain any of the filters
func containsAny(protocols []multiaddr.Protocol, filters []multiaddr.Protocol) bool {
for _, filter := range filters {
if containsProtocol(protocols, filter) {
return true
}
}
return false
}

func containsProtocol(protos []multiaddr.Protocol, proto multiaddr.Protocol) bool {
for _, p := range protos {
if p.Code == proto.Code {
return true
}
}
return false
}

// protocolsAllowed returns true if the peerProtocols are allowed by the filter protocols.
func protocolsAllowed(peerProtocols []string, filterProtocols []string) bool {
if len(filterProtocols) == 0 {
// If no filter is passed, do not filter
return true
}

for _, filterProtocol := range filterProtocols {
if filterProtocol == "unknown" && len(peerProtocols) == 0 {
return true
}

for _, peerProtocol := range peerProtocols {
if strings.EqualFold(peerProtocol, filterProtocol) {
return true
}

}
}
return false
}
Loading