Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
178 changes: 178 additions & 0 deletions lib/libp2phttp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package vole

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"time"

"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
libp2phttp "github.com/libp2p/go-libp2p/p2p/http"
"github.com/multiformats/go-multiaddr"
)

func Libp2pHTTPSocketProxy(ctx context.Context, p multiaddr.Multiaddr, unixSocketPath string) error {
h, err := libp2pHost()
if err != nil {
return err
}

httpHost := libp2phttp.Host{StreamHost: h}

ai, err := peer.AddrInfoFromP2pAddr(p)
if err == peer.ErrInvalidAddr {
ai = &peer.AddrInfo{Addrs: []multiaddr.Multiaddr{p}} // No peer id
err = nil
}
if err != nil {
return err
}

hasTLS := false
hasHTTP := false
Comment on lines +43 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this logic get into trouble if say /ws/tls is being used for a relay and combined with /http?

Maybe not relevant though since I'm not sure if there are multiaddrs where /http is valid when combined with /p2p/.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could this logic get into trouble if say /ws/tls is being used for a relay and combined with /http?

/http represents the HTTP transport. It shouldn't be combined with /ws.

Maybe not relevant though since I'm not sure if there are multiaddrs where /http is valid when combined with /p2p/.

Not yet, but these could be combined. I just need to finish peer id auth over http and we could have a valid multiaddr that looks like /.../http/p2p/12Foo

host := ""
port := 0
multiaddr.ForEach(p, func(c multiaddr.Component) bool {
switch c.Protocol().Code {
case multiaddr.P_TLS:
hasTLS = true
case multiaddr.P_HTTP:
hasHTTP = true
case multiaddr.P_HTTPS:
hasHTTP = true
hasTLS = true
case multiaddr.P_IP4, multiaddr.P_IP6, multiaddr.P_DNS4, multiaddr.P_DNS6, multiaddr.P_DNS:
host = c.Value()
case multiaddr.P_TCP, multiaddr.P_UDP:
port, err = strconv.Atoi(c.Value())
return false
}
return true
})
if err != nil {
return err
}
if port == 0 && hasHTTP {
port = 80
if hasTLS {
port = 443
}
}

rt, err := httpHost.NewConstrainedRoundTripper(*ai)
if err != nil {
return err
}

var rp http.Handler
if hasTLS && hasHTTP {
u, err := url.Parse("https://" + host + ":" + strconv.Itoa(port) + "/")
if err != nil {
return err
}
revProxy := httputil.NewSingleHostReverseProxy(u)
rp = revProxy
} else {
rp = &httputil.ReverseProxy{
Transport: rt,
Director: func(r *http.Request) {},
}
}

// Serves an HTTP server on the given path using unix sockets
server := &http.Server{
Handler: rp,
}

l, err := net.Listen("unix", unixSocketPath)
if err != nil {
return err
}

go func() {
<-ctx.Done()
server.Close()
}()

if hasTLS && hasHTTP {
c, err := selfSignedTLSConfig()
if err != nil {

return err
}
server.TLSConfig = c

fmt.Println("Endpoint is an HTTPS endpoint. Using a self signed cert locally to proxy.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is obvious, but can you give me some context on the value of "if the endpoint is HTTPS, let's add a self-signed cert on the unix socket proxy" rather not doing it at all or having the self-signed cert being a separate flag

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Imagine you run:

$ vole libp2p http --socket-path=/tmp/vole.sock /dns/google.com/https

What do you expect the curl request to look like?

I would expect:

curl --unix-socket /tmp/vole.sock https://www.google.com

Not

curl --unix-socket /tmp/vole.sock http://www.google.com

https:// vs http://

The only way we can do that is if our proxy itself is an https server, and thus we need a cert. Self signed is easiest. So we actually need to add a -k to the curl command.

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you expect the curl request to look like?

I guess this is probably my issue. Because I'm having trouble seeing the value of the HTTP -> HTTP proxy other than to demonstrate that it's possible via libp2p multiaddrs the need to drop an s vs to add a -k doesn't mean a ton to me.

On the other hand it doesn't really hurt, so as long as it's not causing trouble and you think it's useful I don't really mind

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I'm having trouble seeing the value of the HTTP -> HTTP proxy other than to demonstrate that it's possible via libp2p multiaddrs

This is not the main use case, but it's nice that it works consistently. You can make the exact same curl request to a standard HTTP server as you would to a libp2p node over a stream.

fmt.Println("Curl will only work with -k flag. This is only for debugging. Do *not* use this in production.")

return server.ServeTLS(l, "", "")
}

return server.Serve(l)
}

// libp2pHTTPServer serves an libp2p enabled HTTP server
func libp2pHTTPServer() (host.Host, *libp2phttp.Host, error) {
h, err := libp2pHost()
if err != nil {
return nil, nil, err
}

httpHost := &libp2phttp.Host{StreamHost: h}
return h, httpHost, nil
}

func selfSignedTLSConfig() (*tls.Config, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}

notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}

