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

[http_proxy_over_p2p] #5526

Merged
merged 40 commits into from
Nov 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
90021c1
[http_proxy_over_p2p]
cboddy Aug 22, 2018
c862ac1
[http_proxy_over_p2p] more tests, fix build
cboddy Sep 26, 2018
1945e44
[http_proxy_over_p2p] Add sharness tests for http-proxy-over-p2p
cboddy Sep 29, 2018
22f3b11
change handler mount point to /proxy/http/
ianopolous Oct 1, 2018
335bca2
[http_proxy_over_p2p] remove now superfluous test
cboddy Oct 2, 2018
5f246e3
[http_proxy_over_p2p] httputil.ReverseProxy
cboddy Oct 2, 2018
90f5bad
[http_proxy_over_p2p] proxy async.
cboddy Oct 2, 2018
f052d18
fix non empty paths in p2p http proxy
ianopolous Oct 2, 2018
c67d2b4
Remove unnecessary pointer usage.
ianopolous Oct 3, 2018
6e24fc6
Use request context in p2p stream http proxy
ianopolous Oct 3, 2018
d30c41a
Improve p2p stream closing in http proxy
ianopolous Oct 3, 2018
f03efbd
Use go-libp2p-http
hsanjuan Oct 4, 2018
2cc1a99
Let URL's host take precedence
hsanjuan Oct 5, 2018
91c919a
Fix test
hsanjuan Oct 5, 2018
42a843d
Fix request path. Wasn't using proxy correctly
hsanjuan Oct 5, 2018
654105a
fix test
ianopolous Oct 10, 2018
a818b43
fix some sharness tests
ianopolous Oct 10, 2018
264d9d6
fix remaining sharness tests
ianopolous Oct 10, 2018
9e79c5e
add docs for p2p http proxy
ianopolous Oct 10, 2018
8676b2e
[http_proxy_over_p2p] multipart request proxy test
cboddy Oct 11, 2018
749ba25
[http_proxy_over_p2p] cfg.Experiments.P2pHttpProxy
cboddy Oct 12, 2018
f614406
add doc item
ianopolous Oct 20, 2018
3f6b866
[http_proxy_over_p2p] url-decode the proxy name and fix test
cboddy Oct 24, 2018
acb2cac
use iptb in http proxy tests
Stebalien Oct 25, 2018
83369b5
setup and teardown nodes only once
Stebalien Oct 25, 2018
13b0483
Don't url decode protocol name. It won't work.
ianopolous Oct 28, 2018
2865275
fix tests for iptb update
Stebalien Nov 6, 2018
78c43fe
move p2p http proxy from api to gateway
ianopolous Nov 10, 2018
fd43f47
switch to new path format in p2p http proxy
ianopolous Nov 10, 2018
47d45c7
fix tests
ianopolous Nov 10, 2018
19d4d66
update p2p http proxy docs
ianopolous Nov 10, 2018
d8cab79
update tests
Stebalien Nov 13, 2018
fe8ffde
add some additional tests for custom protocols
Stebalien Nov 13, 2018
b720d1f
uses the global PeerHost and don't expose the P2P one
Stebalien Nov 13, 2018
bf18437
p2p http proxy: go fmt
Stebalien Nov 13, 2018
3b2ce4a
Apply suggestions from code review
hsanjuan Nov 15, 2018
9956630
add more p2p http proxy tests
ianopolous Nov 15, 2018
7e54107
Update test/sharness/t0184-http-proxy-over-p2p.sh
magik6k Nov 16, 2018
8694984
Add another test case for invalid p2p http proxy path
ianopolous Nov 21, 2018
9a443ad
p2p proxy tests: make robust against timing
Stebalien Nov 29, 2018
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
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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function looks a bit messy and will get messier adding /ws. I think now it allows things like /x//http (weird).

I think the cleanest is to do regexps:

// /p2p/$peer_id/x/$protocol/http/$http_path
// Change to ^/p2p/(\w+)/(x/\w+/(?:http|ws))/(.*)$ when adding /ws
var longProtocolRex = regexp.MustCompile(`^/p2p/(\w+)/(x/\w+/http)/(.*)$`)

// /p2p/$peer_id/http/$http_path
// Change to `^/p2p/(\w+)/(http|ws)/(.*)$` when adding /ws
var shortProtocolRex = regexp.MustCompile(`^/p2p/(\w+)/(http)/(.*)$`)

// 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

	matches := longProtocolRex.FindAllStringSubmatch(path, -1)
	if matches == nil {
		matches = shortProtocolRex.FindAllStringSubmatch(path, -1)
	}

	if matches == nil || len(matches) < 1 || len(matches[0]) < 4 {
		return nil, fmt.Errorf("invalid proxy request path: '%s'", path)
	}

	return &proxyRequest{
		target:   matches[0][1],
		name:     protocol.ID("/" + matches[0][2]),
		httpPath: matches[0][3],
	}, nil
}

Technically this would allow not having to change the function when we add /ws. It imposes that custom protocols have a-zA-Z0-9 characters though.

Copy link
Member

Choose a reason for hiding this comment

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

I personally don't think regexes are cleaner. I prefer simpler, faster code.

I also have the fast path for /http (by checking it first) which I imagine will be more common, being the default.

Regarding ws, my understanding (I'm not an expert in them by any measure) is that a web socket starts out as a normal http request, but with a special header, which signals the server to treat it differently. If that is true, I don't understand the need for a separate /ws endpoint. Certainly for my usecase we don't need it.

If we want to ban empty $custom protocol components then that is trivial to add.

Copy link
Member

Choose a reason for hiding this comment

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

I personally don't think regexes are cleaner. I prefer simpler, faster code.

I tend to agree and I think we can refactor this later if we need to.

If that is true, I don't understand the need for a separate /ws endpoint. Certainly for my usecase we don't need it.

The ws case is a slightly different use-case: the websocket would be handled by the proxy and the inner stream itself would be forewarded. Thinking through that case, let's just ignore the ws stuff for now. My proposal wasn't quite right for that use-case but I think we can make it work (although it might not be completely symmetric to http).

Copy link
Contributor

Choose a reason for hiding this comment

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

I tried to refactor this already and the result was as ugly. Personally I don't think it's simpler in this case. It is not super clear that the given request url needs to be an absolute one and it will need re-visiting sooner of later.

Regexp sets a very clear definition of what's an accepted proxy path and what not, without needing to decipher the function.

Personally I don't like the but it's faster argument. Yes, it is slower, 4 times slower (from ~1000ns/op to ~4500ns/op), in the worst case (/http) which means string splitting is not orders of magnitude better. I think it would not matter much compared to the time it takes to make an actual request.

Anyway, all in all, it's not so important, you can move ahead with this if you want. I was just trying to make things a little bit better.

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",
ianopolous marked this conversation as resolved.
Show resolved Hide resolved
"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`.
Copy link
Member

Choose a reason for hiding this comment

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

(non-blocking issue: should be more explicit that this isn't some arbitrary choice; it sounds like I can use "/foo").

Copy link
Member

Choose a reason for hiding this comment

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

Also, we may want to start with an /x/... example as that's really what users should use for custom protocols (for now, at least).


***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
Copy link
Member

Choose a reason for hiding this comment

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

We should probably give the /x/http example here

```

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