Skip to content

Commit

Permalink
Merge pull request #5526 from cboddy/feat/http_proxy_over_p2p
Browse files Browse the repository at this point in the history
[http_proxy_over_p2p]
  • Loading branch information
Stebalien authored Nov 30, 2018
2 parents f505e06 + 9a443ad commit 2d94a3f
Show file tree
Hide file tree
Showing 6 changed files with 440 additions and 0 deletions.
4 changes: 4 additions & 0 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,10 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
corehttp.CommandsROOption(*cctx),
}

if cfg.Experimental.P2pHttpProxy {
opts = append(opts, corehttp.ProxyOption())
}

if len(cfg.Gateway.RootRedirect) > 0 {
opts = append(opts, corehttp.RedirectOption("", cfg.Gateway.RootRedirect))
}
Expand Down
79 changes: 79 additions & 0 deletions core/corehttp/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package corehttp

import (
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"

core "github.com/ipfs/go-ipfs/core"

protocol "gx/ipfs/QmZNkThpqfVXs9GNbexPrfBbXSLNYeKrE7jwFM2oqHbyqN/go-libp2p-protocol"
p2phttp "gx/ipfs/QmcLYfmHLsaVRKGMZQovwEYhHAjWtRjg1Lij3pnzw5UkRD/go-libp2p-http"
)

// ProxyOption is an endpoint for proxying a HTTP request to another ipfs peer
func ProxyOption() ServeOption {
return func(ipfsNode *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
mux.HandleFunc("/p2p/", func(w http.ResponseWriter, request *http.Request) {
// parse request
parsedRequest, err := parseRequest(request)
if err != nil {
handleError(w, "failed to parse request", err, 400)
return
}

request.Host = "" // Let URL's Host take precedence.
request.URL.Path = parsedRequest.httpPath
target, err := url.Parse(fmt.Sprintf("libp2p://%s", parsedRequest.target))
if err != nil {
handleError(w, "failed to parse url", err, 400)
return
}

rt := p2phttp.NewTransport(ipfsNode.PeerHost, p2phttp.ProtocolOption(parsedRequest.name))
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Transport = rt
proxy.ServeHTTP(w, request)
})
return mux, nil
}
}

type proxyRequest struct {
target string
name protocol.ID
httpPath string // path to send to the proxy-host
}

// from the url path parse the peer-ID, name and http path
// /p2p/$peer_id/http/$http_path
// or
// /p2p/$peer_id/x/$protocol/http/$http_path
func parseRequest(request *http.Request) (*proxyRequest, error) {
path := request.URL.Path

split := strings.SplitN(path, "/", 5)
if len(split) < 5 {
return nil, fmt.Errorf("Invalid request path '%s'", path)
}

if split[3] == "http" {
return &proxyRequest{split[2], protocol.ID("/http"), split[4]}, nil
}

split = strings.SplitN(path, "/", 7)
if split[3] != "x" || split[5] != "http" {
return nil, fmt.Errorf("Invalid request path '%s'", path)
}

return &proxyRequest{split[2], protocol.ID("/x/" + split[4] + "/http"), split[6]}, nil
}

func handleError(w http.ResponseWriter, msg string, err error, code int) {
w.WriteHeader(code)
fmt.Fprintf(w, "%s: %s\n", msg, err)
log.Warningf("http proxy error: %s: %s", err)
}
56 changes: 56 additions & 0 deletions core/corehttp/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package corehttp

import (
"net/http"
"strings"
"testing"

"github.com/ipfs/go-ipfs/thirdparty/assert"

protocol "gx/ipfs/QmZNkThpqfVXs9GNbexPrfBbXSLNYeKrE7jwFM2oqHbyqN/go-libp2p-protocol"
)

type TestCase struct {
urlprefix string
target string
name string
path string
}

var validtestCases = []TestCase{
{"http://localhost:5001", "QmT8JtU54XSmC38xSb1XHFSMm775VuTeajg7LWWWTAwzxT", "/http", "path/to/index.txt"},
{"http://localhost:5001", "QmT8JtU54XSmC38xSb1XHFSMm775VuTeajg7LWWWTAwzxT", "/x/custom/http", "path/to/index.txt"},
{"http://localhost:5001", "QmT8JtU54XSmC38xSb1XHFSMm775VuTeajg7LWWWTAwzxT", "/x/custom/http", "http/path/to/index.txt"},
}

