Skip to content

Commit

Permalink
feat(gateway): migrate subdomain and dnslink code (#153)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
hacdias and lidel authored Feb 7, 2023
1 parent a4e0934 commit 1a932f7
Show file tree
Hide file tree
Showing 11 changed files with 1,128 additions and 23 deletions.
19 changes: 14 additions & 5 deletions examples/gateway/car/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Gateway backed by a CAR File
# HTTP Gateway backed by a CAR File

This is an example that shows how to build a Gateway backed by the contents of
a CAR file. A [CAR file](https://ipld.io/specs/transport/car/) is a Content
Expand All @@ -7,7 +7,7 @@ Addressable aRchive that contains blocks.
## Build

```bash
> go build -o gateway
> go build -o car-gateway
```

## Usage
Expand All @@ -23,10 +23,19 @@ Then, you can start the gateway with:


```
./gateway -c data.car -p 8040
./car-gateway -c data.car -p 8040
```

Now you can access the gateway in [127.0.0.1:8040](http://127.0.0.1:8040). It will
behave like a regular IPFS Gateway, except for the fact that all contents are provided
### Subdomain gateway

Now you can access the gateway in [localhost:8040](http://localhost:8040). It will
behave like a regular [Subdomain IPFS Gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#subdomain-gateway),
except for the fact that all contents are provided
from the CAR file. Therefore, things such as IPNS resolution and fetching contents
from nodes in the IPFS network won't work.

### Path gateway

If you don't need Origin isolation and only care about hosting flat files,
a plain [path gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#path-gateway) at [127.0.0.1:8040](http://127.0.0.1:8040)
may suffice.
34 changes: 27 additions & 7 deletions examples/gateway/car/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (
"github.com/ipfs/go-cid"
offline "github.com/ipfs/go-ipfs-exchange-offline"
"github.com/ipfs/go-libipfs/examples/gateway/common"
"github.com/ipfs/go-libipfs/gateway"
carblockstore "github.com/ipld/go-car/v2/blockstore"
)

func main() {
carFilePtr := flag.String("c", "", "path to CAR file to back this gateway from")
portPtr := flag.Int("p", 8080, "port to run this gateway from")
port := flag.Int("p", 8040, "port to run this gateway from")
flag.Parse()

blockService, roots, f, err := newBlockServiceFromCAR(*carFilePtr)
Expand All @@ -26,20 +27,39 @@ func main() {
}
defer f.Close()

gateway, err := common.NewBlocksGateway(blockService, nil)
gwAPI, err := common.NewBlocksGateway(blockService, nil)
if err != nil {
log.Fatal(err)
}
handler := common.NewBlocksHandler(gwAPI, *port)

handler := common.NewBlocksHandler(gateway, *portPtr)
// Initialize the public gateways that we will want to have available through
// Host header rewritting. This step is optional and only required if you're
// running multiple public gateways and want different settings and support
// for DNSLink and Subdomain Gateways.
noDNSLink := true // If you set DNSLink to point at the CID from CAR, you can load it!
publicGateways := map[string]*gateway.Specification{
// Support public requests with Host: CID.ipfs.example.net and ID.ipns.example.net
"example.net": {
Paths: []string{"/ipfs", "/ipns"},
NoDNSLink: noDNSLink,
UseSubdomains: true,
},
// Support local requests
"localhost": {
Paths: []string{"/ipfs", "/ipns"},
NoDNSLink: noDNSLink,
UseSubdomains: true,
},
}
handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink)

address := "127.0.0.1:" + strconv.Itoa(*portPtr)
log.Printf("Listening on http://%s", address)
log.Printf("Listening on http://localhost:%d", *port)
for _, cid := range roots {
log.Printf("Hosting CAR root at http://%s/ipfs/%s", address, cid.String())
log.Printf("Hosting CAR root at http://localhost:%d/ipfs/%s", *port, cid.String())
}

if err := http.ListenAndServe(address, handler); err != nil {
if err := http.ListenAndServe(":"+strconv.Itoa(*port), handler); err != nil {
log.Fatal(err)
}
}
Expand Down
14 changes: 14 additions & 0 deletions examples/gateway/common/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package common

import (
"context"
"errors"
"fmt"
"net/http"
gopath "path"
Expand All @@ -24,6 +25,7 @@ import (
uio "github.com/ipfs/go-unixfs/io"
"github.com/ipfs/go-unixfsnode"
iface "github.com/ipfs/interface-go-ipfs-core"
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
ifacepath "github.com/ipfs/interface-go-ipfs-core/path"
dagpb "github.com/ipld/go-codec-dagpb"
"github.com/ipld/go-ipld-prime"
Expand Down Expand Up @@ -143,6 +145,18 @@ func (api *BlocksGateway) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte,
return nil, routing.ErrNotSupported
}

func (api *BlocksGateway) GetDNSLinkRecord(ctx context.Context, hostname string) (ifacepath.Path, error) {
if api.namesys != nil {
p, err := api.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1))
if err == namesys.ErrResolveRecursion {
err = nil
}
return ifacepath.New(p.String()), err
}

return nil, errors.New("not implemented")
}

func (api *BlocksGateway) IsCached(ctx context.Context, p ifacepath.Path) bool {
rp, err := api.ResolvePath(ctx, p)
if err != nil {
Expand Down
28 changes: 25 additions & 3 deletions examples/gateway/proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,31 @@ types. Once you have it, run the proxy gateway with its address as the host para


```
./verifying-proxy -h https://ipfs.io -p 8040
./verifying-proxy -g https://ipfs.io -p 8040
```

Now you can access the gateway in [127.0.0.1:8040](http://127.0.0.1:8040). It will
behave like a regular IPFS Gateway, except for the fact that it runs no libp2p, and has no local blockstore.
### Subdomain gateway

Now you can access the gateway in [localhost:8040](http://localhost:8040). It will
behave like a regular [Subdomain IPFS Gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#subdomain-gateway),
except for the fact that it runs no libp2p, and has no local blockstore.
All contents are provided by a remote gateway and fetched as RAW Blocks and Records, and verified locally.

### Path gateway

If you don't need Origin isolation and only care about hosting flat files,
a plain [path gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#path-gateway) at [127.0.0.1:8040](http://127.0.0.1:8040)
may suffice.

### DNSLink gateway

Gateway supports hosting of [DNSLink](https://dnslink.dev/) websites. All you need is to pass `Host` header with FQDN that has DNSLink set up:

```console
$ curl -sH 'Host: en.wikipedia-on-ipfs.org' 'http://127.0.0.1:8080/wiki/' | head -3
<!DOCTYPE html><html class="client-js"><head>
<meta charset="UTF-8">
<title>Wikipedia, the free encyclopedia</title>
```

Put it behind a reverse proxy terminating TLS (like Nginx) and voila!
34 changes: 27 additions & 7 deletions examples/gateway/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"github.com/ipfs/go-blockservice"
offline "github.com/ipfs/go-ipfs-exchange-offline"
"github.com/ipfs/go-libipfs/examples/gateway/common"
"github.com/ipfs/go-libipfs/gateway"
)

func main() {
gatewayUrlPtr := flag.String("g", "", "gateway to proxy to")
portPtr := flag.Int("p", 8080, "port to run this gateway from")
port := flag.Int("p", 8040, "port to run this gateway from")
flag.Parse()

// Sets up the block store, which will proxy the block requests to the given gateway.
Expand All @@ -24,16 +25,35 @@ func main() {
routing := newProxyRouting(*gatewayUrlPtr, nil)

// Creates the gateway with the block service and the routing.
gateway, err := common.NewBlocksGateway(blockService, routing)
gwAPI, err := common.NewBlocksGateway(blockService, routing)
if err != nil {
log.Fatal(err)
}
handler := common.NewBlocksHandler(gwAPI, *port)

// Initialize the public gateways that we will want to have available through
// Host header rewritting. This step is optional and only required if you're
// running multiple public gateways and want different settings and support
// for DNSLink and Subdomain Gateways.
noDNSLink := false
publicGateways := map[string]*gateway.Specification{
// Support public requests with Host: CID.ipfs.example.net and ID.ipns.example.net
"example.net": {
Paths: []string{"/ipfs", "/ipns"},
NoDNSLink: noDNSLink,
UseSubdomains: true,
},
// Support local requests
"localhost": {
Paths: []string{"/ipfs", "/ipns"},
NoDNSLink: noDNSLink,
UseSubdomains: true,
},
}
handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink)

handler := common.NewBlocksHandler(gateway, *portPtr)
address := "127.0.0.1:" + strconv.Itoa(*portPtr)
log.Printf("Listening on http://%s", address)

if err := http.ListenAndServe(address, handler); err != nil {
log.Printf("Listening on http://localhost:%d", *port)
if err := http.ListenAndServe(":"+strconv.Itoa(*port), handler); err != nil {
log.Fatal(err)
}
}
6 changes: 6 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ type API interface {
// from the routing system.
GetIPNSRecord(context.Context, cid.Cid) ([]byte, error)

// GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN.
// Unlike ResolvePath, it does not perform recursive resolution. It only
// checks for the existence of a DNSLink TXT record with path starting with
// /ipfs/ or /ipns/ and returns the path as-is.
GetDNSLinkRecord(context.Context, string) (path.Path, error)

// IsCached returns whether or not the path exists locally.
IsCached(context.Context, path.Path) bool

Expand Down
103 changes: 103 additions & 0 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package gateway

import (
"context"
"errors"
"strings"

cid "github.com/ipfs/go-cid"
"github.com/ipfs/go-libipfs/blocks"
"github.com/ipfs/go-libipfs/files"
"github.com/ipfs/go-namesys"
path "github.com/ipfs/go-path"
iface "github.com/ipfs/interface-go-ipfs-core"
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/libp2p/go-libp2p/core/crypto"
)

type mockNamesys map[string]path.Path

func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) {
cfg := nsopts.DefaultResolveOpts()
for _, o := range opts {
o(&cfg)
}
depth := cfg.Depth
if depth == nsopts.UnlimitedDepth {
// max uint
depth = ^uint(0)
}
for strings.HasPrefix(name, "/ipns/") {
if depth == 0 {
return value, namesys.ErrResolveRecursion
}
depth--

var ok bool
value, ok = m[name]
if !ok {
return "", namesys.ErrResolveFailed
}
name = value.String()
}
return value, nil
}

func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsopts.ResolveOpt) <-chan namesys.Result {
out := make(chan namesys.Result, 1)
v, err := m.Resolve(ctx, name, opts...)
out <- namesys.Result{Path: v, Err: err}
close(out)
return out
}

func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...nsopts.PublishOption) error {
return errors.New("not implemented for mockNamesys")
}

func (m mockNamesys) GetResolver(subs string) (namesys.Resolver, bool) {
return nil, false
}

type mockApi struct {
ns mockNamesys
}

func newMockApi() *mockApi {
return &mockApi{
ns: mockNamesys{},
}
}

func (m *mockApi) GetUnixFsNode(context.Context, ipath.Resolved) (files.Node, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) LsUnixFsDir(context.Context, ipath.Resolved) (<-chan iface.DirEntry, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) GetBlock(context.Context, cid.Cid) (blocks.Block, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) GetIPNSRecord(context.Context, cid.Cid) ([]byte, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) {
p, err := m.ns.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1))
if err == namesys.ErrResolveRecursion {
err = nil
}
return ipath.New(p.String()), err
}

func (m *mockApi) IsCached(context.Context, ipath.Path) bool {
return false
}

func (m *mockApi) ResolvePath(context.Context, ipath.Path) (ipath.Resolved, error) {
return nil, errors.New("not implemented")
}
Loading

0 comments on commit 1a932f7

Please sign in to comment.