Skip to content

Commit 37aeead

Browse files
committed
feat(libp2phttp): More ergonomic auth
Auth is now more uniform across HTTP and stream transports.
1 parent 88ae979 commit 37aeead

File tree

4 files changed

+229
-7
lines changed

4 files changed

+229
-7
lines changed

p2p/http/auth/auth_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ func TestMutualAuth(t *testing.T) {
139139
req.Host = "example.com"
140140
serverID, resp, err = clientAuth.AuthenticatedDo(client, req)
141141
require.NotEmpty(t, req.Header.Get("Authorization"))
142+
require.True(t, HasAuthHeader(req))
142143
require.NoError(t, err)
143144
require.Equal(t, expectedServerID, serverID)
144145
require.NotZero(t, clientAuth.tm.tokenMap["example.com"])

p2p/http/auth/server.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"hash"
99
"net/http"
10+
"strings"
1011
"sync"
1112
"time"
1213

@@ -35,6 +36,10 @@ type ServerPeerIDAuth struct {
3536
// scheme. If a Next handler is set, it will be called on authenticated
3637
// requests.
3738
func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
39+
a.ServeHTTPWithNextHandler(w, r, a.Next)
40+
}
41+
42+
func (a *ServerPeerIDAuth) ServeHTTPWithNextHandler(w http.ResponseWriter, r *http.Request, next func(peer.ID, http.ResponseWriter, *http.Request)) {
3843
a.initHmac.Do(func() {
3944
if a.Hmac == nil {
4045
key := make([]byte, 32)
@@ -101,7 +106,7 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
101106
TokenTTL: a.TokenTTL,
102107
Hmac: a.Hmac,
103108
}
104-
hs.Run()
109+
_ = hs.Run() // First run will never err
105110
hs.SetHeader(w.Header())
106111
w.WriteHeader(http.StatusUnauthorized)
107112

@@ -120,9 +125,16 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
120125
return
121126
}
122127

123-
if a.Next == nil {
128+
if next == nil {
124129
w.WriteHeader(http.StatusOK)
125130
return
126131
}
127-
a.Next(peer, w, r)
132+
next(peer, w, r)
133+
}
134+
135+
// HasAuthHeader checks if the HTTP request contains an Authorization header
136+
// that starts with the PeerIDAuthScheme prefix.
137+
func HasAuthHeader(r *http.Request) bool {
138+
h := r.Header.Get("Authorization")
139+
return h != "" && strings.HasPrefix(h, handshake.PeerIDAuthScheme)
128140
}

p2p/http/libp2phttp.go

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/libp2p/go-libp2p/core/peer"
2626
"github.com/libp2p/go-libp2p/core/peerstore"
2727
"github.com/libp2p/go-libp2p/core/protocol"
28+
httpauth "github.com/libp2p/go-libp2p/p2p/http/auth"
2829
gostream "github.com/libp2p/go-libp2p/p2p/net/gostream"
2930
ma "github.com/multiformats/go-multiaddr"
3031
)
@@ -44,6 +45,23 @@ const LegacyWellKnownProtocols = "/.well-known/libp2p"
4445
const peerMetadataLimit = 8 << 10 // 8KB
4546
const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache
4647

48+
type clientPeerIDContextKey struct{}
49+
type serverPeerIDContextKey struct{}
50+
51+
func ClientPeerID(r *http.Request) peer.ID {
52+
if id, ok := r.Context().Value(clientPeerIDContextKey{}).(peer.ID); ok {
53+
return id
54+
}
55+
return ""
56+
}
57+
58+
func ServerPeerID(r *http.Response) peer.ID {
59+
if id, ok := r.Request.Context().Value(serverPeerIDContextKey{}).(peer.ID); ok {
60+
return id
61+
}
62+
return ""
63+
}
64+
4765
// ProtocolMeta is metadata about a protocol.
4866
type ProtocolMeta struct {
4967
// Path defines the HTTP Path prefix used for this protocol
@@ -134,6 +152,12 @@ type Host struct {
134152
// InsecureAllowHTTP indicates if the server is allowed to serve unencrypted
135153
// HTTP requests over TCP.
136154
InsecureAllowHTTP bool
155+
156+
// ServerPeerIDAuth TODO add comment
157+
ServerPeerIDAuth *httpauth.ServerPeerIDAuth
158+
// ClientPeerIDAuth TODO add comment
159+
ClientPeerIDAuth *httpauth.ClientPeerIDAuth
160+
137161
// ServeMux is the http.ServeMux used by the server to serve requests. If
138162
// nil, a new serve mux will be created. Users may manually add handlers to
139163
// this mux instead of using `SetHTTPHandler`, but if they do, they should
@@ -264,15 +288,18 @@ func (h *Host) setupListeners(listenerErrCh chan error) error {
264288
if parsedAddr.useHTTPS {
265289
go func() {
266290
srv := http.Server{
267-
Handler: h.ServeMux,
291+
Handler: maybeDecorateContextWithAuthMiddleware(h.ServerPeerIDAuth, h.ServeMux),
268292
TLSConfig: h.TLSConfig,
269293
}
270294
listenerErrCh <- srv.ServeTLS(l, "", "")
271295
}()
272296
h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr)
273297
} else if h.InsecureAllowHTTP {
274298
go func() {
275-
listenerErrCh <- http.Serve(l, h.ServeMux)
299+
srv := http.Server{
300+
Handler: maybeDecorateContextWithAuthMiddleware(h.ServerPeerIDAuth, h.ServeMux),
301+
}
302+
listenerErrCh <- srv.Serve(l)
276303
}()
277304
h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr)
278305
} else {
@@ -332,7 +359,20 @@ func (h *Host) Serve() error {
332359
h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.StreamHost.Addrs()...)
333360

334361
go func() {
335-
errCh <- http.Serve(listener, connectionCloseHeaderMiddleware(h.ServeMux))
362+
srv := &http.Server{
363+
Handler: connectionCloseHeaderMiddleware(h.ServeMux),
364+
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
365+
remote := c.RemoteAddr()
366+
if remote.Network() == gostream.Network {
367+
remoteID, err := peer.Decode(remote.String())
368+
if err == nil {
369+
return context.WithValue(ctx, clientPeerIDContextKey{}, remoteID)
370+
}
371+
}
372+
return ctx
373+
},
374+
}
375+
errCh <- srv.Serve(listener)
336376
}()
337377
}
338378

@@ -497,6 +537,8 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error)
497537
}
498538
}
499539