func TestParseRequest(t *testing.T) {
for _, tc := range validtestCases {
url := tc.urlprefix + "/p2p/" + tc.target + tc.name + "/" + tc.path
req, _ := http.NewRequest("GET", url, strings.NewReader(""))

parsed, err := parseRequest(req)
if err != nil {
t.Fatal(err)
}
assert.True(parsed.httpPath == tc.path, t, "proxy request path")
assert.True(parsed.name == protocol.ID(tc.name), t, "proxy request name")
assert.True(parsed.target == tc.target, t, "proxy request peer-id")
}
}

var invalidtestCases = []string{
"http://localhost:5001/p2p/http/foobar",
"http://localhost:5001/p2p/QmT8JtU54XSmC38xSb1XHFSMm775VuTeajg7LWWWTAwzxT/x/custom/foobar",
}

func TestParseRequestInvalidPath(t *testing.T) {
for _, tc := range invalidtestCases {
url := tc
req, _ := http.NewRequest("GET", url, strings.NewReader(""))

_, err := parseRequest(req)
if err == nil {
t.Fail()
}
}
}
82 changes: 82 additions & 0 deletions docs/experimental-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ the above issue.
- [BadgerDB datastore](#badger-datastore)
- [Private Networks](#private-networks)
- [ipfs p2p](#ipfs-p2p)
- [p2p http proxy](#p2p-http-proxy)
- [Circuit Relay](#circuit-relay)
- [Plugins](#plugins)
- [Directory Sharding / HAMT](#directory-sharding-hamt)
Expand Down Expand Up @@ -382,6 +383,87 @@ with `ssh [user]@127.0.0.1 -p 2222`.

---

## p2p http proxy

Allows proxying of HTTP requests over p2p streams. This allows serving any standard http app over p2p streams.

### State

Experimental

### In Version

master, 0.4.19

### How to enable

The `p2p` command needs to be enabled in config:

```sh
> ipfs config --json Experimental.Libp2pStreamMounting true
```

On the client, the p2p http proxy needs to be enabled in the config:

```sh
> ipfs config --json Experimental.P2pHttpProxy true
```

### How to use

**Netcat example:**

First, pick a protocol name for your application. Think of the protocol name as
a port number, just significantly more user-friendly. In this example, we're
going to use `/http`.

***Setup:***

1. A "server" node with peer ID `$SERVER_ID`
2. A "client" node.

***On the "server" node:***

First, start your application and have it listen for TCP connections on
port `$APP_PORT`.

Then, configure the p2p listener by running:

```sh
> ipfs p2p listen --allow-custom-protocol /http /ip4/127.0.0.1/tcp/$APP_PORT
```

This will configure IPFS to forward all incoming `/http` streams to
`127.0.0.1:$APP_PORT` (opening a new connection to `127.0.0.1:$APP_PORT` per incoming stream.

***On the "client" node:***

Next, have your application make a http request to `127.0.0.1:8080/p2p/$SERVER_ID/http/$FORWARDED_PATH`. This
connection will be forwarded to the service running on `127.0.0.1:$APP_PORT` on
the remote machine (which needs to be a http server!) with path `$FORWARDED_PATH`. You can test it with netcat:

***On "server" node:***
```sh
> echo -e "HTTP/1.1 200\nContent-length: 11\n\nIPFS rocks!" | nc -l -p $APP_PORT
```

***On "client" node:***
```sh
> curl http://localhost:8080/p2p/$SERVER_ID/http/
```

You should now see the resulting http response: IPFS rocks!

### Custom protocol names
We also support use of protocol names of the form /x/$NAME/http where $NAME doesn't contain any "/"'s

### Road to being a real feature
- [ ] Needs p2p streams to graduate from experiments
- [ ] Needs more people to use and report on how well it works / fits use cases
- [ ] More documentation

---

## Circuit Relay

Allows peers to connect through an intermediate relay node when there
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@
"hash": "QmTqLBwme9BusYWdACqL62NFb8WV2Q72gXLsQVfC7vmCr4",
"name": "iptb-plugins",
"version": "1.0.5"
},
{
"author": "hsanjuan",
"hash": "QmcLYfmHLsaVRKGMZQovwEYhHAjWtRjg1Lij3pnzw5UkRD",
"name": "go-libp2p-http",
"version": "1.1.8"
}
],
"gxVersion": "0.10.0",
Expand Down
Loading

0 comments on commit 2d94a3f

Please sign in to comment.