Browse Source

Redesign authentication protocol

tags/fbb2381b
William Petit 6 months ago
parent
commit
19732daaf5

+ 3
- 1
.gitignore View File

@@ -1,2 +1,4 @@
1 1
 /coverage
2
-/vendor
2
+/vendor
3
+/bin
4
+/testdata

+ 3
- 1
Makefile View File

@@ -26,5 +26,7 @@ doc:
26 26
 	@echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/wpetit/go-http-peering to see the documentation"
27 27
 	godoc -http=:6060
28 28
 
29
+bin/keygen:
30
+	go build -o bin/keygen ./cmd/keygen
29 31
 
30
-.PHONY: test lint doc sequence-diagram
32
+.PHONY: test lint doc sequence-diagram bin/keygen

+ 5
- 3
chi/mount.go View File

@@ -1,15 +1,17 @@
1 1
 package chi
2 2
 
3 3
 import (
4
+	"crypto/rsa"
5
+
4 6
 	peering "forge.cadoles.com/wpetit/go-http-peering"
5 7
 	"forge.cadoles.com/wpetit/go-http-peering/server"
6 8
 	"github.com/go-chi/chi"
7 9
 )
8 10
 
9
-func Mount(r chi.Router, store peering.Store, funcs ...server.OptionFunc) {
10
-	r.Post(peering.AdvertisePath, server.AdvertiseHandler(store, funcs...))
11
+func Mount(r chi.Router, store peering.Store, key *rsa.PublicKey, funcs ...server.OptionFunc) {
12
+	r.Post(peering.AdvertisePath, server.AdvertiseHandler(store, key, funcs...))
11 13
 	r.Group(func(r chi.Router) {
12
-		r.Use(server.Authenticate(store, funcs...))
14
+		r.Use(server.Authenticate(store, key, funcs...))
13 15
 		r.Post(peering.UpdatePath, server.UpdateHandler(store, funcs...))
14 16
 		r.Post(peering.PingPath, server.PingHandler(store, funcs...))
15 17
 	})

+ 7
- 1
chi/mount_test.go View File

@@ -4,6 +4,7 @@ import (
4 4
 	"testing"
5 5
 
6 6
 	peering "forge.cadoles.com/wpetit/go-http-peering"
7
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
7 8
 	"forge.cadoles.com/wpetit/go-http-peering/memory"
8 9
 	"github.com/go-chi/chi"
9 10
 )
@@ -13,7 +14,12 @@ func TestMount(t *testing.T) {
13 14
 	r := chi.NewRouter()
14 15
 	store := memory.NewStore()
15 16
 
16
-	Mount(r, store)
17
+	pk, err := peeringCrypto.CreateRSAKey(1024)
18
+	if err != nil {
19
+		t.Fatal(err)
20
+	}
21
+
22
+	Mount(r, store, &pk.PublicKey)
17 23
 
18 24
 	routes := r.Routes()
19 25
 

+ 38
- 11
client/client.go View File

@@ -2,10 +2,10 @@ package client
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"crypto/rsa"
5 6
 	"crypto/sha256"
6 7
 	"encoding/json"
7 8
 	"errors"
8
-	"fmt"
9 9
 	"net/http"
10 10
 	"time"
11 11
 
@@ -25,6 +25,7 @@ var (
25 25
 	ErrUnexpectedResponse = errors.New("unexpected response")
26 26
 	ErrUnauthorized       = errors.New("unauthorized")
27 27
 	ErrRejected           = errors.New("rejected")
28
+	ErrInvalidServerToken = errors.New("invalid server token")
28 29
 )
29 30
 
30 31
 type Client struct {
@@ -40,18 +41,15 @@ func (c *Client) Advertise(attrs peering.PeerAttributes) error {
40 41
 	}
41 42
 
42 43
 	data := &peering.AdvertisingRequest{
43
-		ID:         c.options.PeerID,
44 44
 		Attributes: attrs,
45 45
 		PublicKey:  publicKey,
46 46
 	}
47 47
 
48
-	req, _, err := c.createPostRequest(url, data)
48
+	res, err := c.Post(url, data)
49 49
 	if err != nil {
50 50
 		return err
51 51
 	}
52 52
 
53
-	res, err := c.options.HTTPClient.Do(req)
54
-
55 53
 	switch res.StatusCode {
56 54
 	case http.StatusCreated:
57 55
 		return nil
@@ -111,7 +109,7 @@ func (c *Client) Get(url string) (*http.Response, error) {
111 109
 	if err != nil {
112 110
 		return nil, err
113 111
 	}
114
-	if err := c.signRequest(req, nil); err != nil {
112
+	if err := c.addClientToken(req, nil); err != nil {
115 113
 		return nil, err
116 114
 	}
117 115
 	return c.options.HTTPClient.Do(req)
@@ -122,7 +120,7 @@ func (c *Client) Post(url string, data interface{}) (*http.Response, error) {
122 120
 	if err != nil {
123 121
 		return nil, err
124 122
 	}
125
-	if err := c.signRequest(req, body); err != nil {
123
+	if err := c.addClientToken(req, body); err != nil {
126 124
 		return nil, err
127 125
 	}
128 126
 	return c.options.HTTPClient.Do(req)
@@ -144,16 +142,22 @@ func (c *Client) createPostRequest(url string, data interface{}) (*http.Request,
144 142
 	return req, body, nil
145 143
 }
146 144
 
147
-func (c *Client) signRequest(r *http.Request, body []byte) error {
145
+func (c *Client) addServerToken(r *http.Request) {
146
+	r.Header.Set(
147
+		server.ServerTokenHeader,
148
+		c.options.ServerToken,
149
+	)
150
+}
151
+
152
+func (c *Client) addClientToken(r *http.Request, body []byte) error {
148 153
 	bodySum, err := c.createBodySum(body)
149 154
 	if err != nil {
150 155
 		return err
151 156
 	}
152 157
 
153
-	token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.PeerClaims{
158
+	token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.ClientTokenClaims{
154 159
 		StandardClaims: jwt.StandardClaims{
155 160
 			NotBefore: time.Now().Unix(),
156
-			Issuer:    string(c.options.PeerID),
157 161
 			ExpiresAt: time.Now().Add(time.Minute * 10).Unix(),
158 162
 		},
159 163
 		BodySum: bodySum,
@@ -164,7 +168,9 @@ func (c *Client) signRequest(r *http.Request, body []byte) error {
164 168
 		return err
165 169
 	}
166 170
 
167
-	r.Header.Set("Authorization", fmt.Sprintf("%s %s", server.AuthorizationType, tokenStr))
171
+	r.Header.Set(server.ClientTokenHeader, tokenStr)
172
+
173
+	c.addServerToken(r)
168 174
 
169 175
 	return nil
170 176
 }
@@ -181,6 +187,27 @@ func (c *Client) createBodySum(body []byte) ([]byte, error) {
181 187
 	return sha.Sum(nil), nil
182 188
 }
183 189
 
190
+func (c *Client) PeerID(serverPublicKey *rsa.PublicKey) (peering.PeerID, error) {
191
+	token, err := jwt.ParseWithClaims(
192
+		c.options.ServerToken,
193
+		&peering.ServerTokenClaims{},
194
+		func(token *jwt.Token) (interface{}, error) {
195
+			return serverPublicKey, nil
196
+		},
197
+	)
198
+	if err != nil {
199
+		return "", err
200
+	}
201
+	if !token.Valid {
202
+		return "", ErrInvalidServerToken
203
+	}
204
+	serverClaims, ok := token.Claims.(*peering.ServerTokenClaims)
205
+	if !ok {
206
+		return "", ErrInvalidServerToken
207
+	}
208
+	return serverClaims.PeerID, nil
209
+}
210
+
184 211
 func New(funcs ...OptionFunc) *Client {
185 212
 	options := createOptions(funcs...)
186 213
 	return &Client{options}

+ 44
- 0
client/client_test.go View File

@@ -0,0 +1,44 @@
1
+package client
2
+
3
+import (
4
+	"crypto/rand"
5
+	"crypto/rsa"
6
+	"testing"
7
+
8
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
9
+
10
+	peering "forge.cadoles.com/wpetit/go-http-peering"
11
+)
12
+
13
+func TestClientPeerID(t *testing.T) {
14
+
15
+	serverPK := mustGeneratePrivateKey()
16
+	peerID := peering.NewPeerID()
17
+
18
+	serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID)
19
+	if err != nil {
20
+		t.Fatal(err)
21
+	}
22
+
23
+	client := New(
24
+		WithServerToken(serverToken),
25
+	)
26
+
27
+	clientPeerID, err := client.PeerID(&serverPK.PublicKey)
28
+	if err != nil {
29
+		t.Fatal(err)
30
+	}
31
+
32
+	if g, e := clientPeerID, peerID; g != e {
33
+		t.Errorf("client.PeerID(): got '%v', expected '%v'", g, e)
34
+	}
35
+
36
+}
37
+
38
+func mustGeneratePrivateKey() *rsa.PrivateKey {
39
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
40
+	if err != nil {
41
+		panic(err)
42
+	}
43
+	return privateKey
44
+}

+ 11
- 10
client/option.go View File

@@ -3,22 +3,24 @@ package client
3 3
 import (
4 4
 	"crypto/rsa"
5 5
 	"net/http"
6
-
7
-	peering "forge.cadoles.com/wpetit/go-http-peering"
8 6
 )
9 7
 
8
+type HTTPClient interface {
9
+	Do(*http.Request) (*http.Response, error)
10
+}
11
+
10 12
 type Options struct {
11
-	PeerID     peering.PeerID
12
-	HTTPClient *http.Client
13
-	BaseURL    string
14
-	PrivateKey *rsa.PrivateKey
13
+	HTTPClient  HTTPClient
14
+	BaseURL     string
15
+	PrivateKey  *rsa.PrivateKey
16
+	ServerToken string
15 17
 }
16 18
 
17 19
 type OptionFunc func(*Options)
18 20
 
19
-func WithPeerID(id peering.PeerID) OptionFunc {
21
+func WithServerToken(token string) OptionFunc {
20 22
 	return func(opts *Options) {
21
-		opts.PeerID = id
23
+		opts.ServerToken = token
22 24
 	}
23 25
 }
24 26
 
@@ -28,7 +30,7 @@ func WithPrivateKey(pk *rsa.PrivateKey) OptionFunc {
28 30
 	}
29 31
 }
30 32
 
31
-func WithHTTPClient(client *http.Client) OptionFunc {
33
+func WithHTTPClient(client HTTPClient) OptionFunc {
32 34
 	return func(opts *Options) {
33 35
 		opts.HTTPClient = client
34 36
 	}
@@ -43,7 +45,6 @@ func WithBaseURL(url string) OptionFunc {
43 45
 func defaultOptions() *Options {
44 46
 	return &Options{
45 47
 		HTTPClient: http.DefaultClient,
46
-		PeerID:     peering.NewPeerID(),
47 48
 	}
48 49
 }
49 50
 

+ 33
- 0
cmd/keygen/README.md View File

@@ -0,0 +1,33 @@
1
+# keygen
2
+
3
+Utilitaire de génération de jetons d'authentifications.
4
+
5
+## Usage
6
+
7
+### Créer une nouvelle clé privée
8
+
9
+```
10
+bin/keygen -create-key
11
+```
12
+
13
+### Récupérer la clé publique associée à une clé privée précedemment créée
14
+
15
+```
16
+bin/keygen -get-public-key -key chemin/vers/clé/privée
17
+```
18
+
19
+### Générer un jeton d'authentification à partir d'une clé privée
20
+
21
+```
22
+bin/keygen -create-token -key chemin/vers/clé/privée
23
+```
24
+
25
+### Afficher l'aide
26
+
27
+```
28
+bin/keygen -help
29
+```
30
+
31
+## Mode sans interaction
32
+
33
+Les commandes nécessitant l'entrée d'une phrase de passe peuvent utiliser la variable d'environnement `KEY_PASSPHRASE` pour fonctionner sans interaction.

+ 23
- 0
cmd/keygen/create_key.go View File

@@ -0,0 +1,23 @@
1
+package main
2
+
3
+import (
4
+	"fmt"
5
+
6
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
7
+)
8
+
9
+func createKey() {
10
+	passphrase, err := getPassphrase()
11
+	if err != nil {
12
+		handleError(err)
13
+	}
14
+	key, err := crypto.CreateRSAKey(keySize)
15
+	if err != nil {
16
+		handleError(err)
17
+	}
18
+	privatePEM, err := crypto.EncodePrivateKeyToEncryptedPEM(key, passphrase)
19
+	if err != nil {
20
+		handleError(err)
21
+	}
22
+	fmt.Print(string(privatePEM))
23
+}

+ 21
- 0
cmd/keygen/create_token.go View File

@@ -0,0 +1,21 @@
1
+package main
2
+
3
+import (
4
+	"fmt"
5
+
6
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
7
+
8
+	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+)
10
+
11
+func createToken() {
12
+	privateKey, err := loadPrivateKey()
13
+	if err != nil {
14
+		handleError(err)
15
+	}
16
+	token, err := crypto.CreateServerToken(privateKey, tokenIssuer, peering.PeerID(tokenPeerID))
17
+	if err != nil {
18
+		handleError(err)
19
+	}
20
+	fmt.Println(token)
21
+}

+ 19
- 0
cmd/keygen/get_public_key.go View File

@@ -0,0 +1,19 @@
1
+package main
2
+
3
+import (
4
+	"fmt"
5
+
6
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
7
+)
8
+
9
+func getPublicKey() {
10
+	privateKey, err := loadPrivateKey()
11
+	if err != nil {
12
+		handleError(err)
13
+	}
14
+	publicPEM, err := crypto.EncodePublicKeyToPEM(privateKey.Public())
15
+	if err != nil {
16
+		handleError(err)
17
+	}
18
+	fmt.Print(string(publicPEM))
19
+}

+ 54
- 0
cmd/keygen/main.go View File

@@ -0,0 +1,54 @@
1
+package main
2
+
3
+import (
4
+	"flag"
5
+
6
+	"github.com/pborman/uuid"
7
+)
8
+
9
+// nolint:gochecknoglobals
10
+var (
11
+	createKeyCmd    = false
12
+	getPublicKeyCmd = false
13
+	createTokenCmd  = false
14
+	debug           = false
15
+	keyFile         string
16
+	tokenIssuer     string
17
+	tokenPeerID     = uuid.New()
18
+	keySize         = 2048
19
+)
20
+
21
+// nolint:gochecknoinits
22
+func init() {
23
+	flag.BoolVar(
24
+		&createKeyCmd, "create-key", createKeyCmd,
25
+		"Create a new encrypted PEM private key to sign authentication tokens",
26
+	)
27
+	flag.BoolVar(
28
+		&createTokenCmd, "create-token", createTokenCmd,
29
+		"Create a new signed authentication token",
30
+	)
31
+	flag.BoolVar(
32
+		&getPublicKeyCmd, "get-public-key", getPublicKeyCmd,
33
+		"Get the PEM encoded public key associated with the private key",
34
+	)
35
+	flag.BoolVar(&debug, "debug", debug, "Debug mode")
36
+	flag.StringVar(&keyFile, "key", keyFile, "Path to the encrypted PEM encoded key")
37
+	flag.StringVar(&tokenIssuer, "token-issuer", tokenIssuer, "Token issuer")
38
+	flag.StringVar(&tokenPeerID, "token-peer-id", tokenPeerID, "Token peer ID")
39
+	flag.IntVar(&keySize, "key-size", keySize, "Size of the private key")
40
+}
41
+
42
+func main() {
43
+	flag.Parse()
44
+	switch {
45
+	case createKeyCmd:
46
+		createKey()
47
+	case getPublicKeyCmd:
48
+		getPublicKey()
49
+	case createTokenCmd:
50
+		createToken()
51
+	default:
52
+		flag.Usage()
53
+	}
54
+}

+ 96
- 0
cmd/keygen/util.go View File

@@ -0,0 +1,96 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"crypto/rand"
6
+	"crypto/rsa"
7
+	"crypto/x509"
8
+	"encoding/pem"
9
+	"errors"
10
+	"fmt"
11
+	"io/ioutil"
12
+	"os"
13
+	"syscall"
14
+
15
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
16
+
17
+	"golang.org/x/crypto/ssh/terminal"
18
+)
19
+
20
+func getPassphrase() ([]byte, error) {
21
+	passphrase := os.Getenv("KEY_PASSPHRASE")
22
+	if passphrase != "" {
23
+		return []byte(passphrase), nil
24
+	}
25
+	return askPassphrase()
26
+}
27
+
28
+func askPassphrase() ([]byte, error) {
29
+	fmt.Print("Passphrase: ")
30
+	passphrase, err := terminal.ReadPassword(syscall.Stdin)
31
+	if err != nil {
32
+		return nil, err
33
+	}
34
+	fmt.Println()
35
+
36
+	fmt.Print("Confirm passphrase: ")
37
+	passphraseConfirmation, err := terminal.ReadPassword(syscall.Stdin)
38
+	if err != nil {
39
+		return nil, err
40
+	}
41
+	fmt.Println()
42
+
43
+	if !bytes.Equal(passphrase, passphraseConfirmation) {
44
+		return nil, errors.New("passphrases does not match")
45
+	}
46
+
47
+	return passphrase, nil
48
+}
49
+
50
+func privateKeyToEncryptedPEM(key *rsa.PrivateKey, passphrase []byte) ([]byte, error) {
51
+
52
+	if passphrase == nil {
53
+		return nil, errors.New("passphrase cannot be empty")
54
+	}
55
+
56
+	// Convert it to pem
57
+	block := &pem.Block{
58
+		Type:  "RSA PRIVATE KEY",
59
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
60
+	}
61
+
62
+	block, err := x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, passphrase, x509.PEMCipherAES256)
63
+	if err != nil {
64
+		return nil, err
65
+	}
66
+
67
+	return pem.EncodeToMemory(block), nil
68
+}
69
+
70
+func loadPrivateKey() (*rsa.PrivateKey, error) {
71
+	if keyFile == "" {
72
+		return nil, errors.New("you must specify a key file to load")
73
+	}
74
+	pem, err := ioutil.ReadFile(keyFile)
75
+	if err != nil {
76
+		return nil, err
77
+	}
78
+	passphrase, err := getPassphrase()
79
+	if err != nil {
80
+		return nil, err
81
+	}
82
+	privateKey, err := crypto.DecodePEMEncryptedPrivateKey(pem, passphrase)
83
+	if err != nil {
84
+		return nil, err
85
+	}
86
+	return privateKey, nil
87
+}
88
+
89
+func handleError(err error) {
90
+	if !debug {
91
+		fmt.Println(err)
92
+	} else {
93
+		panic(err)
94
+	}
95
+	os.Exit(1)
96
+}

+ 54
- 2
crypto/pem.go View File

@@ -1,14 +1,17 @@
1 1
 package crypto
2 2
 
3 3
 import (
4
+	"crypto"
5
+	"crypto/rand"
4 6
 	"crypto/rsa"
5 7
 	"crypto/x509"
6 8
 	"encoding/pem"
9
+	"errors"
7 10
 
8 11
 	jwt "github.com/dgrijalva/jwt-go"
9 12
 )
10 13
 
11
-func EncodePublicKeyToPEM(key interface{}) ([]byte, error) {
14
+func EncodePublicKeyToPEM(key crypto.PublicKey) ([]byte, error) {
12 15
 	pub, err := x509.MarshalPKIXPublicKey(key)
13 16
 	if err != nil {
14 17
 		return nil, err
@@ -20,6 +23,55 @@ func EncodePublicKeyToPEM(key interface{}) ([]byte, error) {
20 23
 	return data, nil
21 24
 }
22 25
 
23
-func DecodePEMToPublicKey(pem []byte) (*rsa.PublicKey, error) {
26
+func DecodePEMToPublicKey(pem []byte) (crypto.PublicKey, error) {
24 27
 	return jwt.ParseRSAPublicKeyFromPEM(pem)
25 28
 }
29
+
30
+func DecodePEMEncryptedPrivateKey(key []byte, passphrase []byte) (*rsa.PrivateKey, error) {
31
+	var err error
32
+
33
+	// Parse PEM block
34
+	var block *pem.Block
35
+	if block, _ = pem.Decode(key); block == nil {
36
+		return nil, errors.New("invalid PEM block")
37
+	}
38
+
39
+	decryptedBlock, err := x509.DecryptPEMBlock(block, passphrase)
40
+	if err != nil {
41
+		return nil, err
42
+	}
43
+
44
+	var parsedKey interface{}
45
+	if parsedKey, err = x509.ParsePKCS1PrivateKey(decryptedBlock); err != nil {
46
+		return nil, err
47
+	}
48
+
49
+	var privateKey *rsa.PrivateKey
50
+	var ok bool
51
+	if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok {
52
+		return nil, errors.New("invalid RSA private key")
53
+	}
54
+
55
+	return privateKey, nil
56
+}
57
+
58
+func EncodePrivateKeyToEncryptedPEM(key *rsa.PrivateKey, passphrase []byte) ([]byte, error) {
59
+	if passphrase == nil {
60
+		return nil, errors.New("passphrase cannot be empty")
61
+	}
62
+
63
+	block := &pem.Block{
64
+		Type:  "RSA PRIVATE KEY",
65
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
66
+	}
67
+
68
+	block, err := x509.EncryptPEMBlock(
69
+		rand.Reader, block.Type,
70
+		block.Bytes, passphrase, x509.PEMCipherAES256,
71
+	)
72
+	if err != nil {
73
+		return nil, err
74
+	}
75
+
76
+	return pem.EncodeToMemory(block), nil
77
+}

+ 34
- 0
crypto/rsa.go View File

@@ -0,0 +1,34 @@
1
+package crypto
2
+
3
+import (
4
+	"crypto/rand"
5
+	"crypto/rsa"
6
+	"time"
7
+
8
+	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+
10
+	jwt "github.com/dgrijalva/jwt-go"
11
+)
12
+
13
+func CreateRSAKey(bits int) (*rsa.PrivateKey, error) {
14
+	key, err := rsa.GenerateKey(rand.Reader, bits)
15
+	if err != nil {
16
+		return nil, err
17
+	}
18
+	return key, nil
19
+}
20
+
21
+func CreateServerToken(privateKey *rsa.PrivateKey, issuer string, peerID peering.PeerID) (string, error) {
22
+	token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.ServerTokenClaims{
23
+		StandardClaims: jwt.StandardClaims{
24
+			NotBefore: time.Now().Unix(),
25
+			Issuer:    issuer,
26
+		},
27
+		PeerID: peerID,
28
+	})
29
+	tokenStr, err := token.SignedString(privateKey)
30
+	if err != nil {
31
+		return "", err
32
+	}
33
+	return tokenStr, nil
34
+}

+ 1
- 1
doc/sequence-diagram/advertise.seq View File

@@ -1,2 +1,2 @@
1
-Client -> Server: POST /advertise\n\n{"ID": <PEER_ID>, "Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> }
1
+Client -> Server: POST /advertise\nX-Server-Token: <JWT_TOKEN>\n\n{"Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> }
2 2
 Server -> Client: 201 Created

+ 19
- 18
doc/sequence-diagram/advertise.svg View File

@@ -1,6 +1,6 @@
1 1
 <?xml version="1.0"?>
2 2
 <!-- Generated by SVGo -->
3
-<svg width="711" height="196"
3
+<svg width="589" height="212"
4 4
      xmlns="http://www.w3.org/2000/svg"
5 5
      xmlns:xlink="http://www.w3.org/1999/xlink">
6 6
 <defs>
@@ -13,23 +13,24 @@
13 13
 }
14 14
 </style>
15 15
 </defs>
16
-<line x1="45" y1="24" x2="45" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
16
+<line x1="45" y1="24" x2="45" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17 17
 <rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
18 18
 <text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
19
-<rect x="8" y="156" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
-<text x="24" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
-<line x1="661" y1="24" x2="661" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
-<rect x="619" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
-<text x="635" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
-<rect x="619" y="156" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
-<text x="635" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
-<rect x="61" y="56" width="584" height="46" style="fill:white;stroke:white;" />
27
-<text x="298" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /advertise</text>
28
-<text x="61" y="100" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{&#34;ID&#34;: &lt;PEER_ID&gt;, &#34;Attributes&#34;: &lt;PEER_ATTRIBUTES&gt;, &#34;PublicKey&#34;: &lt;PUBLIC_KEY&gt; }</text>
29
-<line x1="45" y1="106" x2="661" y2="106" style="stroke:black;stroke-width:2px;" />
30
-<polyline points="652,101 661,106 652,111" style="fill:black;stroke-width:2px;stroke:black;" />
31
-<rect x="310" y="122" width="87" height="14" style="fill:white;stroke:white;" />
32
-<text x="310" y="134" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >201 Created</text>
33
-<line x1="661" y1="140" x2="45" y2="140" style="stroke:black;stroke-width:2px;" />
34
-<polyline points="54,135 45,140 54,145" style="fill:black;stroke-width:2px;stroke:black;" />
19
+<rect x="8" y="172" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
+<text x="24" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
+<line x1="539" y1="24" x2="539" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
+<rect x="497" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
+<text x="513" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
+<rect x="497" y="172" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
+<text x="513" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
+<rect x="61" y="56" width="462" height="62" style="fill:white;stroke:white;" />
27
+<text x="237" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /advertise</text>
28
+<text x="184" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >X-Server-Token: &lt;JWT_TOKEN&gt;</text>
29
+<text x="61" y="116" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{&#34;Attributes&#34;: &lt;PEER_ATTRIBUTES&gt;, &#34;PublicKey&#34;: &lt;PUBLIC_KEY&gt; }</text>
30
+<line x1="45" y1="122" x2="539" y2="122" style="stroke:black;stroke-width:2px;" />
31
+<polyline points="530,117 539,122 530,127" style="fill:black;stroke-width:2px;stroke:black;" />
32
+<rect x="249" y="138" width="87" height="14" style="fill:white;stroke:white;" />
33
+<text x="249" y="150" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >201 Created</text>
34
+<line x1="539" y1="156" x2="45" y2="156" style="stroke:black;stroke-width:2px;" />
35
+<polyline points="54,151 45,156 54,161" style="fill:black;stroke-width:2px;stroke:black;" />
35 36
 </svg>

+ 1
- 1
doc/sequence-diagram/ping.seq View File

@@ -1,2 +1,2 @@
1
-Client -> Server: POST /ping\nAuthorization: Bearer <JWT_SIGNING_TOKEN>
1
+Client -> Server: POST /ping\nX-Server-Token: <JWT_TOKEN>\nX-Client-Token: <JWT_TOKEN>
2 2
 Server -> Client: 204 No Content

+ 19
- 18
doc/sequence-diagram/ping.svg View File

@@ -1,6 +1,6 @@
1 1
 <?xml version="1.0"?>
2 2
 <!-- Generated by SVGo -->
3
-<svg width="446" height="180"
3
+<svg width="343" height="196"
4 4
      xmlns="http://www.w3.org/2000/svg"
5 5
      xmlns:xlink="http://www.w3.org/1999/xlink">
6 6
 <defs>
@@ -13,23 +13,24 @@
13 13
 }
14 14
 </style>
15 15
 </defs>
16
-<line x1="45" y1="24" x2="45" y2="156" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
16
+<line x1="45" y1="24" x2="45" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17 17
 <rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
18 18
 <text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
19
-<rect x="8" y="140" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
-<text x="24" y="161" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
-<line x1="396" y1="24" x2="396" y2="156" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
-<rect x="354" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
-<text x="370" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
-<rect x="354" y="140" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
-<text x="370" y="161" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
-<rect x="61" y="56" width="319" height="30" style="fill:white;stroke:white;" />
27
-<text x="181" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /ping</text>
28
-<text x="61" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >Authorization: Bearer &lt;JWT_SIGNING_TOKEN&gt;</text>
29
-<line x1="45" y1="90" x2="396" y2="90" style="stroke:black;stroke-width:2px;" />
30
-<polyline points="387,85 396,90 387,95" style="fill:black;stroke-width:2px;stroke:black;" />
31
-<rect x="166" y="106" width="111" height="14" style="fill:white;stroke:white;" />
32
-<text x="166" y="118" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
33
-<line x1="396" y1="124" x2="45" y2="124" style="stroke:black;stroke-width:2px;" />
34
-<polyline points="54,119 45,124 54,129" style="fill:black;stroke-width:2px;stroke:black;" />
19
+<rect x="8" y="156" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
+<text x="24" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
+<line x1="293" y1="24" x2="293" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
+<rect x="251" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
+<text x="267" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
+<rect x="251" y="156" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
+<text x="267" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
+<rect x="61" y="56" width="216" height="46" style="fill:white;stroke:white;" />
27
+<text x="130" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /ping</text>
28
+<text x="61" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >X-Server-Token: &lt;JWT_TOKEN&gt;</text>
29
+<text x="63" y="100" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >X-Client-Token: &lt;JWT_TOKEN&gt;</text>
30
+<line x1="45" y1="106" x2="293" y2="106" style="stroke:black;stroke-width:2px;" />
31
+<polyline points="284,101 293,106 284,111" style="fill:black;stroke-width:2px;stroke:black;" />
32
+<rect x="114" y="122" width="111" height="14" style="fill:white;stroke:white;" />
33
+<text x="114" y="134" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
34
+<line x1="293" y1="140" x2="45" y2="140" style="stroke:black;stroke-width:2px;" />
35
+<polyline points="54,135 45,140 54,145" style="fill:black;stroke-width:2px;stroke:black;" />
35 36
 </svg>

+ 1
- 1
doc/sequence-diagram/update.seq View File

@@ -1,2 +1,2 @@
1
-Client -> Server: POST /update\nAuthorization: Bearer <JWT_SIGNING_TOKEN>\n\n{"Attributes": <PEER_ATTRIBUTES>}
1
+Client -> Server: POST /update\nX-Server-Token: <JWT_TOKEN>\nX-Client-Token: <JWT_TOKEN>\n\n{"Attributes": <PEER_ATTRIBUTES>}
2 2
 Server -> Client: 204 No Content

+ 20
- 19
doc/sequence-diagram/update.svg View File

@@ -1,6 +1,6 @@
1 1
 <?xml version="1.0"?>
2 2
 <!-- Generated by SVGo -->
3
-<svg width="446" height="212"
3
+<svg width="386" height="228"
4 4
      xmlns="http://www.w3.org/2000/svg"
5 5
      xmlns:xlink="http://www.w3.org/1999/xlink">
6 6
 <defs>
@@ -13,24 +13,25 @@
13 13
 }
14 14
 </style>
15 15
 </defs>
16
-<line x1="45" y1="24" x2="45" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
16
+<line x1="45" y1="24" x2="45" y2="204" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17 17
 <rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
18 18
 <text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
19
-<rect x="8" y="172" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
-<text x="24" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
-<line x1="396" y1="24" x2="396" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
-<rect x="354" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
-<text x="370" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
-<rect x="354" y="172" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
-<text x="370" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
-<rect x="61" y="56" width="319" height="62" style="fill:white;stroke:white;" />
27
-<text x="172" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /update</text>
28
-<text x="61" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >Authorization: Bearer &lt;JWT_SIGNING_TOKEN&gt;</text>
29
-<text x="91" y="116" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{&#34;Attributes&#34;: &lt;PEER_ATTRIBUTES&gt;}</text>
30
-<line x1="45" y1="122" x2="396" y2="122" style="stroke:black;stroke-width:2px;" />
31
-<polyline points="387,117 396,122 387,127" style="fill:black;stroke-width:2px;stroke:black;" />
32
-<rect x="166" y="138" width="111" height="14" style="fill:white;stroke:white;" />
33
-<text x="166" y="150" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
34
-<line x1="396" y1="156" x2="45" y2="156" style="stroke:black;stroke-width:2px;" />
35
-<polyline points="54,151 45,156 54,161" style="fill:black;stroke-width:2px;stroke:black;" />
19
+<rect x="8" y="188" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
20
+<text x="24" y="209" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
21
+<line x1="336" y1="24" x2="336" y2="204" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
22
+<rect x="294" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
23
+<text x="310" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
24
+<rect x="294" y="188" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
25
+<text x="310" y="209" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
26
+<rect x="61" y="56" width="259" height="78" style="fill:white;stroke:white;" />
27
+<text x="142" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /update</text>
28
+<text x="82" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >X-Server-Token: &lt;JWT_TOKEN&gt;</text>
29
+<text x="84" y="100" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >X-Client-Token: &lt;JWT_TOKEN&gt;</text>
30
+<text x="61" y="132" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{&#34;Attributes&#34;: &lt;PEER_ATTRIBUTES&gt;}</text>
31
+<line x1="45" y1="138" x2="336" y2="138" style="stroke:black;stroke-width:2px;" />
32
+<polyline points="327,133 336,138 327,143" style="fill:black;stroke-width:2px;stroke:black;" />
33
+<rect x="136" y="154" width="111" height="14" style="fill:white;stroke:white;" />
34
+<text x="136" y="166" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
35
+<line x1="336" y1="172" x2="45" y2="172" style="stroke:black;stroke-width:2px;" />
36
+<polyline points="54,167 45,172 54,177" style="fill:black;stroke-width:2px;stroke:black;" />
36 37
 </svg>

+ 3
- 0
go.mod View File

@@ -1,7 +1,10 @@
1 1
 module forge.cadoles.com/wpetit/go-http-peering
2 2
 
3 3
 require (
4
+	github.com/davecgh/go-spew v1.1.1
4 5
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
5 6
 	github.com/go-chi/chi v3.3.3+incompatible
6 7
 	github.com/pborman/uuid v1.2.0
8
+	golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2
9
+	golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect
7 10
 )

+ 6
- 0
go.sum View File

@@ -1,3 +1,5 @@
1
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1 3
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
2 4
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
3 5
 github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8=
@@ -6,3 +8,7 @@ github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
6 8
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7 9
 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
8 10
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
11
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 h1:NwxKRvbkH5MsNkvOtPZi3/3kmI8CAzs3mtv+GLQMkNo=
12
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
13
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
14
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

+ 1
- 0
modd.conf View File

@@ -1,6 +1,7 @@
1 1
 **/*.go
2 2
 !vendor/**.go {
3 3
     prep: make test
4
+    prep: make bin/keygen
4 5
 }
5 6
 
6 7
 doc/sequence-diagram/*.seq {

+ 6
- 2
request.go View File

@@ -9,7 +9,6 @@ const (
9 9
 )
10 10
 
11 11
 type AdvertisingRequest struct {
12
-	ID         PeerID
13 12
 	Attributes PeerAttributes
14 13
 	PublicKey  []byte
15 14
 }
@@ -18,7 +17,12 @@ type UpdateRequest struct {
18 17
 	Attributes PeerAttributes
19 18
 }
20 19
 
21
-type PeerClaims struct {
20
+type ClientTokenClaims struct {
22 21
 	jwt.StandardClaims
23 22
 	BodySum []byte `json:"bodySum"`
24 23
 }
24
+
25
+type ServerTokenClaims struct {
26
+	jwt.StandardClaims
27
+	PeerID PeerID `json:"peerID"`
28
+}

+ 0
- 158
server/advertise_test.go View File

@@ -1,158 +0,0 @@
1
-package server
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/json"
6
-	"net/http"
7
-	"net/http/httptest"
8
-	"testing"
9
-
10
-	"forge.cadoles.com/wpetit/go-http-peering/crypto"
11
-
12
-	peering "forge.cadoles.com/wpetit/go-http-peering"
13
-	"forge.cadoles.com/wpetit/go-http-peering/memory"
14
-)
15
-
16
-func TestAdvertiseHandlerBadRequest(t *testing.T) {
17
-	store := memory.NewStore()
18
-	handler := AdvertiseHandler(store)
19
-
20
-	req := httptest.NewRequest("POST", peering.AdvertisePath, nil)
21
-	w := httptest.NewRecorder()
22
-
23
-	handler(w, req)
24
-
25
-	res := w.Result()
26
-
27
-	if g, e := res.StatusCode, http.StatusBadRequest; g != e {
28
-		t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
29
-	}
30
-
31
-	peers, err := store.List()
32
-	if err != nil {
33
-		t.Fatal(err)
34
-	}
35
-
36
-	if g, e := len(peers), 0; g != e {
37
-		t.Errorf("len(peers): got '%v', expected '%v'", g, e)
38
-	}
39
-}
40
-
41
-func TestAdvertiseHandlerInvalidPublicKeyFormat(t *testing.T) {
42
-	store := memory.NewStore()
43
-	handler := AdvertiseHandler(store)
44
-
45
-	advertising := &peering.AdvertisingRequest{
46
-		ID:        peering.NewPeerID(),
47
-		PublicKey: []byte("Test"),
48
-	}
49
-
50
-	body, err := json.Marshal(advertising)
51
-	if err != nil {
52
-		t.Fatal(err)
53
-	}
54
-
55
-	req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
56
-	w := httptest.NewRecorder()
57
-
58
-	handler(w, req)
59
-
60
-	res := w.Result()
61
-
62
-	if g, e := res.StatusCode, http.StatusBadRequest; g != e {
63
-		t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
64
-	}
65
-
66
-	peers, err := store.List()
67
-	if err != nil {
68
-		t.Fatal(err)
69
-	}
70
-
71
-	if g, e := len(peers), 0; g != e {
72
-		t.Errorf("len(peers): got '%v', expected '%v'", g, e)
73
-	}
74
-}
75
-
76
-func TestAdvertiseHandlerExistingPeer(t *testing.T) {
77
-	store := memory.NewStore()
78
-	handler := AdvertiseHandler(store)
79
-
80
-	pk := mustGeneratePrivateKey()
81
-	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
82
-	if err != nil {
83
-		t.Fatal(err)
84
-	}
85
-
86
-	peerID := peering.NewPeerID()
87
-
88
-	advertising := &peering.AdvertisingRequest{
89
-		ID:        peerID,
90
-		PublicKey: pem,
91
-	}
92
-
93
-	body, err := json.Marshal(advertising)
94
-	if err != nil {
95
-		t.Fatal(err)
96
-	}
97
-
98
-	req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
99
-	w := httptest.NewRecorder()
100
-
101
-	handler(w, req)
102
-
103
-	req = httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
104
-	w = httptest.NewRecorder()
105
-
106
-	handler(w, req)
107
-
108
-	res := w.Result()
109
-
110
-	if g, e := res.StatusCode, http.StatusConflict; g != e {
111
-		t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
112
-	}
113
-
114
-}
115
-
116
-func TestAdvertiseHandlerValidRequest(t *testing.T) {
117
-	store := memory.NewStore()
118
-	handler := AdvertiseHandler(store)
119
-
120
-	pk := mustGeneratePrivateKey()
121
-	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
122
-	if err != nil {
123
-		t.Fatal(err)
124
-	}
125
-
126
-	peerID := peering.NewPeerID()
127
-
128
-	advertising := &peering.AdvertisingRequest{
129
-		ID:        peerID,
130
-		PublicKey: pem,
131
-	}
132
-
133
-	body, err := json.Marshal(advertising)
134
-	if err != nil {
135
-		t.Fatal(err)
136
-	}
137
-
138
-	req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
139
-	w := httptest.NewRecorder()
140
-
141
-	handler(w, req)
142
-
143
-	res := w.Result()
144
-
145
-	if g, e := res.StatusCode, http.StatusCreated; g != e {
146
-		t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
147
-	}
148
-
149
-	peer, err := store.Get(peerID)
150
-	if err != nil {
151
-		t.Fatal(err)
152
-	}
153
-
154
-	if g, e := peer.PublicKey, advertising.PublicKey; !bytes.Equal(peer.PublicKey, advertising.PublicKey) {
155
-		t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
156
-	}
157
-
158
-}

+ 26
- 15
server/handler.go View File

@@ -1,13 +1,14 @@
1 1
 package server
2 2
 
3 3
 import (
4
+	"crypto/rsa"
4 5
 	"encoding/json"
5 6
 	"errors"
6 7
 	"net/http"
7 8
 	"time"
8 9
 
9 10
 	peering "forge.cadoles.com/wpetit/go-http-peering"
10
-	"forge.cadoles.com/wpetit/go-http-peering/crypto"
11
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
11 12
 )
12 13
 
13 14
 var (
@@ -18,34 +19,42 @@ var (
18 19
 	ErrUnauthorized              = errors.New("unauthorized")
19 20
 )
20 21
 
21
-func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
22
+func AdvertiseHandler(store peering.Store, key *rsa.PublicKey, funcs ...OptionFunc) http.HandlerFunc {
22 23
 
23 24
 	options := createOptions(funcs...)
24 25
 	logger := options.Logger
25 26
 
26 27
 	handler := func(w http.ResponseWriter, r *http.Request) {
27
-		advertising := &peering.AdvertisingRequest{}
28 28
 
29
-		decoder := json.NewDecoder(r.Body)
30
-		if err := decoder.Decode(advertising); err != nil {
31
-			logger.Printf("[ERROR] %s", err)
29
+		serverToken := r.Header.Get(ServerTokenHeader)
30
+		if serverToken == "" {
32 31
 			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
33 32
 			return
34 33
 		}
35 34
 
36
-		if !options.PeerIDValidator(advertising.ID) {
37
-			logger.Printf("[ERROR] %s", ErrInvalidAdvertisingRequest)
35
+		serverClaims, err := assertServerToken(key, serverToken)
36
+		if err != nil {
37
+			logger.Printf("[ERROR] %s", err)
38
+			sendError(w, http.StatusUnauthorized)
39
+			return
40
+		}
41
+
42
+		advertising := &peering.AdvertisingRequest{}
43
+
44
+		decoder := json.NewDecoder(r.Body)
45
+		if err := decoder.Decode(advertising); err != nil {
46
+			logger.Printf("[ERROR] %s", err)
38 47
 			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
39 48
 			return
40 49
 		}
41 50
 
42
-		if _, err := crypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil {
51
+		if _, err := peeringCrypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil {
43 52
 			logger.Printf("[ERROR] %s", err)
44 53
 			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
45 54
 			return
46 55
 		}
47 56
 
48
-		peer, err := store.Get(advertising.ID)
57
+		peer, err := store.Get(serverClaims.PeerID)
49 58
 
50 59
 		if err == nil {
51 60
 			logger.Printf("[ERROR] %s", ErrPeerIDAlreadyInUse)
@@ -61,7 +70,7 @@ func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc
61 70
 
62 71
 		attrs := filterAttributes(options.PeerAttributes, advertising.Attributes)
63 72
 
64
-		peer, err = store.Create(advertising.ID, attrs)
73
+		peer, err = store.Create(serverClaims.PeerID, attrs)
65 74
 		if err != nil {
66 75
 			logger.Printf("[ERROR] %s", err)
67 76
 			options.ErrorHandler(w, r, err)
@@ -74,6 +83,12 @@ func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc
74 83
 			return
75 84
 		}
76 85
 
86
+		if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
87
+			logger.Printf("[ERROR] %s", err)
88
+			options.ErrorHandler(w, r, err)
89
+			return
90
+		}
91
+
77 92
 		if err := store.UpdatePublicKey(peer.ID, advertising.PublicKey); err != nil {
78 93
 			logger.Printf("[ERROR] %s", err)
79 94
 			options.ErrorHandler(w, r, err)
@@ -212,10 +227,6 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
212 227
 	}
213 228
 }
214 229
 
215
-func DefaultPeerIDValidator(id peering.PeerID) bool {
216
-	return string(id) != ""
217
-}
218
-
219 230
 func filterAttributes(filters []string, attrs peering.PeerAttributes) peering.PeerAttributes {
220 231
 	filtered := peering.PeerAttributes{}
221 232
 	for _, key := range filters {

+ 88
- 58
server/middleware.go View File

@@ -3,13 +3,14 @@ package server
3 3
 import (
4 4
 	"bytes"
5 5
 	"context"
6
+	"crypto/rsa"
6 7
 	"crypto/sha256"
7 8
 	"errors"
9
+	"io"
8 10
 	"io/ioutil"
9
-	"strings"
10 11
 	"time"
11 12
 
12
-	"forge.cadoles.com/wpetit/go-http-peering/crypto"
13
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
13 14
 
14 15
 	peering "forge.cadoles.com/wpetit/go-http-peering"
15 16
 	jwt "github.com/dgrijalva/jwt-go"
@@ -18,8 +19,9 @@ import (
18 19
 )
19 20
 
20 21
 const (
21
-	AuthorizationType            = "Bearer"
22
-	KeyPeerID         ContextKey = "peerID"
22
+	ServerTokenHeader            = "X-Server-Token" // nolint: gosec
23
+	ClientTokenHeader            = "X-Client-Token"
24
+	KeyPeerID         ContextKey = "PeerID"
23 25
 )
24 26
 
25 27
 var (
@@ -30,82 +32,47 @@ var (
30 32
 
31 33
 type ContextKey string
32 34
 
33
-func Authenticate(store peering.Store, funcs ...OptionFunc) func(http.Handler) http.Handler {
35
+func Authenticate(store peering.Store, key *rsa.PublicKey, funcs ...OptionFunc) func(http.Handler) http.Handler {
34 36
 	options := createOptions(funcs...)
35 37
 	logger := options.Logger
36 38
 
37 39
 	middleware := func(next http.Handler) http.Handler {
38 40
 		fn := func(w http.ResponseWriter, r *http.Request) {
39
-			authorization := r.Header.Get("Authorization")
40 41
 
41
-			if authorization == "" {
42
+			serverToken := r.Header.Get(ServerTokenHeader)
43
+			if serverToken == "" {
42 44
 				sendError(w, http.StatusUnauthorized)
43 45
 				return
44 46
 			}
45 47
 
46
-			parts := strings.SplitN(authorization, " ", 2)
47
-
48
-			if len(parts) != 2 || parts[0] != AuthorizationType {
48
+			clientToken := r.Header.Get(ClientTokenHeader)
49
+			if clientToken == "" {
49 50
 				sendError(w, http.StatusUnauthorized)
50 51
 				return
51 52
 			}
52 53
 
53
-			token, err := jwt.ParseWithClaims(parts[1], &peering.PeerClaims{}, func(token *jwt.Token) (interface{}, error) {
54
-				claims, ok := token.Claims.(*peering.PeerClaims)
55
-				if !ok {
56
-					return nil, ErrInvalidClaims
57
-				}
58
-				peerID := peering.PeerID(claims.Issuer)
59
-				peer, err := store.Get(peerID)
60
-				if err != nil {
61
-					return nil, err
62
-				}
63
-				if peer.Status == peering.StatusRejected {
64
-					return nil, ErrPeerRejected
65
-				}
66
-				if peer.Status != peering.StatusPeered {
67
-					return nil, ErrNotPeered
68
-				}
69
-				publicKey, err := crypto.DecodePEMToPublicKey(peer.PublicKey)
70
-				if err != nil {
71
-					return nil, err
72
-				}
73
-				return publicKey, nil
74
-			})
75
-			if err != nil || !token.Valid {
54
+			serverClaims, err := assertServerToken(key, serverToken)
55
+			if err != nil {
76 56
 				logger.Printf("[ERROR] %s", err)
77
-				if err == ErrPeerRejected {
78
-					sendError(w, http.StatusForbidden)
79
-				} else {
80
-					sendError(w, http.StatusUnauthorized)
81
-				}
82
-				return
83
-			}
84
-
85
-			claims, ok := token.Claims.(*peering.PeerClaims)
86
-			if !ok {
87
-				logger.Printf("[ERROR] %s", ErrInvalidClaims)
88 57
 				sendError(w, http.StatusUnauthorized)
89 58
 				return
90 59
 			}
91 60
 
92
-			body, err := ioutil.ReadAll(r.Body)
61
+			clientClaims, err := assertClientToken(serverClaims.PeerID, store, clientToken)
93 62
 			if err != nil {
94 63
 				logger.Printf("[ERROR] %s", err)
95
-				sendError(w, http.StatusInternalServerError)
96
-				return
97
-			}
98
-
99
-			if err := r.Body.Close(); err != nil {
100
-				logger.Printf("[ERROR] %s", err)
101
-				sendError(w, http.StatusInternalServerError)
64
+				if err == peering.ErrPeerNotFound {
65
+					sendError(w, http.StatusUnauthorized)
66
+				} else {
67
+					sendError(w, http.StatusInternalServerError)
68
+				}
102 69
 				return
103 70
 			}
104 71
 
105
-			match, err := compareChecksum(body, claims.BodySum)
72
+			match, body, err := assertBodySum(r.Body, clientClaims.BodySum)
106 73
 			if err != nil {
107 74
 				logger.Printf("[ERROR] %s", err)
108
-				sendError(w, http.StatusUnauthorized)
75
+				sendError(w, http.StatusInternalServerError)
109 76
 				return
110 77
 			}
111 78
 
@@ -115,15 +82,13 @@ func Authenticate(store peering.Store, funcs ...OptionFunc) func(http.Handler) h
115 82
 				return
116 83
 			}
117 84
 
118
-			peerID := peering.PeerID(claims.Issuer)
119
-
120
-			if err := store.UpdateLastContact(peerID, r.RemoteAddr, time.Now()); err != nil {
85
+			if err := store.UpdateLastContact(serverClaims.PeerID, r.RemoteAddr, time.Now()); err != nil {
121 86
 				logger.Printf("[ERROR] %s", err)
122 87
 				sendError(w, http.StatusInternalServerError)
123 88
 				return
124 89
 			}
125 90
 
126
-			ctx := context.WithValue(r.Context(), KeyPeerID, peerID)
91
+			ctx := context.WithValue(r.Context(), KeyPeerID, serverClaims.PeerID)
127 92
 			r = r.WithContext(ctx)
128 93
 			r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
129 94
 
@@ -143,6 +108,71 @@ func GetPeerID(r *http.Request) (peering.PeerID, error) {
143 108
 	return peerID, nil
144 109
 }
145 110
 
111
+func assertServerToken(key *rsa.PublicKey, serverToken string) (*peering.ServerTokenClaims, error) {
112
+	fn := func(token *jwt.Token) (interface{}, error) {
113
+		return key, nil
114
+	}
115
+	token, err := jwt.ParseWithClaims(serverToken, &peering.ServerTokenClaims{}, fn)
116
+	if err != nil {
117
+		return nil, err
118
+	}
119
+	if !token.Valid {
120
+		return nil, ErrInvalidClaims
121
+	}
122
+	claims, ok := token.Claims.(*peering.ServerTokenClaims)
123
+	if !ok {
124
+		return nil, ErrInvalidClaims
125
+	}
126
+	return claims, nil
127
+}
128
+
129
+func assertClientToken(peerID peering.PeerID, store peering.Store, clientToken string) (*peering.ClientTokenClaims, error) {
130
+	fn := func(token *jwt.Token) (interface{}, error) {
131
+		peer, err := store.Get(peerID)
132
+		if err != nil {
133
+			return nil, err
134
+		}
135
+		if peer.Status == peering.StatusRejected {
136
+			return nil, ErrPeerRejected
137
+		}
138
+		if peer.Status != peering.StatusPeered {
139
+			return nil, ErrNotPeered
140
+		}
141
+		publicKey, err := peeringCrypto.DecodePEMToPublicKey(peer.PublicKey)
142
+		if err != nil {
143
+			return nil, err
144
+		}
145
+		return publicKey, nil
146
+	}
147
+	token, err := jwt.ParseWithClaims(clientToken, &peering.ClientTokenClaims{}, fn)
148
+	if err != nil {
149
+		return nil, err
150
+	}
151
+	if !token.Valid {
152
+		return nil, ErrInvalidClaims
153
+	}
154
+	claims, ok := token.Claims.(*peering.ClientTokenClaims)
155
+	if !ok {
156
+		return nil, ErrInvalidClaims
157
+	}
158
+	return claims, nil
159
+}
160
+
161
+func assertBodySum(rc io.ReadCloser, bodySum []byte) (bool, []byte, error) {
162
+	body, err := ioutil.ReadAll(rc)
163
+	if err != nil {
164
+		return false, nil, err
165
+	}
166
+	if err := rc.Close(); err != nil {
167
+		return false, nil, err
168
+	}
169
+	match, err := compareChecksum(body, bodySum)
170
+	if err != nil {
171
+		return false, nil, err
172
+	}
173
+	return match, body, nil
174
+}
175
+
146 176
 func sendError(w http.ResponseWriter, status int) {
147 177
 	http.Error(w, http.StatusText(status), status)
148 178
 }

+ 6
- 10
server/option.go View File

@@ -4,8 +4,6 @@ import (
4 4
 	"log"
5 5
 	"net/http"
6 6
 	"os"
7
-
8
-	peering "forge.cadoles.com/wpetit/go-http-peering"
9 7
 )
10 8
 
11 9
 type Logger interface {
@@ -13,10 +11,9 @@ type Logger interface {
13 11
 }
14 12
 
15 13
 type Options struct {
16
-	PeerAttributes  []string
17
-	ErrorHandler    ErrorHandler
18
-	PeerIDValidator func(peering.PeerID) bool
19
-	Logger          Logger
14
+	PeerAttributes []string
15
+	ErrorHandler   ErrorHandler
16
+	Logger         Logger
20 17
 }
21 18
 
22 19
 type OptionFunc func(*Options)
@@ -44,10 +41,9 @@ func WithErrorHandler(handler ErrorHandler) OptionFunc {
44 41
 func defaultOptions() *Options {
45 42
 	logger := log.New(os.Stdout, "[go-http-peering] ", log.LstdFlags|log.Lshortfile)
46 43
 	return &Options{
47
-		PeerAttributes:  []string{"Label"},
48
-		ErrorHandler:    DefaultErrorHandler,
49
-		PeerIDValidator: DefaultPeerIDValidator,
50
-		Logger:          logger,
44
+		PeerAttributes: []string{"Label"},
45
+		ErrorHandler:   DefaultErrorHandler,
46
+		Logger:         logger,
51 47
 	}
52 48
 }
53 49
 

+ 25
- 8
test/advertise_test.go View File

@@ -5,6 +5,11 @@ import (
5 5
 	"testing"
6 6
 	"time"
7 7
 
8
+	"forge.cadoles.com/wpetit/go-http-peering/client"
9
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
10
+	"forge.cadoles.com/wpetit/go-http-peering/memory"
11
+	"forge.cadoles.com/wpetit/go-http-peering/server"
12
+
8 13
 	peering "forge.cadoles.com/wpetit/go-http-peering"
9 14
 	"forge.cadoles.com/wpetit/go-http-peering/crypto"
10 15
 )
@@ -15,19 +20,35 @@ func TestAdvertise(t *testing.T) {
15 20
 		t.SkipNow()
16 21
 	}
17 22
 
18
-	id, pk, client, store := setup(t)
23
+	store := memory.NewStore()
24
+	serverPK := mustGeneratePrivateKey()
25
+	clientPK := mustGeneratePrivateKey()
26
+	peerID := peering.NewPeerID()
27
+
28
+	serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID)
29
+	if err != nil {
30
+		t.Fatal(err)
31
+	}
32
+
33
+	advertise := server.AdvertiseHandler(store, &serverPK.PublicKey)
34
+
35
+	client := client.New(
36
+		client.WithHTTPClient(NewHTTPClientMock(advertise)),
37
+		client.WithPrivateKey(clientPK),
38
+		client.WithServerToken(serverToken),
39
+	)
19 40
 
20 41
 	attrs := peering.PeerAttributes{}
21 42
 	if err := client.Advertise(attrs); err != nil {
22 43
 		t.Fatal(err)
23 44
 	}
24 45
 
25
-	peer, err := store.Get(id)
46
+	peer, err := store.Get(peerID)
26 47
 	if err != nil {
27 48
 		t.Error(err)
28 49
 	}
29 50
 
30
-	if g, e := peer.ID, id; g != e {
51
+	if g, e := peer.ID, peerID; g != e {
31 52
 		t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
32 53
 	}
33 54
 
@@ -40,11 +61,7 @@ func TestAdvertise(t *testing.T) {
40 61
 		t.Error("peer.LastContact should not be time.Time zero value")
41 62
 	}
42 63
 
43
-	if peer.LastAddress == "" {
44
-		t.Error("peer.LastAddress should not be empty")
45
-	}
46
-
47
-	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
64
+	pem, err := crypto.EncodePublicKeyToPEM(clientPK.Public())
48 65
 	if err != nil {
49 66
 		t.Fatal(err)
50 67
 	}

+ 44
- 6
test/ping_test.go View File

@@ -4,6 +4,10 @@ import (
4 4
 	"testing"
5 5
 
6 6
 	peering "forge.cadoles.com/wpetit/go-http-peering"
7
+	"forge.cadoles.com/wpetit/go-http-peering/client"
8
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
9
+	"forge.cadoles.com/wpetit/go-http-peering/memory"
10
+	"forge.cadoles.com/wpetit/go-http-peering/server"
7 11
 )
8 12
 
9 13
 func TestPing(t *testing.T) {
@@ -12,33 +16,67 @@ func TestPing(t *testing.T) {
12 16
 		t.SkipNow()
13 17
 	}
14 18
 
15
-	id, _, client, store := setup(t)
19
+	store := memory.NewStore()
20
+	serverPK := mustGeneratePrivateKey()
21
+	clientPK := mustGeneratePrivateKey()
22
+	peerID := peering.NewPeerID()
16 23
 
24
+	// Generate a server token for the peer client
25
+	serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID)
26
+	if err != nil {
27
+		t.Fatal(err)
28
+	}
29
+
30
+	advertise := server.AdvertiseHandler(store, &serverPK.PublicKey)
31
+	// Create advertise client
32
+	c := client.New(
33
+		client.WithHTTPClient(NewHTTPClientMock(advertise)),
34
+		client.WithPrivateKey(clientPK),
35
+		client.WithServerToken(serverToken),
36
+	)
37
+
38
+	// Advertise client with empty peer attributes
17 39
 	attrs := peering.PeerAttributes{}
18
-	if err := client.Advertise(attrs); err != nil {
40
+	if err := c.Advertise(attrs); err != nil {
19 41
 		t.Fatal(err)
20 42
 	}
21 43
 
22
-	peer, err := store.Get(id)
44
+	// Retrieve peer from store
45
+	peer, err := store.Get(peerID)
23 46
 	if err != nil {
24 47
 		t.Fatal(err)
25 48
 	}
26 49
 
50
+	// Store last contact after advertising
27 51
 	lastContact := peer.LastContact
28 52
 
29
-	if err := store.Accept(id); err != nil {
53
+	// Accept peer
54
+	if err := store.Accept(peerID); err != nil {
30 55
 		t.Error(err)
31 56
 	}
32 57
 
33
-	if err := client.Ping(); err != nil {
58
+	// Create ping authenticated handler
59
+	ping := server.Authenticate(store, &serverPK.PublicKey)(server.PingHandler(store))
60
+
61
+	// Create client
62
+	c = client.New(
63
+		client.WithHTTPClient(NewHTTPClientMock(ping)),
64
+		client.WithPrivateKey(clientPK),
65
+		client.WithServerToken(serverToken),
66
+	)
67
+
68
+	// Do ping
69
+	if err := c.Ping(); err != nil {
34 70
 		t.Fatal(err)
35 71
 	}
36 72
 
37
-	peer, err = store.Get(id)
73
+	// Retrieve peer
74
+	peer, err = store.Get(peerID)
38 75
 	if err != nil {
39 76
 		t.Fatal(err)
40 77
 	}
41 78
 
79
+	// Assert that last contact has changed after ping
42 80
 	if peer.LastContact == lastContact {
43 81
 		t.Error("peer.LastContact should have been updated")
44 82
 	}

+ 47
- 8
test/update_test.go View File

@@ -6,7 +6,11 @@ import (
6 6
 	"time"
7 7
 
8 8
 	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+	"forge.cadoles.com/wpetit/go-http-peering/client"
9 10
 	"forge.cadoles.com/wpetit/go-http-peering/crypto"
11
+	peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto"
12
+	"forge.cadoles.com/wpetit/go-http-peering/memory"
13
+	"forge.cadoles.com/wpetit/go-http-peering/server"
10 14
 )
11 15
 
12 16
 func TestUpdate(t *testing.T) {
@@ -15,42 +19,77 @@ func TestUpdate(t *testing.T) {
15 19
 		t.SkipNow()
16 20
 	}
17 21
 
18
-	id, pk, client, store := setup(t)
22
+	store := memory.NewStore()
23
+	serverPK := mustGeneratePrivateKey()
24
+	clientPK := mustGeneratePrivateKey()
25
+	peerID := peering.NewPeerID()
19 26
 
20
-	attrs := peering.PeerAttributes{}
27
+	// Generate a server token for the peer client
28
+	serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID)
29
+	if err != nil {
30
+		t.Fatal(err)
31
+	}
32
+
33
+	advertise := server.AdvertiseHandler(store, &serverPK.PublicKey)
34
+	// Create advertise client
35
+	c := client.New(
36
+		client.WithHTTPClient(NewHTTPClientMock(advertise)),
37
+		client.WithPrivateKey(clientPK),
38
+		client.WithServerToken(serverToken),
39
+	)
21 40
 
22
-	if err := client.Advertise(attrs); err != nil {
41
+	// Advertise client with empty peer attributes
42
+	attrs := peering.PeerAttributes{}
43
+	if err := c.Advertise(attrs); err != nil {
23 44
 		t.Fatal(err)
24 45
 	}
25 46
 
26
-	if err := store.Accept(id); err != nil {
47
+	// Accept peer
48
+	if err := store.Accept(peerID); err != nil {
27 49
 		t.Error(err)
28 50
 	}
29 51
 
52
+	// Create authenticated update handler
53
+	update := server.Authenticate(store, &serverPK.PublicKey)(server.UpdateHandler(store))
54
+
55
+	// Create update client
56
+	c = client.New(
57
+		client.WithHTTPClient(NewHTTPClientMock(update)),
58
+		client.WithPrivateKey(clientPK),
59
+		client.WithServerToken(serverToken),
60
+	)
61
+
62
+	// Update local attributes
30 63
 	attrs["Label"] = "Foo Bar"
31
-	if err := client.UpdateAttributes(attrs); err != nil {
64
+
65
+	// Update attributes
66
+	if err := c.UpdateAttributes(attrs); err != nil {
32 67
 		t.Fatal(err)
33 68
 	}
34 69
 
35
-	peer, err := store.Get(id)
70
+	// Retrieve peer from store
71
+	peer, err := store.Get(peerID)
36 72
 	if err != nil {
37 73
 		t.Fatal(err)
38 74
 	}
39 75
 
40
-	if g, e := peer.ID, id; g != e {
76
+	// Assert that peer's ID did not change
77
+	if g, e := peer.ID, peerID; g != e {
41 78
 		t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
42 79
 	}
43 80
 
81
+	// Assert that stored attributes are the same as the local ones
44 82
 	if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) {
45 83
 		t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e)
46 84
 	}
47 85
 
86
+	// Assert that lastContact has changed
48 87
 	var defaultTime time.Time
49 88
 	if peer.LastContact == defaultTime {
50 89
 		t.Error("peer.LastContact should not be time.Time zero value")
51 90
 	}
52 91
 
53
-	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
92
+	pem, err := crypto.EncodePublicKeyToPEM(clientPK.Public())
54 93
 	if err != nil {
55 94
 		t.Fatal(err)
56 95
 	}

+ 14
- 42
test/util_test.go View File

@@ -3,15 +3,8 @@ package test
3 3
 import (
4 4
 	"crypto/rand"
5 5
 	"crypto/rsa"
6
-	"fmt"
7
-	"net"
8 6
 	"net/http"
9
-	"testing"
10
-
11
-	peering "forge.cadoles.com/wpetit/go-http-peering"
12
-	"forge.cadoles.com/wpetit/go-http-peering/client"
13
-	"forge.cadoles.com/wpetit/go-http-peering/memory"
14
-	"forge.cadoles.com/wpetit/go-http-peering/server"
7
+	"net/http/httptest"
15 8
 )
16 9
 
17 10
 func mustGeneratePrivateKey() *rsa.PrivateKey {
@@ -22,43 +15,22 @@ func mustGeneratePrivateKey() *rsa.PrivateKey {
22 15
 	return privateKey
23 16
 }
24 17
 
25
-func startServer(store peering.Store) (int, error) {
26
-	listener, err := net.Listen("tcp", ":0")
27
-	if err != nil {
28
-		return -1, err
29
-	}
30
-	mux := createServerMux(store)
31
-	go http.Serve(listener, mux)
32
-	port := listener.Addr().(*net.TCPAddr).Port
33
-	return port, nil
18
+type HTTPClientMock struct {
19
+	handler  http.Handler
20
+	recorder *httptest.ResponseRecorder
34 21
 }
35 22
 
36
-func createServerMux(store peering.Store) *http.ServeMux {
37
-	mux := http.NewServeMux()
38
-	mux.HandleFunc(peering.AdvertisePath, server.AdvertiseHandler(store))
39
-	update := server.Authenticate(store)(server.UpdateHandler(store))
40
-	mux.Handle(peering.UpdatePath, update)
41
-	ping := server.Authenticate(store)(server.PingHandler(store))
42
-	mux.Handle(peering.PingPath, ping)
43
-	return mux
23
+func (c *HTTPClientMock) Do(r *http.Request) (*http.Response, error) {
24
+	w := httptest.NewRecorder()
25
+	c.recorder = w
26
+	c.handler.ServeHTTP(w, r)
27
+	return w.Result(), nil
44 28
 }
45 29
 
46
-func setup(t *testing.T) (peering.PeerID, *rsa.PrivateKey, *client.Client, peering.Store) {
47
-	store := memory.NewStore()
48
-
49
-	port, err := startServer(store)
50
-	if err != nil {
51
-		t.Fatal(err)
52
-	}
53
-
54
-	pk := mustGeneratePrivateKey()
55
-	id := peering.NewPeerID()
56
-
57
-	c := client.New(
58
-		client.WithBaseURL(fmt.Sprintf("http://127.0.0.1:%d", port)),
59
-		client.WithPrivateKey(pk),
60
-		client.WithPeerID(id),
61
-	)
30
+func (c *HTTPClientMock) Recorder() *httptest.ResponseRecorder {
31
+	return c.recorder
32
+}
62 33
 
63
-	return id, pk, c, store
34
+func NewHTTPClientMock(h http.Handler) *HTTPClientMock {
35
+	return &HTTPClientMock{h, nil}
64 36
 }

Loading…
Cancel
Save