Browse Source

Initial commit

redesign
William Petit 6 months ago
commit
2a8a20195a

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+/coverage
2
+/vendor

+ 30
- 0
Makefile View File

@@ -0,0 +1,30 @@
1
+test:
2
+	go clean -testcache
3
+	go test -cover -v ./...
4
+
5
+watch:
6
+	modd	
7
+
8
+deps:
9
+	GO111MODULE=off go get -u golang.org/x/tools/cmd/godoc
10
+	GO111MODULE=off go get -u github.com/cortesi/modd/cmd/modd
11
+	GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
12
+	GO111MODULE=off go get -u github.com/lmika/goseq
13
+
14
+tidy:
15
+	go mod tidy
16
+
17
+lint:
18
+	golangci-lint run --tests=false --enable-all
19
+
20
+sequence-diagram: sd-advertise sd-update sd-ping
21
+
22
+sd-%:
23
+	goseq doc/sequence-diagram/$*.seq > doc/sequence-diagram/$*.svg
24
+
25
+doc:
26
+	@echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/wpetit/go-http-peering to see the documentation"
27
+	godoc -http=:6060
28
+
29
+
30
+.PHONY: test lint doc sequence-diagram

+ 25
- 0
README.md View File

@@ -0,0 +1,25 @@
1
+# go-http-peering
2
+
3
+Librairie implémentant un protocole d'authentification par "appairage" d'un serveur et client HTTP basé sur [JWT](https://jwt.io/).
4
+
5
+[Documentation](https://godoc.org/forge.cadoles.com/wpetit/go-http-peering)
6
+
7
+## Séquences
8
+
9
+### Annonce du client
10
+
11
+![](./doc/sequence-diagram/advertise.svg)
12
+
13
+### Mise à jour des attributs
14
+
15
+![](./doc/sequence-diagram/update.svg)
16
+
17
+### Ping
18
+
19
+![](./doc/sequence-diagram/ping.svg)
20
+
21
+**Statut** Instable
22
+
23
+## Licence
24
+
25
+AGPL-3.0

+ 16
- 0
chi/mount.go View File

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

+ 35
- 0
chi/mount_test.go View File

@@ -0,0 +1,35 @@
1
+package chi
2
+
3
+import (
4
+	"testing"
5
+
6
+	peering "forge.cadoles.com/wpetit/go-http-peering"
7
+	"forge.cadoles.com/wpetit/go-http-peering/memory"
8
+	"github.com/go-chi/chi"
9
+)
10
+
11
+func TestMount(t *testing.T) {
12
+
13
+	r := chi.NewRouter()
14
+	store := memory.NewStore()
15
+
16
+	Mount(r, store)
17
+
18
+	routes := r.Routes()
19
+
20
+	if g, e := len(routes), 3; g != e {
21
+		t.Errorf("len(r.Routes()): got '%v', expected '%v'", g, e)
22
+	}
23
+
24
+	if g, e := routes[0].Pattern, peering.AdvertisePath; g != e {
25
+		t.Errorf("routes[0].Pattern: got '%v', expected '%v'", g, e)
26
+	}
27
+
28
+	if g, e := routes[1].Pattern, peering.PingPath; g != e {
29
+		t.Errorf("routes[1].Pattern: got '%v', expected '%v'", g, e)
30
+	}
31
+
32
+	if g, e := routes[2].Pattern, peering.UpdatePath; g != e {
33
+		t.Errorf("routes[2].Pattern: got '%v', expected '%v'", g, e)
34
+	}
35
+}

+ 187
- 0
client/client.go View File

@@ -0,0 +1,187 @@
1
+package client
2
+
3
+import (
4
+	"bytes"
5
+	"crypto/sha256"
6
+	"encoding/json"
7
+	"errors"
8
+	"fmt"
9
+	"net/http"
10
+	"time"
11
+
12
+	"github.com/dgrijalva/jwt-go"
13
+
14
+	peering "forge.cadoles.com/wpetit/go-http-peering"
15
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
16
+	"forge.cadoles.com/wpetit/go-http-peering/server"
17
+)
18
+
19
+const (
20
+	DefaultBody = "{}"
21
+)
22
+
23
+var (
24
+	ErrInvalidPrivateKey  = errors.New("invalid private key")
25
+	ErrUnexpectedResponse = errors.New("unexpected response")
26
+	ErrUnauthorized       = errors.New("unauthorized")
27
+	ErrRejected           = errors.New("rejected")
28
+)
29
+
30
+type Client struct {
31
+	options *Options
32
+}
33
+
34
+func (c *Client) Advertise(attrs peering.PeerAttributes) error {
35
+	url := c.options.BaseURL + peering.AdvertisePath
36
+
37
+	publicKey, err := crypto.EncodePublicKeyToPEM(c.options.PrivateKey.Public())
38
+	if err != nil {
39
+		return err
40
+	}
41
+
42
+	data := &peering.AdvertisingRequest{
43
+		ID:         c.options.PeerID,
44
+		Attributes: attrs,
45
+		PublicKey:  publicKey,
46
+	}
47
+
48
+	req, _, err := c.createPostRequest(url, data)
49
+	if err != nil {
50
+		return err
51
+	}
52
+
53
+	res, err := c.options.HTTPClient.Do(req)
54
+
55
+	switch res.StatusCode {
56
+	case http.StatusCreated:
57
+		return nil
58
+	case http.StatusConflict:
59
+		return peering.ErrPeerExists
60
+	default:
61
+		return ErrUnexpectedResponse
62
+	}
63
+}
64
+
65
+func (c *Client) UpdateAttributes(attrs peering.PeerAttributes) error {
66
+	url := c.options.BaseURL + peering.UpdatePath
67
+
68
+	data := &peering.UpdateRequest{
69
+		Attributes: attrs,
70
+	}
71
+
72
+	res, err := c.Post(url, data)
73
+	if err != nil {
74
+		return err
75
+	}
76
+
77
+	switch res.StatusCode {
78
+	case http.StatusNoContent:
79
+		return nil
80
+	case http.StatusUnauthorized:
81
+		return ErrUnauthorized
82
+	case http.StatusForbidden:
83
+		return ErrRejected
84
+	default:
85
+		return ErrUnexpectedResponse
86
+	}
87
+}
88
+
89
+func (c *Client) Ping() error {
90
+	url := c.options.BaseURL + peering.PingPath
91
+
92
+	res, err := c.Post(url, nil)
93
+	if err != nil {
94
+		return err
95
+	}
96
+
97
+	switch res.StatusCode {
98
+	case http.StatusNoContent:
99
+		return nil
100
+	case http.StatusUnauthorized:
101
+		return ErrUnauthorized
102
+	case http.StatusForbidden:
103
+		return ErrRejected
104
+	default:
105
+		return ErrUnexpectedResponse
106
+	}
107
+}
108
+
109
+func (c *Client) Get(url string) (*http.Response, error) {
110
+	req, err := http.NewRequest(http.MethodGet, url, nil)
111
+	if err != nil {
112
+		return nil, err
113
+	}
114
+	if err := c.signRequest(req, nil); err != nil {
115
+		return nil, err
116
+	}
117
+	return c.options.HTTPClient.Do(req)
118
+}
119
+
120
+func (c *Client) Post(url string, data interface{}) (*http.Response, error) {
121
+	req, body, err := c.createPostRequest(url, data)
122
+	if err != nil {
123
+		return nil, err
124
+	}
125
+	if err := c.signRequest(req, body); err != nil {
126
+		return nil, err
127
+	}
128
+	return c.options.HTTPClient.Do(req)
129
+}
130
+
131
+func (c *Client) createPostRequest(url string, data interface{}) (*http.Request, []byte, error) {
132
+	body, err := json.Marshal(data)
133
+	if err != nil {
134
+		return nil, nil, err
135
+	}
136
+
137
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
138
+	if err != nil {
139
+		return nil, nil, err
140
+	}
141
+
142
+	req.Header.Set("Content-Type", "application/json")
143
+
144
+	return req, body, nil
145
+}
146
+
147
+func (c *Client) signRequest(r *http.Request, body []byte) error {
148
+	bodySum, err := c.createBodySum(body)
149
+	if err != nil {
150
+		return err
151
+	}
152
+
153
+	token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.PeerClaims{
154
+		StandardClaims: jwt.StandardClaims{
155
+			NotBefore: time.Now().Unix(),
156
+			Issuer:    string(c.options.PeerID),
157
+			ExpiresAt: time.Now().Add(time.Minute * 10).Unix(),
158
+		},
159
+		BodySum: bodySum,
160
+	})
161
+
162
+	tokenStr, err := token.SignedString(c.options.PrivateKey)
163
+	if err != nil {
164
+		return err
165
+	}
166
+
167
+	r.Header.Set("Authorization", fmt.Sprintf("%s %s", server.AuthorizationType, tokenStr))
168
+
169
+	return nil
170
+}
171
+
172
+func (c *Client) createBodySum(body []byte) ([]byte, error) {
173
+	if body == nil || len(body) == 0 {
174
+		body = []byte(DefaultBody)
175
+	}
176
+	sha := sha256.New()
177
+	_, err := sha.Write(body)
178
+	if err != nil {
179
+		return nil, err
180
+	}
181
+	return sha.Sum(nil), nil
182
+}
183
+
184
+func New(funcs ...OptionFunc) *Client {
185
+	options := createOptions(funcs...)
186
+	return &Client{options}
187
+}

+ 56
- 0
client/option.go View File

@@ -0,0 +1,56 @@
1
+package client
2
+
3
+import (
4
+	"crypto/rsa"
5
+	"net/http"
6
+
7
+	peering "forge.cadoles.com/wpetit/go-http-peering"
8
+)
9
+
10
+type Options struct {
11
+	PeerID     peering.PeerID
12
+	HTTPClient *http.Client
13
+	BaseURL    string
14
+	PrivateKey *rsa.PrivateKey
15
+}
16
+
17
+type OptionFunc func(*Options)
18
+
19
+func WithPeerID(id peering.PeerID) OptionFunc {
20
+	return func(opts *Options) {
21
+		opts.PeerID = id
22
+	}
23
+}
24
+
25
+func WithPrivateKey(pk *rsa.PrivateKey) OptionFunc {
26
+	return func(opts *Options) {
27
+		opts.PrivateKey = pk
28
+	}
29
+}
30
+
31
+func WithHTTPClient(client *http.Client) OptionFunc {
32
+	return func(opts *Options) {
33
+		opts.HTTPClient = client
34
+	}
35
+}
36
+
37
+func WithBaseURL(url string) OptionFunc {
38
+	return func(opts *Options) {
39
+		opts.BaseURL = url
40
+	}
41
+}
42
+
43
+func defaultOptions() *Options {
44
+	return &Options{
45
+		HTTPClient: http.DefaultClient,
46
+		PeerID:     peering.NewPeerID(),
47
+	}
48
+}
49
+
50
+func createOptions(funcs ...OptionFunc) *Options {
51
+	options := defaultOptions()
52
+	for _, fn := range funcs {
53
+		fn(options)
54
+	}
55
+	return options
56
+}

+ 25
- 0
crypto/pem.go View File

@@ -0,0 +1,25 @@
1
+package crypto
2
+
3
+import (
4
+	"crypto/rsa"
5
+	"crypto/x509"
6
+	"encoding/pem"
7
+
8
+	jwt "github.com/dgrijalva/jwt-go"
9
+)
10
+
11
+func EncodePublicKeyToPEM(key interface{}) ([]byte, error) {
12
+	pub, err := x509.MarshalPKIXPublicKey(key)
13
+	if err != nil {
14
+		return nil, err
15
+	}
16
+	data := pem.EncodeToMemory(&pem.Block{
17
+		Type:  "PUBLIC KEY",
18
+		Bytes: pub,
19
+	})
20
+	return data, nil
21
+}
22
+
23
+func DecodePEMToPublicKey(pem []byte) (*rsa.PublicKey, error) {
24
+	return jwt.ParseRSAPublicKeyFromPEM(pem)
25
+}

+ 2
- 0
doc/sequence-diagram/advertise.seq View File

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

+ 35
- 0
doc/sequence-diagram/advertise.svg View File

@@ -0,0 +1,35 @@
1
+<?xml version="1.0"?>
2
+<!-- Generated by SVGo -->
3
+<svg width="711" height="196"
4
+     xmlns="http://www.w3.org/2000/svg"
5
+     xmlns:xlink="http://www.w3.org/1999/xlink">
6
+<defs>
7
+<style>
8
+@font-face {
9
+  font-family: 'DejaVuSans';
10
+  src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
11
+  font-weight: normal;
12
+  font-style: normal;
13
+}
14
+</style>
15
+</defs>
16
+<line x1="45" y1="24" x2="45" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17
+<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
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;" />
35
+</svg>

+ 2
- 0
doc/sequence-diagram/ping.seq View File

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

+ 35
- 0
doc/sequence-diagram/ping.svg View File

@@ -0,0 +1,35 @@
1
+<?xml version="1.0"?>
2
+<!-- Generated by SVGo -->
3
+<svg width="446" height="180"
4
+     xmlns="http://www.w3.org/2000/svg"
5
+     xmlns:xlink="http://www.w3.org/1999/xlink">
6
+<defs>
7
+<style>
8
+@font-face {
9
+  font-family: 'DejaVuSans';
10
+  src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
11
+  font-weight: normal;
12
+  font-style: normal;
13
+}
14
+</style>
15
+</defs>
16
+<line x1="45" y1="24" x2="45" y2="156" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17
+<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
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;" />
35
+</svg>

+ 2
- 0
doc/sequence-diagram/update.seq View File

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

+ 36
- 0
doc/sequence-diagram/update.svg View File

@@ -0,0 +1,36 @@
1
+<?xml version="1.0"?>
2
+<!-- Generated by SVGo -->
3
+<svg width="446" height="212"
4
+     xmlns="http://www.w3.org/2000/svg"
5
+     xmlns:xlink="http://www.w3.org/1999/xlink">
6
+<defs>
7
+<style>
8
+@font-face {
9
+  font-family: 'DejaVuSans';
10
+  src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
11
+  font-weight: normal;
12
+  font-style: normal;
13
+}
14
+</style>
15
+</defs>
16
+<line x1="45" y1="24" x2="45" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
17
+<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
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;" />
36
+</svg>

+ 7
- 0
go.mod View File

@@ -0,0 +1,7 @@
1
+module forge.cadoles.com/wpetit/go-http-peering
2
+
3
+require (
4
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
5
+	github.com/go-chi/chi v4.0.1+incompatible
6
+	github.com/pborman/uuid v1.2.0
7
+)

+ 8
- 0
go.sum View File

@@ -0,0 +1,8 @@
1
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
2
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
3
+github.com/go-chi/chi v4.0.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA=
4
+github.com/go-chi/chi v4.0.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
5
+github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
6
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
8
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=

+ 153
- 0
memory/store.go View File

@@ -0,0 +1,153 @@
1
+package memory
2
+
3
+import (
4
+	"sync"
5
+	"time"
6
+
7
+	peering "forge.cadoles.com/wpetit/go-http-peering"
8
+)
9
+
10
+type Store struct {
11
+	peers map[peering.PeerID]*peering.Peer
12
+	mutex sync.RWMutex
13
+}
14
+
15
+func (s *Store) Create(id peering.PeerID, attrs peering.PeerAttributes) (*peering.Peer, error) {
16
+	s.mutex.Lock()
17
+	defer s.mutex.Unlock()
18
+
19
+	if _, exists := s.peers[id]; exists {
20
+		return nil, peering.ErrPeerExists
21
+	}
22
+
23
+	peer := &peering.Peer{
24
+		PeerHeader: peering.PeerHeader{
25
+			ID:     id,
26
+			Status: peering.StatusPending,
27
+		},
28
+		Attributes: attrs,
29
+	}
30
+
31
+	s.peers[id] = peer
32
+
33
+	return copy(peer), nil
34
+}
35
+
36
+func (s *Store) Get(id peering.PeerID) (*peering.Peer, error) {
37
+	s.mutex.RLock()
38
+	defer s.mutex.RUnlock()
39
+
40
+	peer, exists := s.peers[id]
41
+	if !exists {
42
+		return nil, peering.ErrPeerNotFound
43
+	}
44
+
45
+	return copy(peer), nil
46
+}
47
+
48
+func (s *Store) List() ([]peering.PeerHeader, error) {
49
+	s.mutex.RLock()
50
+	defer s.mutex.RUnlock()
51
+
52
+	headers := make([]peering.PeerHeader, len(s.peers))
53
+	i := 0
54
+	for _, p := range s.peers {
55
+		headers[i] = p.PeerHeader
56
+		i++
57
+	}
58
+
59
+	return headers, nil
60
+}
61
+
62
+func (s *Store) UpdateAttributes(id peering.PeerID, attrs peering.PeerAttributes) error {
63
+	s.mutex.Lock()
64
+	defer s.mutex.Unlock()
65
+
66
+	peer, exists := s.peers[id]
67
+	if !exists {
68
+		return peering.ErrPeerNotFound
69
+	}
70
+
71
+	peer.Attributes = attrs
72
+
73
+	return nil
74
+}
75
+
76
+func (s *Store) UpdateLastContact(id peering.PeerID, remoteAddress string, ts time.Time) error {
77
+	s.mutex.Lock()
78
+	defer s.mutex.Unlock()
79
+
80
+	peer, exists := s.peers[id]
81
+	if !exists {
82
+		return peering.ErrPeerNotFound
83
+	}
84
+
85
+	peer.LastAddress = remoteAddress
86
+	peer.LastContact = ts
87
+
88
+	return nil
89
+}
90
+
91
+func (s *Store) UpdatePublicKey(id peering.PeerID, publicKey []byte) error {
92
+	s.mutex.Lock()
93
+	defer s.mutex.Unlock()
94
+
95
+	peer, exists := s.peers[id]
96
+	if !exists {
97
+		return peering.ErrPeerNotFound
98
+	}
99
+
100
+	peer.PublicKey = publicKey
101
+
102
+	return nil
103
+}
104
+
105
+func (s *Store) Delete(id peering.PeerID) error {
106
+	s.mutex.Lock()
107
+	defer s.mutex.Unlock()
108
+
109
+	if _, exists := s.peers[id]; !exists {
110
+		return peering.ErrPeerNotFound
111
+	}
112
+
113
+	delete(s.peers, id)
114
+
115
+	return nil
116
+}
117
+
118
+func (s *Store) Accept(id peering.PeerID) error {
119
+	return s.updateStatus(id, peering.StatusPeered)
120
+}
121
+
122
+func (s *Store) Forget(id peering.PeerID) error {
123
+	return s.updateStatus(id, peering.StatusPending)
124
+}
125
+
126
+func (s *Store) Reject(id peering.PeerID) error {
127
+	return s.updateStatus(id, peering.StatusRejected)
128
+}
129
+
130
+func (s *Store) updateStatus(id peering.PeerID, status peering.PeerStatus) error {
131
+	s.mutex.Lock()
132
+	defer s.mutex.Unlock()
133
+
134
+	peer, exists := s.peers[id]
135
+	if !exists {
136
+		return peering.ErrPeerNotFound
137
+	}
138
+
139
+	peer.Status = status
140
+
141
+	return nil
142
+}
143
+
144
+func NewStore() *Store {
145
+	return &Store{
146
+		peers: make(map[peering.PeerID]*peering.Peer),
147
+	}
148
+}
149
+
150
+func copy(p *peering.Peer) *peering.Peer {
151
+	copy := *p
152
+	return &copy
153
+}

+ 8
- 0
modd.conf View File

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

+ 37
- 0
peer.go View File

@@ -0,0 +1,37 @@
1
+package peering
2
+
3
+import (
4
+	"time"
5
+
6
+	"github.com/pborman/uuid"
7
+)
8
+
9
+type PeerID string
10
+
11
+func NewPeerID() PeerID {
12
+	id := uuid.New()
13
+	return PeerID(id)
14
+}
15
+
16
+type PeerStatus int
17
+
18
+const (
19
+	StatusPending PeerStatus = iota
20
+	StatusPeered
21
+	StatusRejected
22
+)
23
+
24
+type PeerHeader struct {
25
+	ID          PeerID
26
+	Status      PeerStatus
27
+	LastAddress string
28
+	LastContact time.Time
29
+}
30
+
31
+type PeerAttributes map[string]interface{}
32
+
33
+type Peer struct {
34
+	PeerHeader
35
+	Attributes PeerAttributes
36
+	PublicKey  []byte
37
+}

+ 24
- 0
request.go View File

@@ -0,0 +1,24 @@
1
+package peering
2
+
3
+import jwt "github.com/dgrijalva/jwt-go"
4
+
5
+const (
6
+	AdvertisePath = "/advertise"
7
+	UpdatePath    = "/update"
8
+	PingPath      = "/ping"
9
+)
10
+
11
+type AdvertisingRequest struct {
12
+	ID         PeerID
13
+	Attributes PeerAttributes
14
+	PublicKey  []byte
15
+}
16
+
17
+type UpdateRequest struct {
18
+	Attributes PeerAttributes
19
+}
20
+
21
+type PeerClaims struct {
22
+	jwt.StandardClaims
23
+	BodySum []byte `json:"bodySum"`
24
+}

+ 158
- 0
server/advertise_test.go View File

@@ -0,0 +1,158 @@
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
+}