certTemplate := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Test"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

derBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &priv.PublicKey, priv)
if err != nil {
return nil, err
}

cert := tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: priv,
}

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
return tlsConfig, nil
}
148 changes: 148 additions & 0 deletions lib/libp2phttp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package vole

import (
"context"
"net"
"net/http"
"os"
"testing"
"time"

"github.com/multiformats/go-multiaddr"
)

func TestHTTPProxyAndServer(t *testing.T) {
// Start libp2p HTTP server
h, hh, err := libp2pHTTPServer()
if err != nil {
t.Fatal(err)
}
hh.SetHTTPHandlerAtPath("/hello", "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))

go hh.Serve()
defer hh.Close()

serverAddr := h.Addrs()[0].Encapsulate(multiaddr.StringCast("/p2p/" + h.ID().String()))
port, err := serverAddr.ValueForProtocol(multiaddr.P_TCP)
if err != nil || port == "" {
port, err = serverAddr.ValueForProtocol(multiaddr.P_UDP)
if err != nil || port == "" {
t.Fatal("could not get port from server address")
}
}

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

socketFile, err := os.CreateTemp("", "libp2phttp-*.sock")
if err != nil {
t.Fatal(err)
}

socketFile.Close()
os.Remove(socketFile.Name())

go func() {
err := Libp2pHTTPSocketProxy(ctx, serverAddr, socketFile.Name())
if err != http.ErrServerClosed && err != nil {
panic(err)
}
}()

// Wait a bit to let the proxy start up.
for i := 0; i < 10; i++ {
time.Sleep(100 * time.Millisecond)
_, err := os.Stat(socketFile.Name())
if err == nil {
break
}
}

client := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", socketFile.Name())
},
},
}

resp, err := client.Get("http://127.0.0.1:" + port + "/")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status code: %d", resp.StatusCode)
}
}

func TestHTTPProxyAndServerOverHTTPTransport(t *testing.T) {
// Start a basic http server
s := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
}
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
go s.Serve(l)
defer s.Close()

// get port of listener
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
t.Fatal(err)
}

serverAddr := multiaddr.StringCast("/ip4/127.0.0.1/tcp/" + port + "/http")

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

socketFile, err := os.CreateTemp("", "libp2phttp-*.sock")
if err != nil {
t.Fatal(err)
}

socketFile.Close()
os.Remove(socketFile.Name())

go func() {
err := Libp2pHTTPSocketProxy(ctx, serverAddr, socketFile.Name())
if err != http.ErrServerClosed && err != nil {
panic(err)
}
}()

// Wait a bit to let the proxy start up.
for i := 0; i < 10; i++ {
time.Sleep(100 * time.Millisecond)
_, err := os.Stat(socketFile.Name())
if err == nil {
break
}
}

client := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", socketFile.Name())
},
},
}

resp, err := client.Get("http://127.0.0.1:" + port + "/")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status code: %d", resp.StatusCode)
}
}
Loading