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 gateway to http over libp2p #10108

Merged
merged 16 commits into from
Sep 20, 2023
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
142 changes: 127 additions & 15 deletions .github/workflows/gateway-conformance.yml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ I've separated action for testing HTTP Gateway from experimental subset over libp2p (in 1efd9d4)

This way experiment does not impact existing CI and branch protection rule (which requires full conformance on the HTTP port to pass).

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,24 @@ defaults:
run:
shell: bash

env:
# hostnames expected by https://github.com/ipfs/gateway-conformance
GATEWAY_PUBLIC_GATEWAYS: |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this had to be moved to the global scope ? This should do nothing for the libp2p gateway.

{
"example.com": {
"UseSubdomains": true,
"InlineDNSLink": true,
"Paths": ["/ipfs", "/ipns"]
},
"localhost": {
"UseSubdomains": true,
"InlineDNSLink": true,
"Paths": ["/ipfs", "/ipns"]
}
}

jobs:
# Testing all gateway features via TCP port specified in Addresses.Gateway
gateway-conformance:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -33,6 +50,9 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: 1.21.x
- uses: protocol/cache-go-action@v1
with:
name: ${{ github.job }}
- name: Checkout kubo-gateway
uses: actions/checkout@v4
with:
Expand All @@ -43,22 +63,8 @@ jobs:

# 3. Init the kubo-gateway
- name: Init kubo-gateway
env:
GATEWAY_PUBLIC_GATEWAYS: |
{
"example.com": {
"UseSubdomains": true,
"InlineDNSLink": true,
"Paths": ["/ipfs", "/ipns"]
},
"localhost": {
"UseSubdomains": true,
"InlineDNSLink": true,
"Paths": ["/ipfs", "/ipns"]
}
}
run: |
./ipfs init
./ipfs init -e
./ipfs config --json Gateway.PublicGateways "$GATEWAY_PUBLIC_GATEWAYS"
working-directory: kubo-gateway/cmd/ipfs

Expand Down Expand Up @@ -115,3 +121,109 @@ jobs:
with:
name: gateway-conformance.json
path: output.json

# Testing trustless gateway feature subset exposed as libp2p protocol
gateway-conformance-libp2p-experiment:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# 1. Download the gateway-conformance fixtures
- name: Download gateway-conformance fixtures
uses: ipfs/gateway-conformance/.github/actions/[email protected]
with:
output: fixtures

# 2. Build the kubo-gateway
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.20.x
Jorropo marked this conversation as resolved.
Show resolved Hide resolved
- uses: protocol/cache-go-action@v1
with:
name: ${{ github.job }}
- name: Checkout kubo-gateway
uses: actions/checkout@v3
with:
path: kubo-gateway
- name: Build kubo-gateway
run: make build
working-directory: kubo-gateway

# 3. Init the kubo-gateway
- name: Init kubo-gateway
run: |
./ipfs init --profile=test
./ipfs config --json Gateway.PublicGateways "$GATEWAY_PUBLIC_GATEWAYS"
./ipfs config --json Experimental.GatewayOverLibp2p true
./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/8080"
./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5001"
working-directory: kubo-gateway/cmd/ipfs

# 4. Populate the Kubo gateway with the gateway-conformance fixtures
- name: Import fixtures
run: |
# Import car files
find ./fixtures -name '*.car' -exec kubo-gateway/cmd/ipfs/ipfs dag import --pin-roots=false {} \;

# 5. Start the kubo-gateway
- name: Start kubo-gateway
run: |
( ./ipfs daemon & ) | sed '/Daemon is ready/q'
while [[ "$(./ipfs id | jq '.Addresses | length')" == '0' ]]; do sleep 1; done
working-directory: kubo-gateway/cmd/ipfs

# 6. Setup a kubo http-p2p-proxy to expose libp2p protocol as a regular HTTP port for gateway conformance tests
- name: Init p2p-proxy kubo node
env:
IPFS_PATH: "~/.kubo-p2p-proxy"
run: |
./ipfs init --profile=test -e
./ipfs config --json Experimental.Libp2pStreamMounting true
./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/8081"
./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5002"
working-directory: kubo-gateway/cmd/ipfs

# 7. Start the kubo http-p2p-proxy
- name: Start kubo http-p2p-proxy
env:
IPFS_PATH: "~/.kubo-p2p-proxy"
run: |
( ./ipfs daemon & ) | sed '/Daemon is ready/q'
while [[ "$(./ipfs id | jq '.Addresses | length')" == '0' ]]; do sleep 1; done
working-directory: kubo-gateway/cmd/ipfs