+ 227
- 0
server/handler.go View File

@@ -0,0 +1,227 @@
1
+package server
2
+
3
+import (
4
+	"encoding/json"
5
+	"errors"
6
+	"net/http"
7
+	"time"
8
+
9
+	peering "forge.cadoles.com/wpetit/go-http-peering"
10
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
11
+)
12
+
13
+var (
14
+	ErrInvalidAdvertisingRequest = errors.New("invalid advertising request")
15
+	ErrInvalidUpdateRequest      = errors.New("invalid update request")
16
+	ErrPeerRejected              = errors.New("peer rejected")
17
+	ErrPeerIDAlreadyInUse        = errors.New("peer id already in use")
18
+	ErrUnauthorized              = errors.New("unauthorized")
19
+)
20
+
21
+func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
22
+
23
+	options := createOptions(funcs...)
24
+	logger := options.Logger
25
+
26
+	handler := func(w http.ResponseWriter, r *http.Request) {
27
+		advertising := &peering.AdvertisingRequest{}
28
+
29
+		decoder := json.NewDecoder(r.Body)
30
+		if err := decoder.Decode(advertising); err != nil {
31
+			logger.Printf("[ERROR] %s", err)
32
+			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
33
+			return
34
+		}
35
+
36
+		if !options.PeerIDValidator(advertising.ID) {
37
+			logger.Printf("[ERROR] %s", ErrInvalidAdvertisingRequest)
38
+			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
39
+			return
40
+		}
41
+
42
+		if _, err := crypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil {
43
+			logger.Printf("[ERROR] %s", err)
44
+			options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
45
+			return
46
+		}
47
+
48
+		peer, err := store.Get(advertising.ID)
49
+
50
+		if err == nil {
51
+			logger.Printf("[ERROR] %s", ErrPeerIDAlreadyInUse)
52
+			options.ErrorHandler(w, r, ErrPeerIDAlreadyInUse)
53
+			return
54
+		}
55
+
56
+		if err != peering.ErrPeerNotFound {
57
+			logger.Printf("[ERROR] %s", err)
58
+			options.ErrorHandler(w, r, err)
59
+			return
60
+		}
61
+
62
+		attrs := filterAttributes(options.PeerAttributes, advertising.Attributes)
63
+
64
+		peer, err = store.Create(advertising.ID, attrs)
65
+		if err != nil {
66
+			logger.Printf("[ERROR] %s", err)
67
+			options.ErrorHandler(w, r, err)
68
+			return
69
+		}
70
+
71
+		if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
72
+			logger.Printf("[ERROR] %s", err)
73
+			options.ErrorHandler(w, r, err)
74
+			return
75
+		}
76
+
77
+		if err := store.UpdatePublicKey(peer.ID, advertising.PublicKey); err != nil {
78
+			logger.Printf("[ERROR] %s", err)
79
+			options.ErrorHandler(w, r, err)
80
+			return
81
+		}
82
+
83
+		w.WriteHeader(http.StatusCreated)
84
+
85
+	}
86
+
87
+	return handler
88
+}
89
+
90
+func UpdateHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
91
+	options := createOptions(funcs...)
92
+	logger := options.Logger
93
+
94
+	handler := func(w http.ResponseWriter, r *http.Request) {
95
+
96
+		update := &peering.UpdateRequest{}
97
+		decoder := json.NewDecoder(r.Body)
98
+		if err := decoder.Decode(update); err != nil {
99
+			options.ErrorHandler(w, r, ErrInvalidUpdateRequest)
100
+			return
101
+		}
102
+
103
+		peerID, err := GetPeerID(r)
104
+		if err != nil {
105
+			logger.Printf("[ERROR] %s", err)
106
+			options.ErrorHandler(w, r, err)
107
+			return
108
+		}
109
+
110
+		peer, err := store.Get(peerID)
111
+		if err != nil {
112
+			logger.Printf("[ERROR] %s", err)
113
+			options.ErrorHandler(w, r, err)
114
+			return
115
+		}
116
+
117
+		if peer == nil {
118
+			logger.Printf("[ERROR] %s", ErrUnauthorized)
119
+			options.ErrorHandler(w, r, ErrUnauthorized)
120
+			return
121
+		}
122
+
123
+		if peer.Status == peering.StatusRejected {
124
+			logger.Printf("[ERROR] %s", ErrPeerRejected)
125
+			options.ErrorHandler(w, r, ErrPeerRejected)
126
+			return
127
+		}
128
+
129
+		if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
130
+			logger.Printf("[ERROR] %s", err)
131
+			options.ErrorHandler(w, r, err)
132
+			return
133
+		}
134
+
135
+		attrs := filterAttributes(options.PeerAttributes, update.Attributes)
136
+		if err := store.UpdateAttributes(peer.ID, attrs); err != nil {
137
+			logger.Printf("[ERROR] %s", err)
138
+			options.ErrorHandler(w, r, err)
139
+			return
140
+		}
141
+
142
+		w.WriteHeader(http.StatusNoContent)
143
+
144
+	}
145
+
146
+	return handler
147
+}
148
+
149
+func PingHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
150
+	options := createOptions(funcs...)
151
+	logger := options.Logger
152
+
153
+	handler := func(w http.ResponseWriter, r *http.Request) {
154
+
155
+		update := &peering.UpdateRequest{}
156
+		decoder := json.NewDecoder(r.Body)
157
+		if err := decoder.Decode(update); err != nil {
158
+			options.ErrorHandler(w, r, ErrInvalidUpdateRequest)
159
+			return
160
+		}
161
+
162
+		peerID, err := GetPeerID(r)
163
+		if err != nil {
164
+			logger.Printf("[ERROR] %s", err)
165
+			options.ErrorHandler(w, r, err)
166
+			return
167
+		}
168
+
169
+		peer, err := store.Get(peerID)
170
+		if err != nil {
171
+			logger.Printf("[ERROR] %s", err)
172
+			options.ErrorHandler(w, r, err)
173
+			return
174
+		}
175
+
176
+		if peer == nil {
177
+			logger.Printf("[ERROR] %s", ErrUnauthorized)
178
+			options.ErrorHandler(w, r, ErrUnauthorized)
179
+			return
180
+		}
181
+
182
+		if peer.Status == peering.StatusRejected {
183
+			logger.Printf("[ERROR] %s", ErrPeerRejected)
184
+			options.ErrorHandler(w, r, ErrPeerRejected)
185
+			return
186
+		}
187
+
188
+		if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
189
+			logger.Printf("[ERROR] %s", err)
190
+			options.ErrorHandler(w, r, err)
191
+			return
192
+		}
193
+
194
+		w.WriteHeader(http.StatusNoContent)
195
+	}
196
+
197
+	return handler
198
+}
199
+
200
+func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
201
+	switch err {
202
+	case ErrInvalidAdvertisingRequest:
203
+		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
204
+	case ErrPeerIDAlreadyInUse:
205
+		http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
206
+	case ErrUnauthorized:
207
+		http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
208
+	case ErrPeerRejected:
209
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
210
+	default:
211
+		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
212
+	}
213
+}
214
+
215
+func DefaultPeerIDValidator(id peering.PeerID) bool {
216
+	return string(id) != ""
217
+}
218
+
219
+func filterAttributes(filters []string, attrs peering.PeerAttributes) peering.PeerAttributes {
220
+	filtered := peering.PeerAttributes{}
221
+	for _, key := range filters {
222
+		if _, exists := attrs[key]; exists {
223
+			filtered[key] = attrs[key]
224
+		}
225
+	}
226
+	return filtered
227
+}