540+
ctxWithServerID := context.WithValue(r.Context(), serverPeerIDContextKey{}, rt.server)
541+
resp.Request = resp.Request.WithContext(ctxWithServerID)
500542
return resp, nil
501543
}
502544

@@ -529,10 +571,16 @@ func relativeMultiaddrURIToAbs(original *url.URL, relative *url.URL) (*url.URL,
529571
return nil, errors.New("relative path is not a valid http-path")
530572
}
531573

532-
withoutPath, _ := ma.SplitFunc(originalMa, func(c ma.Component) bool {
574+
withoutPath, afterAndIncludingPath := ma.SplitFunc(originalMa, func(c ma.Component) bool {
533575
return c.Protocol().Code == ma.P_HTTP_PATH
534576
})
535577
withNewPath := withoutPath.Encapsulate(relativePathComponent)
578+
_, afterPath := ma.SplitFirst(afterAndIncludingPath)
579+
580+
// Include after path since it may include other relevant parts (/p2p?)
581+
if afterPath != nil {
582+
withNewPath = withNewPath.Encapsulate(afterPath)
583+
}
536584
return url.Parse("multiaddr:" + withNewPath.String())
537585
}
538586

@@ -743,6 +791,35 @@ func (h *Host) RoundTrip(r *http.Request) (*http.Response, error) {
743791
rt.TLSClientConfig.ServerName = parsed.sni
744792
}
745793

794+
if parsed.peer != "" {
795+
// The peer ID is present. We are making an authenticated request
796+
if h.ClientPeerIDAuth == nil {
797+
return nil, fmt.Errorf("can not authenticate server. Host.ClientPeerIDAuth field is not set")
798+
}
799+
800+
if r.Host == "" {
801+
// Missing a host header. Default to what we parsed earlier
802+
r.Host = u.Host
803+
}
804+
805+
// thin client as ClientPeerIDAuth expects an *http.Client
806+
c := http.Client{Transport: rt}
807+
serverID, resp, err := h.ClientPeerIDAuth.AuthenticatedDo(&c, r)
808+
if err != nil {
809+
return nil, err
810+
}
811+
812+
if serverID != parsed.peer {
813+
resp.Body.Close()
814+
return nil, fmt.Errorf("authenticated server ID does not match expected server ID")
815+
}
816+
817+
ctxWithServerID := context.WithValue(r.Context(), serverPeerIDContextKey{}, serverID)
818+
resp.Request = resp.Request.WithContext(ctxWithServerID)
819+
820+
return resp, nil
821+
}
822+
746823
return rt.RoundTrip(r)
747824
}
748825