# 8. Start forwarding data from the http-p2p-proxy to the node serving the Gateway API over libp2p
- name: Start http-over-libp2p forwarding proxy
run: |
gatewayNodeId=$(./ipfs --api=/ip4/127.0.0.1/tcp/5001 id -f="<id>")
./ipfs --api=/ip4/127.0.0.1/tcp/5002 swarm connect $(./ipfs --api=/ip4/127.0.0.1/tcp/5001 swarm addrs local --id | head -n 1)
./ipfs --api=/ip4/127.0.0.1/tcp/5002 p2p forward --allow-custom-protocol /http/1.1 /ip4/127.0.0.1/tcp/8092 /p2p/$gatewayNodeId
working-directory: kubo-gateway/cmd/ipfs

# 9. Run the gateway-conformance tests over libp2p
- name: Run gateway-conformance tests over libp2p
uses: ipfs/gateway-conformance/.github/actions/[email protected]
with:
gateway-url: http://127.0.0.1:8092
json: output.json
xml: output.xml
html: output.html
markdown: output.md
args: --specs "trustless-gateway,-trustless-ipns-gateway" -skip 'TestGatewayCar/GET_response_for_application/vnd.ipld.car/Header_Content-Length'

# 10. Upload the results
- name: Upload MD summary
if: failure() || success()
run: cat output.md >> $GITHUB_STEP_SUMMARY
- name: Upload HTML report
if: failure() || success()
uses: actions/upload-artifact@v3
with:
name: gateway-conformance-libp2p.html
path: output.html
- name: Upload JSON report
if: failure() || success()
uses: actions/upload-artifact@v3
with:
name: gateway-conformance-libp2p.json
path: output.json
74 changes: 68 additions & 6 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (

multierror "github.com/hashicorp/go-multierror"

options "github.com/ipfs/boxo/coreiface/options"
cmds "github.com/ipfs/go-ipfs-cmds"
mprome "github.com/ipfs/go-metrics-prometheus"
version "github.com/ipfs/kubo"
utilmain "github.com/ipfs/kubo/cmd/ipfs/util"
oldcmds "github.com/ipfs/kubo/commands"
Expand All @@ -30,14 +33,12 @@ import (
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
"github.com/ipfs/kubo/repo/fsrepo/migrations"
"github.com/ipfs/kubo/repo/fsrepo/migrations/ipfsfetcher"
goprocess "github.com/jbenet/goprocess"
p2pcrypto "github.com/libp2p/go-libp2p/core/crypto"
pnet "github.com/libp2p/go-libp2p/core/pnet"
"github.com/libp2p/go-libp2p/core/protocol"
p2phttp "github.com/libp2p/go-libp2p/p2p/http"
sockets "github.com/libp2p/go-socket-activation"

options "github.com/ipfs/boxo/coreiface/options"
cmds "github.com/ipfs/go-ipfs-cmds"
mprome "github.com/ipfs/go-metrics-prometheus"
goprocess "github.com/jbenet/goprocess"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
prometheus "github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -551,6 +552,12 @@ take effect.
return err
}

// add trustless gateway over libp2p
p2pGwErrc, err := serveTrustlessGatewayOverLibp2p(cctx)
if err != nil {
return err
}

// Add ipfs version info to prometheus metrics
ipfsInfoMetric := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "ipfs_info",
Expand Down Expand Up @@ -617,7 +624,7 @@ take effect.
// collect long-running errors and block for shutdown
// TODO(cryptix): our fuse currently doesn't follow this pattern for graceful shutdown
var errs error
for err := range merge(apiErrc, gwErrc, gcErrc) {
for err := range merge(apiErrc, gwErrc, gcErrc, p2pGwErrc) {
if err != nil {
errs = multierror.Append(errs, err)
}
Expand Down Expand Up @@ -899,6 +906,61 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
return errc, nil
}

const gatewayProtocolID protocol.ID = "/ipfs/gateway" // FIXME: specify https://github.com/ipfs/specs/issues/433

func serveTrustlessGatewayOverLibp2p(cctx *oldcmds.Context) (<-chan error, error) {
node, err := cctx.ConstructNode()
if err != nil {
return nil, fmt.Errorf("serveHTTPGatewayOverLibp2p: ConstructNode() failed: %s", err)
}
cfg, err := node.Repo.Config()
if err != nil {
return nil, fmt.Errorf("could not read config: %w", err)
}

if !cfg.Experimental.GatewayOverLibp2p {
errCh := make(chan error)
close(errCh)
return errCh, nil
}

opts := []corehttp.ServeOption{
corehttp.MetricsCollectionOption("libp2p-gateway"),
corehttp.Libp2pGatewayOption(),
corehttp.VersionOption(),
}

handler, err := corehttp.MakeHandler(node, nil, opts...)
if err != nil {
return nil, err
}

h := p2phttp.Host{
StreamHost: node.PeerHost,
}

tmpProtocol := protocol.ID("/kubo/delete-me")
h.SetHTTPHandler(tmpProtocol, http.NotFoundHandler())
h.WellKnownHandler.RemoveProtocolMeta(tmpProtocol)
Comment on lines +942 to +944
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unfortunate hack as a result of a go-libp2p bug which will be linked shortly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@lidel lidel Sep 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aschmahmann @Jorropo do we still need this after libp2p/go-libp2p#2546 being merged?
if not, maybe add TODO to remove it once libp2p is upgraded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes because libp2p/go-libp2p@9f14eb7 is not released.


h.WellKnownHandler.AddProtocolMeta(gatewayProtocolID, p2phttp.ProtocolMeta{Path: "/"})
h.ServeMux = http.NewServeMux()
h.ServeMux.Handle("/", handler)
Comment on lines +946 to +948
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT mounting at / makes testing easier for us (since we can just use the p2p forward command) and lowers the complexity of things like redirects. This means we can have a proxy that listens on localhost:1234 and forwards to the http-over-libp2p endpoint without needing to worry about path rewrites that could be annoying.

I think we should feel free to move this going forward though since the point of .well-known/libp2p is to make the mountpoint flexible by selecting on the protocol ID.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we have to do the work here a little more explicitly than if we weren't mounting at / due to libp2p/go-libp2p#2545


errc := make(chan error, 1)
go func() {
defer close(errc)
errc <- h.Serve()
}()

go func() {
<-node.Process.Closing()
h.Close()
}()

return errc, nil
}

// collects options and opens the fuse mountpoint.
func mountFuse(req *cmds.Request, cctx *oldcmds.Context) error {
cfg, err := cctx.GetConfig()
Expand Down
1 change: 1 addition & 0 deletions config/experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ type Experiments struct {
AcceleratedDHTClient experimentalAcceleratedDHTClient `json:",omitempty"`
OptimisticProvide bool
OptimisticProvideJobsPoolSize int
GatewayOverLibp2p bool `json:",omitempty"`
}
4 changes: 2 additions & 2 deletions core/corehttp/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func CommandsROOption(cctx oldcmds.Context) ServeOption {
func CheckVersionOption() ServeOption {
daemonVersion := version.ApiVersion

return ServeOption(func(n *core.IpfsNode, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
return func(n *core.IpfsNode, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
mux := http.NewServeMux()
parent.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, APIPath) {
Expand All @@ -188,5 +188,5 @@ func CheckVersionOption() ServeOption {
})

return mux, nil
})
}
}
6 changes: 3 additions & 3 deletions core/corehttp/corehttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const shutdownTimeout = 30 * time.Second
// initially passed in if not.
type ServeOption func(*core.IpfsNode, net.Listener, *http.ServeMux) (*http.ServeMux, error)

// makeHandler turns a list of ServeOptions into a http.Handler that implements
// MakeHandler turns a list of ServeOptions into a http.Handler that implements
// all of the given options, in order.
func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http.Handler, error) {
func MakeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http.Handler, error) {
topMux := http.NewServeMux()
mux := topMux
for _, option := range options {
Expand Down Expand Up @@ -86,7 +86,7 @@ func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error
// make sure we close this no matter what.
defer lis.Close()

handler, err := makeHandler(node, lis, options...)
handler, err := MakeHandler(node, lis, options...)
if err != nil {
return err
}
Expand Down
29 changes: 27 additions & 2 deletions core/corehttp/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func GatewayOption(paths ...string) ServeOption {
handler = otelhttp.NewHandler(handler, "Gateway")

for _, p := range paths {
mux.HandleFunc(p+"/", handler.ServeHTTP)
mux.Handle(p+"/", handler)
}

return mux, nil
Expand All @@ -61,7 +61,7 @@ func HostnameOption() ServeOption {
}

childMux := http.NewServeMux()
mux.HandleFunc("/", gateway.NewHostnameHandler(config, backend, childMux).ServeHTTP)
mux.Handle("/", gateway.NewHostnameHandler(config, backend, childMux))
return childMux, nil
}
}
Expand All @@ -76,6 +76,31 @@ func VersionOption() ServeOption {
}
}

func Libp2pGatewayOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
bserv := blockservice.New(n.Blocks.Blockstore(), offline.Exchange(n.Blocks.Blockstore()))

backend, err := gateway.NewBlocksBackend(bserv)
if err != nil {
return nil, err
}

gwConfig := gateway.Config{
DeserializedResponses: false,
NoDNSLink: true,
PublicGateways: nil,
Menu: nil,
}

handler := gateway.NewHandler(gwConfig, &offlineGatewayErrWrapper{gwimpl: backend})
handler = otelhttp.NewHandler(handler, "Libp2p-Gateway")

mux.Handle("/ipfs/", handler)

return mux, nil
}
}

func newGatewayBackend(n *core.IpfsNode) (gateway.IPFSBackend, error) {
cfg, err := n.Repo.Config()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, iface
ts := httptest.NewServer(dh)
t.Cleanup(func() { ts.Close() })

dh.Handler, err = makeHandler(n,
dh.Handler, err = MakeHandler(n,
ts.Listener,
HostnameOption(),
GatewayOption("/ipfs", "/ipns"),
Expand Down
Loading
Loading