+ 157
- 0
server/middleware.go View File

@@ -0,0 +1,157 @@
1
+package server
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"crypto/sha256"
7
+	"errors"
8
+	"io/ioutil"
9
+	"strings"
10
+	"time"
11
+
12
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
13
+
14
+	peering "forge.cadoles.com/wpetit/go-http-peering"
15
+	jwt "github.com/dgrijalva/jwt-go"
16
+
17
+	"net/http"
18
+)
19
+
20
+const (
21
+	AuthorizationType            = "Bearer"
22
+	KeyPeerID         ContextKey = "peerID"
23
+)
24
+
25
+var (
26
+	ErrInvalidClaims   = errors.New("invalid claims")
27
+	ErrInvalidChecksum = errors.New("invalid checksum")
28
+	ErrNotPeered       = errors.New("not peered")
29
+)
30
+
31
+type ContextKey string
32
+
33
+func Authenticate(store peering.Store, funcs ...OptionFunc) func(http.Handler) http.Handler {
34
+	options := createOptions(funcs...)
35
+	logger := options.Logger
36
+
37
+	middleware := func(next http.Handler) http.Handler {
38
+		fn := func(w http.ResponseWriter, r *http.Request) {
39
+			authorization := r.Header.Get("Authorization")
40
+
41
+			if authorization == "" {
42
+				sendError(w, http.StatusUnauthorized)
43
+				return
44
+			}
45
+
46
+			parts := strings.SplitN(authorization, " ", 2)
47
+
48
+			if len(parts) != 2 || parts[0] != AuthorizationType {
49
+				sendError(w, http.StatusUnauthorized)
50
+				return
51
+			}
52
+
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 {
76
+				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
+				sendError(w, http.StatusUnauthorized)
89
+				return
90
+			}
91
+
92
+			body, err := ioutil.ReadAll(r.Body)
93
+			if err != nil {
94
+				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)
102
+				return
103
+			}
104
+
105
+			match, err := compareChecksum(body, claims.BodySum)
106
+			if err != nil {
107
+				logger.Printf("[ERROR] %s", err)
108
+				sendError(w, http.StatusUnauthorized)
109
+				return
110
+			}
111
+
112
+			if !match {
113
+				logger.Printf("[ERROR] %s", ErrInvalidChecksum)
114
+				sendError(w, http.StatusBadRequest)
115
+				return
116
+			}
117
+
118
+			peerID := peering.PeerID(claims.Issuer)
119
+
120
+			if err := store.UpdateLastContact(peerID, r.RemoteAddr, time.Now()); err != nil {
121
+				logger.Printf("[ERROR] %s", err)
122
+				sendError(w, http.StatusInternalServerError)
123
+				return
124
+			}
125
+
126
+			ctx := context.WithValue(r.Context(), KeyPeerID, peerID)
127
+			r = r.WithContext(ctx)
128
+			r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
129
+
130
+			next.ServeHTTP(w, r)
131
+
132
+		}
133
+		return http.HandlerFunc(fn)
134
+	}
135
+	return middleware
136
+}
137
+
138
+func GetPeerID(r *http.Request) (peering.PeerID, error) {
139
+	peerID, ok := r.Context().Value(KeyPeerID).(peering.PeerID)
140
+	if !ok {
141
+		return "", ErrUnauthorized
142
+	}
143
+	return peerID, nil
144
+}
145
+
146
+func sendError(w http.ResponseWriter, status int) {
147
+	http.Error(w, http.StatusText(status), status)
148
+}
149
+
150
+func compareChecksum(body []byte, sum []byte) (bool, error) {
151
+	sha := sha256.New()
152
+	_, err := sha.Write(body)
153
+	if err != nil {
154
+		return false, err
155
+	}
156
+	return bytes.Equal(sum, sha.Sum(nil)), nil
157
+}