@@ -1086,3 +1163,19 @@ func connectionCloseHeaderMiddleware(next http.Handler) http.Handler {
10861163
next.ServeHTTP(w, r)
10871164
})
10881165
}
1166+
1167+
// maybeDecorateContextWithAuth decorates the request context with
1168+
// authentication information if serverAuth is provided.
1169+
func maybeDecorateContextWithAuthMiddleware(serverAuth *httpauth.ServerPeerIDAuth, next http.Handler) http.Handler {
1170+
if serverAuth == nil {
1171+
return next
1172+
}
1173+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1174+
if httpauth.HasAuthHeader(r) {
1175+
serverAuth.ServeHTTPWithNextHandler(w, r, func(p peer.ID, w http.ResponseWriter, r *http.Request) {
1176+
r = r.WithContext(context.WithValue(r.Context(), clientPeerIDContextKey{}, p))
1177+
next.ServeHTTP(w, r)
1178+
})
1179+
}
1180+
})
1181+
}

p2p/http/libp2phttp_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import (
2424
"time"
2525

2626
"github.com/libp2p/go-libp2p"
27+
"github.com/libp2p/go-libp2p/core/crypto"
2728
host "github.com/libp2p/go-libp2p/core/host"
2829
"github.com/libp2p/go-libp2p/core/peer"
2930
libp2phttp "github.com/libp2p/go-libp2p/p2p/http"
31+
httpauth "github.com/libp2p/go-libp2p/p2p/http/auth"
3032
httpping "github.com/libp2p/go-libp2p/p2p/http/ping"
3133
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
3234
ma "github.com/multiformats/go-multiaddr"
@@ -1014,3 +1016,117 @@ func TestErrServerClosed(t *testing.T) {
10141016
server.Close()
10151017
<-done
10161018
}
1019+
1020+
func TestHTTPOverStreamsGetClientID(t *testing.T) {
1021+
serverHost, err := libp2p.New(
1022+
libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
1023+
)
1024+
require.NoError(t, err)
1025+
1026+
httpHost := libp2phttp.Host{StreamHost: serverHost}
1027+
1028+
httpHost.SetHTTPHandler("/echo-id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1029+
clientID := libp2phttp.ClientPeerID(r)
1030+
w.Write([]byte(clientID.String()))
1031+
}))
1032+
1033+
// Start server
1034+
go httpHost.Serve()
1035+
defer httpHost.Close()
1036+
1037+
// Start client
1038+
clientHost, err := libp2p.New(libp2p.NoListenAddrs)
1039+
require.NoError(t, err)
1040+
clientHost.Connect(context.Background(), peer.AddrInfo{
1041+
ID: serverHost.ID(),
1042+
Addrs: serverHost.Addrs(),
1043+
})
1044+
1045+
client := http.Client{
1046+
Transport: &libp2phttp.Host{StreamHost: clientHost},
1047+
}
1048+
require.NoError(t, err)
1049+
1050+
resp, err := client.Get("multiaddr:" + serverHost.Addrs()[0].String() + "/p2p/" + serverHost.ID().String() + "/http-path/echo-id")
1051+
require.NoError(t, err)
1052+
defer resp.Body.Close()
1053+
1054+
body, err := io.ReadAll(resp.Body)
1055+
require.NoError(t, err)
1056+
1057+
require.Equal(t, clientHost.ID().String(), string(body))
1058+
}
1059+
1060+
func TestAuthenticatedRequest(t *testing.T) {
1061+
serverSK, _, err := crypto.GenerateEd25519Key(rand.Reader)
1062+
require.NoError(t, err)
1063+
serverID, err := peer.IDFromPrivateKey(serverSK)
1064+
require.NoError(t, err)
1065+
1066+
serverStreamHost, err := libp2p.New(
1067+
libp2p.Identity(serverSK),
1068+
libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
1069+
libp2p.Transport(libp2pquic.NewTransport),
1070+
)
1071+
require.NoError(t, err)
1072+
1073+
server := libp2phttp.Host{
1074+
InsecureAllowHTTP: true,
1075+
StreamHost: serverStreamHost,
1076+
ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")},
1077+
ServerPeerIDAuth: &httpauth.ServerPeerIDAuth{
1078+
TokenTTL: time.Hour,
1079+
PrivKey: serverSK,
1080+
NoTLS: true,
1081+
ValidHostnameFn: func(hostname string) bool {
1082+
return strings.HasPrefix(hostname, "127.0.0.1")
1083+
},
1084+
},
1085+
}
1086+
server.SetHTTPHandler("/echo-id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1087+
clientID := libp2phttp.ClientPeerID(r)
1088+
w.Write([]byte(clientID.String()))
1089+
}))
1090+
1091+
go server.Serve()
1092+
1093+
clientSK, _, err := crypto.GenerateEd25519Key(rand.Reader)
1094+
require.NoError(t, err)
1095+
1096+
clientStreamHost, err := libp2p.New(
1097+
libp2p.Identity(clientSK),
1098+
libp2p.NoListenAddrs,
1099+
libp2p.Transport(libp2pquic.NewTransport))
1100+
require.NoError(t, err)
1101+
1102+
client := &http.Client{
1103+
Transport: &libp2phttp.Host{
1104+
StreamHost: clientStreamHost,
1105+
ClientPeerIDAuth: &httpauth.ClientPeerIDAuth{
1106+
TokenTTL: time.Hour,
1107+
PrivKey: clientSK,
1108+
},
1109+
},
1110+
}
1111+
1112+
clientID, err := peer.IDFromPrivateKey(clientSK)
1113+
require.NoError(t, err)
1114+
1115+
for _, serverAddr := range server.Addrs() {
1116+
_, tpt := ma.SplitLast(serverAddr)
1117+
t.Run(tpt.String(), func(t *testing.T) {
1118+
url := fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/echo-id", serverAddr, serverID)
1119+
t.Log("Making a GET request to:", url)
1120+
resp, err := client.Get(url)
1121+
require.NoError(t, err)
1122+
1123+
observedServerID := libp2phttp.ServerPeerID(resp)
1124+
require.Equal(t, serverID, observedServerID)
1125+
1126+
body, err := io.ReadAll(resp.Body)
1127+
require.NoError(t, err)
1128+
1129+
require.Equal(t, clientID.String(), string(body))
1130+
})
1131+
}
1132+
}

0 commit comments

Comments
 (0)