-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The example proxies http requests through a libp2p stream. License: MIT Signed-off-by: Hector Sanjuan <[email protected]>
- Loading branch information
Showing
3 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# HTTP proxy service with libp2p | ||
|
||
This examples shows how to create a simple HTTP proxy service with libp2p: | ||
|
||
|
||
``` | ||
XXX | ||
XX XXXXXX | ||
X XX | ||
XXXXXXX XX XX XXXXXXXXXX | ||
+---------------------+ +---------------------+ XXX XXX XXX XXX | ||
HTTP Request | | | | XX XX | ||
+-------------------> | libp2p stream | | HTTP X X | ||
| Local peer <------------------> Remote peer <-------------> HTTP SERVER - THE INTERNET XX | ||
<-------------------+ | | | Req & Resp XX X | ||
HTTP Response | libp2p host | | libp2p host | XXXX XXXX XXXXXXXXXXXXXXXXXXXX XXXX | ||
+---------------------+ +---------------------+ XXXXX | ||
``` | ||
|
||
In order to proxy an HTTP request, we create a local peer which listens on `localhost:9900`. HTTP requests performed to that address are tunneled via a libp2p stream to a remote peer, which then performs the HTTP requests and sends the response back to the local peer, which relays it | ||
to the user. | ||
|
||
Note that this is a very simple approach to a proxy, and does not perform any header management, nor supports HTTPS. The `proxy.go` code is thoroughly commeted, detailing what is happening in every step. | ||
|
||
## Build | ||
|
||
From `go-libp2p` base folder: | ||
|
||
``` | ||
> make deps | ||
> go build ./examples/echo | ||
``` | ||
|
||
## Usage | ||
|
||
First run the "remote" peer as follows. It will print a local peer address. If you would like to run this on a separate machine, please replace the IP accordingly: | ||
|
||
```sh | ||
$ ./http-proxy | ||
Proxy server is ready | ||
libp2p-peer addresses: | ||
/ip4/127.0.0.1/tcp/12000/ipfs/QmddTrQXhA9AkCpXPTkcY7e22NK73TwkUms3a44DhTKJTD | ||
``` | ||
|
||
The run the local peer, indicating that it will need to forward http requests to the remote peer as follows: | ||
|
||
``` | ||
$ ./http-proxy -d /ip4/127.0.0.1/tcp/12000/ipfs/QmddTrQXhA9AkCpXPTkcY7e22NK73TwkUms3a44DhTKJTD | ||
Proxy server is ready | ||
libp2p-peer addresses: | ||
/ip4/127.0.0.1/tcp/12001/ipfs/Qmaa2AYTha1UqcFVX97p9R1UP7vbzDLY7bqWsZw1135QvN | ||
proxy listening on 127.0.0.1:9900 | ||
``` | ||
|
||
As you can see, the proxy prints the listening address `127.0.0.1:9900`. You can now use this address as proxy, for example with `curl`: | ||
|
||
``` | ||
$ curl -x "127.0.0.1:9900" "http://ipfs.io/ipfs/QmfUX75pGRBRDnjeoMkQzuQczuCup2aYbeLxz5NzeSu9G6" | ||
it works! | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"flag" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
"strings" | ||
|
||
// We need to import libp2p's libraries that we use in this project. | ||
// In order to work, these libraries need to be rewritten by gx-go. | ||
crypto "github.com/libp2p/go-libp2p-crypto" | ||
host "github.com/libp2p/go-libp2p-host" | ||
inet "github.com/libp2p/go-libp2p-net" | ||
peer "github.com/libp2p/go-libp2p-peer" | ||
ps "github.com/libp2p/go-libp2p-peerstore" | ||
swarm "github.com/libp2p/go-libp2p-swarm" | ||
ma "github.com/multiformats/go-multiaddr" | ||
manet "github.com/multiformats/go-multiaddr-net" | ||
|
||
bhost "github.com/libp2p/go-libp2p/p2p/host/basic" | ||
) | ||
|
||
// Protocol defines the libp2p protocol that we will use for the libp2p proxy | ||
// service that we are going to provide. This will tag the streams used for | ||
// this service. Streams are multiplexed and their protocol tag helps | ||
// libp2p handle them to the right handler functions. | ||
const Protocol = "/proxy-example/0.0.1" | ||
|
||
// makeRandomHost creates a libp2p host with a randomly generated identity. | ||
// This step is described in depth in other tutorials. | ||
func makeRandomHost(port int) host.Host { | ||
priv, pub, err := crypto.GenerateKeyPair(crypto.RSA, 2048) | ||
if err != nil { | ||
die(err) | ||
} | ||
pid, err := peer.IDFromPublicKey(pub) | ||
if err != nil { | ||
die(err) | ||
} | ||
listen, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", port)) | ||
if err != nil { | ||
die(err) | ||
} | ||
ps := ps.NewPeerstore() | ||
ps.AddPrivKey(pid, priv) | ||
ps.AddPubKey(pid, pub) | ||
n, err := swarm.NewNetwork(context.Background(), | ||
[]ma.Multiaddr{listen}, pid, ps, nil) | ||
if err != nil { | ||
die(err) | ||
} | ||
return bhost.New(n) | ||
} | ||
|
||
func log(err error) { | ||
fmt.Fprintf(os.Stderr, "%s\n", err) | ||
} | ||
|
||
func die(err error) { | ||
log(err) | ||
os.Exit(1) | ||
} | ||
|
||
// ProxyService provides HTTP proxying on top of libp2p by launching an | ||
// HTTP server which tunnels the requests to a destination peer running | ||
// ProxyService too. | ||
type ProxyService struct { | ||
host host.Host | ||
dest peer.ID | ||
proxyAddr ma.Multiaddr | ||
} | ||
|
||
// NewProxyService attaches a proxy service to the given libp2p Host. | ||
// The proxyAddr parameter specifies the address on which the | ||
// HTTP proxy server listens. The dest parameter specifies the peer | ||
// ID of the remote peer in charge of performing the HTTP requests. | ||
// | ||
// ProxyAddr/dest may be nil/"" it is not necessary that this host | ||
// provides a listening HTTP server (and instead its only function is to | ||
// perform the proxied http requests it receives from a different peer. | ||
// | ||
// The addresses for the dest peer should be part of the host's peerstore. | ||
func NewProxyService(h host.Host, proxyAddr ma.Multiaddr, dest peer.ID) *ProxyService { | ||
// We let our host know that it needs to handle streams tagged with the | ||
// protocol id that we have defined, and then handle them to | ||
// our own streamHandling function. | ||
h.SetStreamHandler(Protocol, streamHandler) | ||
|
||
fmt.Println("Proxy server is ready") | ||
fmt.Println("libp2p-peer addresses:") | ||
for _, a := range h.Addrs() { | ||
fmt.Printf("%s/ipfs/%s\n", a, peer.IDB58Encode(h.ID())) | ||
} | ||
|
||
return &ProxyService{ | ||
host: h, | ||
dest: dest, | ||
proxyAddr: proxyAddr, | ||
} | ||
} | ||
|
||
// streamHandler is our function to handle any libp2p-net streams that belong | ||
// to our protocol. The streams should contain an HTTP request which we need | ||
// to parse, make on behalf of the original node, and then write the response | ||
// on the stream, before closing it. | ||
func streamHandler(stream inet.Stream) { | ||
// Remember to close the stream when we are done. | ||
defer stream.Close() | ||
|
||
// Create a new buffered reader, as ReadRequest needs one. | ||
// The buffered reader reads from our stream, on which we | ||
// have sent the HTTP request (see ServeHTTP()) | ||
buf := bufio.NewReader(stream) | ||
// Read the HTTP request from the buffer | ||
req, err := http.ReadRequest(buf) | ||
if err != nil { | ||
log(err) | ||
return | ||
} | ||
defer req.Body.Close() | ||
|
||
// We need to reset these fields in the request | ||
// URL as they are not maintained. | ||
req.URL.Scheme = "http" | ||
hp := strings.Split(req.Host, ":") | ||
if len(hp) > 1 && hp[1] == "443" { | ||
req.URL.Scheme = "https" | ||
} else { | ||
req.URL.Scheme = "http" | ||
} | ||
req.URL.Host = req.Host | ||
|
||
outreq := new(http.Request) | ||
*outreq = *req | ||
|
||
// We now make the request | ||
fmt.Printf("Making request to %s\n", req.URL) | ||
resp, err := http.DefaultTransport.RoundTrip(outreq) | ||
if err != nil { | ||
log(err) | ||
return | ||
} | ||
|
||
// resp.Write writes whatever response we obtained for our | ||
// request back to the stream. | ||
resp.Write(stream) | ||
} | ||
|
||
// Serve listens on the ProxyService's proxy address. This effectively | ||
// allows to set the listening address as http proxy. | ||
func (p *ProxyService) Serve() { | ||
_, serveArgs, _ := manet.DialArgs(p.proxyAddr) | ||
fmt.Println("proxy listening on ", serveArgs) | ||
if p.dest != "" { | ||
http.ListenAndServe(serveArgs, p) | ||
} | ||
} | ||
|
||
// ServeHTTP implements the http.Handler interface. WARNING: This is the | ||
// simplest approach to a proxy. Therefore we do not do any of the things | ||
// that should be done when implementing a reverse proxy (like handling | ||
// headers correctly). For how to do it properly, see: | ||
// https://golang.org/src/net/http/httputil/reverseproxy.go?s=3845:3920#L121 | ||
// | ||
// ServeHTTP opens a stream to the dest peer for every HTTP request. | ||
// Streams are multiplexed over single connections so, unlike connections | ||
// themselves, they are cheap to create and dispose of. | ||
func (p *ProxyService) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
fmt.Printf("proxying request for %s to peer %s\n", r.URL, p.dest.Pretty()) | ||
// We need to send the request to the remote libp2p peer, so | ||
// we open a stream to it | ||
stream, err := p.host.NewStream(context.Background(), p.dest, Protocol) | ||
// If an error happens, we write an error for response. | ||
if err != nil { | ||
log(err) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
w.Write([]byte(err.Error())) | ||
return | ||
} | ||
defer stream.Close() | ||
|
||
// r.Write() writes the HTTP request to the stream. | ||
err = r.Write(stream) | ||
if err != nil { | ||
log(err) | ||
w.WriteHeader(http.StatusServiceUnavailable) | ||
w.Write([]byte(err.Error())) | ||
return | ||
} | ||
|
||
// Now we read the response that was sent from the dest | ||
// peer | ||
buf := bufio.NewReader(stream) | ||
resp, err := http.ReadResponse(buf, r) | ||
if err != nil { | ||
log(err) | ||
w.WriteHeader(http.StatusServiceUnavailable) | ||
w.Write([]byte(err.Error())) | ||
return | ||
} | ||
|
||
// Make sure the response has the right status | ||
w.WriteHeader(resp.StatusCode) | ||
// Copy any headers | ||
for k, v := range resp.Header { | ||
for _, s := range v { | ||
w.Header().Add(k, s) | ||
} | ||
} | ||
|
||
// Finally copy the body | ||
io.Copy(w, resp.Body) | ||
resp.Body.Close() | ||
} | ||
|
||
// addAddrToPeerstore parses a peer multiaddress and adds | ||
// it to the given host's peerstore, so it knows how to | ||
// contact it. It returns the peer ID of the remote peer. | ||
func addAddrToPeerstore(h host.Host, addr string) peer.ID { | ||
// The following code extracts target's the peer ID from the | ||
// given multiaddress | ||
ipfsaddr, err := ma.NewMultiaddr(addr) | ||
if err != nil { | ||
die(err) | ||
} | ||
pid, err := ipfsaddr.ValueForProtocol(ma.P_IPFS) | ||
if err != nil { | ||
die(err) | ||
} | ||
|
||
peerid, err := peer.IDB58Decode(pid) | ||
if err != nil { | ||
die(err) | ||
} | ||
|
||
// Decapsulate the /ipfs/<peerID> part from the target | ||
// /ip4/<a.b.c.d>/ipfs/<peer> becomes /ip4/<a.b.c.d> | ||
targetPeerAddr, _ := ma.NewMultiaddr( | ||
fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerid))) | ||
targetAddr := ipfsaddr.Decapsulate(targetPeerAddr) | ||
|
||
// We have a peer ID and a targetAddr so we add | ||
// it to the peerstore so LibP2P knows how to contact it | ||
h.Peerstore().AddAddr(peerid, targetAddr, ps.PermanentAddrTTL) | ||
return peerid | ||
} | ||
|
||
func main() { | ||
flag.Usage = func() { | ||
fmt.Println(` | ||
This example creates a simple HTTP Proxy using two libp2p peers. The first peer | ||
provides an HTTP server locally which tunnels the HTTP requests with libp2p | ||
to a remote peer. The remote peer performs the requests and | ||
send the sends the response back. | ||
Usage: Start remote peer first with: ./proxy | ||
Then start the local peer with: ./proxy -d <remote-peer-multiaddress> | ||
Then you can do something like: curl -x "localhost:9900" "http://ipfs.io". | ||
This proxies sends the request through the local peer, which proxies it to | ||
the remote peer, which makes it and sends the response back. | ||
`) | ||
flag.PrintDefaults() | ||
} | ||
|
||
// Parse some flags | ||
destPeer := flag.String("d", "", "destination peer address") | ||
port := flag.Int("p", 9900, "proxy port") | ||
p2pport := flag.Int("l", 12000, "libp2p listen port") | ||
flag.Parse() | ||
|
||
// If we have a destination peer we will start a local server | ||
if *destPeer != "" { | ||
// We use p2pport+1 in order to not collide if the user | ||
// is running the remote peer locally on that port | ||
host := makeRandomHost(*p2pport + 1) | ||
// Make sure our host knows how to reach destPeer | ||
destPeerID := addAddrToPeerstore(host, *destPeer) | ||
proxyAddr, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", *port)) | ||
if err != nil { | ||
die(err) | ||
} | ||
// Create the proxy service and start the http server | ||
proxy := NewProxyService(host, proxyAddr, destPeerID) | ||
proxy.Serve() // serve hangs forever | ||
} else { | ||
host := makeRandomHost(*p2pport) | ||
// In this case we only need to make sure our host | ||
// knows how to handle incoming proxied requests from | ||
// another peer. | ||
_ = NewProxyService(host, nil, "") | ||
<-make(chan struct{}) // hang forever | ||
} | ||
|
||
} |