+ 60
- 0
server/option.go View File

@@ -0,0 +1,60 @@
1
+package server
2
+
3
+import (
4
+	"log"
5
+	"net/http"
6
+	"os"
7
+
8
+	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+)
10
+
11
+type Logger interface {
12
+	Printf(string, ...interface{})
13
+}
14
+
15
+type Options struct {
16
+	PeerAttributes  []string
17
+	ErrorHandler    ErrorHandler
18
+	PeerIDValidator func(peering.PeerID) bool
19
+	Logger          Logger
20
+}
21
+
22
+type OptionFunc func(*Options)
23
+
24
+type ErrorHandler func(http.ResponseWriter, *http.Request, error)
25
+
26
+func WithPeerAttributes(attrs ...string) OptionFunc {
27
+	return func(options *Options) {
28
+		options.PeerAttributes = attrs
29
+	}
30
+}
31
+
32
+func WithLogger(logger Logger) OptionFunc {
33
+	return func(options *Options) {
34
+		options.Logger = logger
35
+	}
36
+}
37
+
38
+func WithErrorHandler(handler ErrorHandler) OptionFunc {
39
+	return func(options *Options) {
40
+		options.ErrorHandler = handler
41
+	}
42
+}
43
+
44
+func defaultOptions() *Options {
45
+	logger := log.New(os.Stdout, "[go-http-peering] ", log.LstdFlags|log.Lshortfile)
46
+	return &Options{
47
+		PeerAttributes:  []string{"Label"},
48
+		ErrorHandler:    DefaultErrorHandler,
49
+		PeerIDValidator: DefaultPeerIDValidator,
50
+		Logger:          logger,
51
+	}
52
+}
53
+
54
+func createOptions(funcs ...OptionFunc) *Options {
55
+	options := defaultOptions()
56
+	for _, fn := range funcs {
57
+		fn(options)
58
+	}
59
+	return options
60
+}

+ 14
- 0
server/util_test.go View File

@@ -0,0 +1,14 @@
1
+package server
2
+
3
+import (
4
+	"crypto/rand"
5
+	"crypto/rsa"
6
+)
7
+
8
+func mustGeneratePrivateKey() *rsa.PrivateKey {
9
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
10
+	if err != nil {
11
+		panic(err)
12
+	}
13
+	return privateKey
14
+}

+ 26
- 0
store.go View File

@@ -0,0 +1,26 @@
1
+package peering
2
+
3
+import (
4
+	"errors"
5
+	"time"
6
+)
7
+
8
+var (
9
+	ErrPeerNotFound = errors.New("peer not found")
10
+	ErrPeerExists   = errors.New("peer exists")
11
+)
12
+
13
+type Store interface {
14
+	Create(id PeerID, attrs PeerAttributes) (*Peer, error)
15
+	Get(id PeerID) (*Peer, error)
16
+	Delete(id PeerID) error
17
+	List() ([]PeerHeader, error)
18
+
19
+	UpdatePublicKey(id PeerID, publicKey []byte) error
20
+	UpdateAttributes(id PeerID, attrs PeerAttributes) error
21
+	UpdateLastContact(id PeerID, remoteAddress string, ts time.Time) error
22
+
23
+	Accept(id PeerID) error
24
+	Forget(id PeerID) error
25
+	Reject(id PeerID) error
26
+}

+ 56
- 0
test/advertise_test.go View File

@@ -0,0 +1,56 @@
1
+package test
2
+
3
+import (
4
+	"reflect"
5
+	"testing"
6
+	"time"
7
+
8
+	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
10
+)
11
+
12
+func TestAdvertise(t *testing.T) {
13
+
14
+	if t.Skipped() {
15
+		t.SkipNow()
16
+	}
17
+
18
+	id, pk, client, store := setup(t)
19
+
20
+	attrs := peering.PeerAttributes{}
21
+	if err := client.Advertise(attrs); err != nil {
22
+		t.Fatal(err)
23
+	}
24
+
25
+	peer, err := store.Get(id)
26
+	if err != nil {
27
+		t.Error(err)
28
+	}
29
+
30
+	if g, e := peer.ID, id; g != e {
31
+		t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
32
+	}
33
+
34
+	if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) {
35
+		t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e)
36
+	}
37
+
38
+	var defaultTime time.Time
39
+	if peer.LastContact == defaultTime {
40
+		t.Error("peer.LastContact should not be time.Time zero value")
41
+	}
42
+
43
+	if peer.LastAddress == "" {
44
+		t.Error("peer.LastAddress should not be empty")
45
+	}
46
+
47
+	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
48
+	if err != nil {
49
+		t.Fatal(err)
50
+	}
51
+
52
+	if g, e := peer.PublicKey, pem; !reflect.DeepEqual(g, e) {
53
+		t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
54
+	}
55
+
56
+}

+ 46
- 0
test/ping_test.go View File

@@ -0,0 +1,46 @@
1
+package test
2
+
3
+import (
4
+	"testing"
5
+
6
+	peering "forge.cadoles.com/wpetit/go-http-peering"
7
+)
8
+
9
+func TestPing(t *testing.T) {
10
+
11
+	if t.Skipped() {
12
+		t.SkipNow()
13
+	}
14
+
15
+	id, _, client, store := setup(t)
16
+
17
+	attrs := peering.PeerAttributes{}
18
+	if err := client.Advertise(attrs); err != nil {
19
+		t.Fatal(err)
20
+	}
21
+
22
+	peer, err := store.Get(id)
23
+	if err != nil {
24
+		t.Fatal(err)
25
+	}
26
+
27
+	lastContact := peer.LastContact
28
+
29
+	if err := store.Accept(id); err != nil {
30
+		t.Error(err)
31
+	}
32
+
33
+	if err := client.Ping(); err != nil {
34
+		t.Fatal(err)
35
+	}
36
+
37
+	peer, err = store.Get(id)
38
+	if err != nil {
39
+		t.Fatal(err)
40
+	}
41
+
42
+	if peer.LastContact == lastContact {
43
+		t.Error("peer.LastContact should have been updated")
44
+	}
45
+
46
+}

+ 62
- 0
test/update_test.go View File

@@ -0,0 +1,62 @@
1
+package test
2
+
3
+import (
4
+	"reflect"
5
+	"testing"
6
+	"time"
7
+
8
+	peering "forge.cadoles.com/wpetit/go-http-peering"
9
+	"forge.cadoles.com/wpetit/go-http-peering/crypto"
10
+)
11
+
12
+func TestUpdate(t *testing.T) {
13
+
14
+	if t.Skipped() {
15
+		t.SkipNow()
16
+	}
17
+
18
+	id, pk, client, store := setup(t)
19
+
20
+	attrs := peering.PeerAttributes{}
21
+
22
+	if err := client.Advertise(attrs); err != nil {
23
+		t.Fatal(err)
24
+	}
25
+
26
+	if err := store.Accept(id); err != nil {
27
+		t.Error(err)
28
+	}
29
+
30
+	attrs["Label"] = "Foo Bar"
31
+	if err := client.UpdateAttributes(attrs); err != nil {
32
+		t.Fatal(err)
33
+	}
34
+
35
+	peer, err := store.Get(id)
36
+	if err != nil {
37
+		t.Fatal(err)
38
+	}
39
+
40
+	if g, e := peer.ID, id; g != e {
41
+		t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
42
+	}
43
+
44
+	if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) {
45
+		t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e)
46
+	}
47
+
48
+	var defaultTime time.Time
49
+	if peer.LastContact == defaultTime {
50
+		t.Error("peer.LastContact should not be time.Time zero value")
51
+	}
52
+
53
+	pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
54
+	if err != nil {
55
+		t.Fatal(err)
56
+	}
57
+
58
+	if g, e := peer.PublicKey, pem; !reflect.DeepEqual(g, e) {
59
+		t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
60
+	}
61
+
62
+}

+ 64
- 0
test/util_test.go View File

@@ -0,0 +1,64 @@
1
+package test
2
+
3
+import (
4
+	"crypto/rand"
5
+	"crypto/rsa"
6
+	"fmt"
7
+	"net"
8
+	"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"
15
+)
16
+
17
+func mustGeneratePrivateKey() *rsa.PrivateKey {
18
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
19
+	if err != nil {
20
+		panic(err)
21
+	}
22
+	return privateKey
23
+}
24
+
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
34
+}
35
+
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
44
+}
45
+
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
+	)
62
+
63
+	return id, pk, c, store
64
+}

Loading…
Cancel
Save