Add http proxy example
The example proxies http requests through a libp2p stream. License: MIT Signed-off-by: Hector Sanjuan <hector@protocol.ai>
This commit is contained in:
parent
9706590149
commit
93802a4007
|
@ -18,6 +18,7 @@ import (
|
||||||
ma "github.com/multiformats/go-multiaddr"
|
ma "github.com/multiformats/go-multiaddr"
|
||||||
gologging "github.com/whyrusleeping/go-logging"
|
gologging "github.com/whyrusleeping/go-logging"
|
||||||
|
|
||||||
|
peerstore "github.com/libp2p/go-libp2p-peerstore"
|
||||||
bhost "github.com/libp2p/go-libp2p/p2p/host/basic"
|
bhost "github.com/libp2p/go-libp2p/p2p/host/basic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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!
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,288 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"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 {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
pid, err := peer.IDFromPublicKey(pub)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
listen, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", port))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(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 {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
return bhost.New(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Println(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.Println(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.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
// r.Write() writes the HTTP request to the stream.
|
||||||
|
err = r.Write(stream)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
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.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy any headers
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
for _, s := range v {
|
||||||
|
w.Header().Add(k, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response status and headers
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
pid, err := ipfsaddr.ValueForProtocol(ma.P_IPFS)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerid, err := peer.IDB58Decode(pid)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(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 {
|
||||||
|
log.Fatalln(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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue