-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
[http_proxy_over_p2p] #5526
Changes from all commits
90021c1
c862ac1
1945e44
22f3b11
335bca2
5f246e3
90f5bad
f052d18
c67d2b4
6e24fc6
d30c41a
f03efbd
2cc1a99
91c919a
42a843d
654105a
a818b43
264d9d6
9e79c5e
8676b2e
749ba25
f614406
3f6b866
acb2cac
83369b5
13b0483
2865275
78c43fe
fd43f47
47d45c7
19d4d66
d8cab79
fe8ffde
b720d1f
bf18437
3b2ce4a
9956630
7e54107
8694984
9a443ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
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() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, we may want to start with an |
||
|
||
***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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably give the |
||
``` | ||
|
||
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 | ||
|
There was a problem hiding this comment.
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:
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to agree and I think we can refactor this later if we need to.
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).There was a problem hiding this comment.
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.