Initial commit
This commit is contained in:
commit
2a8a20195a
|
@ -0,0 +1,2 @@
|
||||||
|
/coverage
|
||||||
|
/vendor
|
|
@ -0,0 +1,30 @@
|
||||||
|
test:
|
||||||
|
go clean -testcache
|
||||||
|
go test -cover -v ./...
|
||||||
|
|
||||||
|
watch:
|
||||||
|
modd
|
||||||
|
|
||||||
|
deps:
|
||||||
|
GO111MODULE=off go get -u golang.org/x/tools/cmd/godoc
|
||||||
|
GO111MODULE=off go get -u github.com/cortesi/modd/cmd/modd
|
||||||
|
GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||||
|
GO111MODULE=off go get -u github.com/lmika/goseq
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run --tests=false --enable-all
|
||||||
|
|
||||||
|
sequence-diagram: sd-advertise sd-update sd-ping
|
||||||
|
|
||||||
|
sd-%:
|
||||||
|
goseq doc/sequence-diagram/$*.seq > doc/sequence-diagram/$*.svg
|
||||||
|
|
||||||
|
doc:
|
||||||
|
@echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/wpetit/go-http-peering to see the documentation"
|
||||||
|
godoc -http=:6060
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: test lint doc sequence-diagram
|
|
@ -0,0 +1,25 @@
|
||||||
|
# go-http-peering
|
||||||
|
|
||||||
|
Librairie implémentant un protocole d'authentification par "appairage" d'un serveur et client HTTP basé sur [JWT](https://jwt.io/).
|
||||||
|
|
||||||
|
[Documentation](https://godoc.org/forge.cadoles.com/wpetit/go-http-peering)
|
||||||
|
|
||||||
|
## Séquences
|
||||||
|
|
||||||
|
### Annonce du client
|
||||||
|
|
||||||
|
![](./doc/sequence-diagram/advertise.svg)
|
||||||
|
|
||||||
|
### Mise à jour des attributs
|
||||||
|
|
||||||
|
![](./doc/sequence-diagram/update.svg)
|
||||||
|
|
||||||
|
### Ping
|
||||||
|
|
||||||
|
![](./doc/sequence-diagram/ping.svg)
|
||||||
|
|
||||||
|
**Statut** Instable
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
AGPL-3.0
|
|
@ -0,0 +1,16 @@
|
||||||
|
package chi
|
||||||
|
|
||||||
|
import (
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/server"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Mount(r chi.Router, store peering.Store, funcs ...server.OptionFunc) {
|
||||||
|
r.Post(peering.AdvertisePath, server.AdvertiseHandler(store, funcs...))
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(server.Authenticate(store, funcs...))
|
||||||
|
r.Post(peering.UpdatePath, server.UpdateHandler(store, funcs...))
|
||||||
|
r.Post(peering.PingPath, server.PingHandler(store, funcs...))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package chi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/memory"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMount(t *testing.T) {
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
store := memory.NewStore()
|
||||||
|
|
||||||
|
Mount(r, store)
|
||||||
|
|
||||||
|
routes := r.Routes()
|
||||||
|
|
||||||
|
if g, e := len(routes), 3; g != e {
|
||||||
|
t.Errorf("len(r.Routes()): got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := routes[0].Pattern, peering.AdvertisePath; g != e {
|
||||||
|
t.Errorf("routes[0].Pattern: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := routes[1].Pattern, peering.PingPath; g != e {
|
||||||
|
t.Errorf("routes[1].Pattern: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := routes[2].Pattern, peering.UpdatePath; g != e {
|
||||||
|
t.Errorf("routes[2].Pattern: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultBody = "{}"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||||
|
ErrUnexpectedResponse = errors.New("unexpected response")
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
ErrRejected = errors.New("rejected")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
options *Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Advertise(attrs peering.PeerAttributes) error {
|
||||||
|
url := c.options.BaseURL + peering.AdvertisePath
|
||||||
|
|
||||||
|
publicKey, err := crypto.EncodePublicKeyToPEM(c.options.PrivateKey.Public())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &peering.AdvertisingRequest{
|
||||||
|
ID: c.options.PeerID,
|
||||||
|
Attributes: attrs,
|
||||||
|
PublicKey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _, err := c.createPostRequest(url, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.options.HTTPClient.Do(req)
|
||||||
|
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusCreated:
|
||||||
|
return nil
|
||||||
|
case http.StatusConflict:
|
||||||
|
return peering.ErrPeerExists
|
||||||
|
default:
|
||||||
|
return ErrUnexpectedResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateAttributes(attrs peering.PeerAttributes) error {
|
||||||
|
url := c.options.BaseURL + peering.UpdatePath
|
||||||
|
|
||||||
|
data := &peering.UpdateRequest{
|
||||||
|
Attributes: attrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Post(url, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusNoContent:
|
||||||
|
return nil
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return ErrUnauthorized
|
||||||
|
case http.StatusForbidden:
|
||||||
|
return ErrRejected
|
||||||
|
default:
|
||||||
|
return ErrUnexpectedResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Ping() error {
|
||||||
|
url := c.options.BaseURL + peering.PingPath
|
||||||
|
|
||||||
|
res, err := c.Post(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusNoContent:
|
||||||
|
return nil
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return ErrUnauthorized
|
||||||
|
case http.StatusForbidden:
|
||||||
|
return ErrRejected
|
||||||
|
default:
|
||||||
|
return ErrUnexpectedResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Get(url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.signRequest(req, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.options.HTTPClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Post(url string, data interface{}) (*http.Response, error) {
|
||||||
|
req, body, err := c.createPostRequest(url, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.signRequest(req, body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.options.HTTPClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) createPostRequest(url string, data interface{}) (*http.Request, []byte, error) {
|
||||||
|
body, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return req, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) signRequest(r *http.Request, body []byte) error {
|
||||||
|
bodySum, err := c.createBodySum(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.PeerClaims{
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
NotBefore: time.Now().Unix(),
|
||||||
|
Issuer: string(c.options.PeerID),
|
||||||
|
ExpiresAt: time.Now().Add(time.Minute * 10).Unix(),
|
||||||
|
},
|
||||||
|
BodySum: bodySum,
|
||||||
|
})
|
||||||
|
|
||||||
|
tokenStr, err := token.SignedString(c.options.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("%s %s", server.AuthorizationType, tokenStr))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) createBodySum(body []byte) ([]byte, error) {
|
||||||
|
if body == nil || len(body) == 0 {
|
||||||
|
body = []byte(DefaultBody)
|
||||||
|
}
|
||||||
|
sha := sha256.New()
|
||||||
|
_, err := sha.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sha.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(funcs ...OptionFunc) *Client {
|
||||||
|
options := createOptions(funcs...)
|
||||||
|
return &Client{options}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
PeerID peering.PeerID
|
||||||
|
HTTPClient *http.Client
|
||||||
|
BaseURL string
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Options)
|
||||||
|
|
||||||
|
func WithPeerID(id peering.PeerID) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.PeerID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPrivateKey(pk *rsa.PrivateKey) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.PrivateKey = pk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHTTPClient(client *http.Client) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.HTTPClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBaseURL(url string) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.BaseURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOptions() *Options {
|
||||||
|
return &Options{
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
PeerID: peering.NewPeerID(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOptions(funcs ...OptionFunc) *Options {
|
||||||
|
options := defaultOptions()
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncodePublicKeyToPEM(key interface{}) ([]byte, error) {
|
||||||
|
pub, err := x509.MarshalPKIXPublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pub,
|
||||||
|
})
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodePEMToPublicKey(pem []byte) (*rsa.PublicKey, error) {
|
||||||
|
return jwt.ParseRSAPublicKeyFromPEM(pem)
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
Client -> Server: POST /advertise\n\n{"ID": <PEER_ID>, "Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> }
|
||||||
|
Server -> Client: 201 Created
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- Generated by SVGo -->
|
||||||
|
<svg width="711" height="196"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DejaVuSans';
|
||||||
|
src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<line x1="45" y1="24" x2="45" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<rect x="8" y="156" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<line x1="661" y1="24" x2="661" y2="172" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="619" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="635" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="619" y="156" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="635" y="177" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="61" y="56" width="584" height="46" style="fill:white;stroke:white;" />
|
||||||
|
<text x="298" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /advertise</text>
|
||||||
|
<text x="61" y="100" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{"ID": <PEER_ID>, "Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> }</text>
|
||||||
|
<line x1="45" y1="106" x2="661" y2="106" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="652,101 661,106 652,111" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="310" y="122" width="87" height="14" style="fill:white;stroke:white;" />
|
||||||
|
<text x="310" y="134" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >201 Created</text>
|
||||||
|
<line x1="661" y1="140" x2="45" y2="140" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="54,135 45,140 54,145" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
Client -> Server: POST /ping\nAuthorization: Bearer <JWT_SIGNING_TOKEN>
|
||||||
|
Server -> Client: 204 No Content
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- Generated by SVGo -->
|
||||||
|
<svg width="446" height="180"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DejaVuSans';
|
||||||
|
src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<line x1="45" y1="24" x2="45" y2="156" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<rect x="8" y="140" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="161" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<line x1="396" y1="24" x2="396" y2="156" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="354" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="370" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="354" y="140" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="370" y="161" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="61" y="56" width="319" height="30" style="fill:white;stroke:white;" />
|
||||||
|
<text x="181" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /ping</text>
|
||||||
|
<text x="61" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >Authorization: Bearer <JWT_SIGNING_TOKEN></text>
|
||||||
|
<line x1="45" y1="90" x2="396" y2="90" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="387,85 396,90 387,95" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="166" y="106" width="111" height="14" style="fill:white;stroke:white;" />
|
||||||
|
<text x="166" y="118" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
|
||||||
|
<line x1="396" y1="124" x2="45" y2="124" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="54,119 45,124 54,129" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
Client -> Server: POST /update\nAuthorization: Bearer <JWT_SIGNING_TOKEN>\n\n{"Attributes": <PEER_ATTRIBUTES>}
|
||||||
|
Server -> Client: 204 No Content
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- Generated by SVGo -->
|
||||||
|
<svg width="446" height="212"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DejaVuSans';
|
||||||
|
src: url('https://fontlibrary.org/assets/fonts/dejavu-sans/f5ec8426554a3a67ebcdd39f9c3fee83/49c0f03ec2fa354df7002bcb6331e106/DejaVuSansBook.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<line x1="45" y1="24" x2="45" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="8" y="8" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<rect x="8" y="172" width="75" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="24" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Client</text>
|
||||||
|
<line x1="396" y1="24" x2="396" y2="188" style="stroke-dasharray:8,8;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="354" y="8" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="370" y="29" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="354" y="172" width="84" height="32" style="fill:white;stroke-width:2px;stroke:black;" />
|
||||||
|
<text x="370" y="193" style="fill:black;font-family:DejaVuSans,sans-serif;font-size:16px;" >Server</text>
|
||||||
|
<rect x="61" y="56" width="319" height="62" style="fill:white;stroke:white;" />
|
||||||
|
<text x="172" y="68" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >POST /update</text>
|
||||||
|
<text x="61" y="84" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >Authorization: Bearer <JWT_SIGNING_TOKEN></text>
|
||||||
|
<text x="91" y="116" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >{"Attributes": <PEER_ATTRIBUTES>}</text>
|
||||||
|
<line x1="45" y1="122" x2="396" y2="122" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="387,117 396,122 387,127" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
<rect x="166" y="138" width="111" height="14" style="fill:white;stroke:white;" />
|
||||||
|
<text x="166" y="150" style="font-family:DejaVuSans,sans-serif;font-size:14px;" >204 No Content</text>
|
||||||
|
<line x1="396" y1="156" x2="45" y2="156" style="stroke:black;stroke-width:2px;" />
|
||||||
|
<polyline points="54,151 45,156 54,161" style="fill:black;stroke-width:2px;stroke:black;" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
module forge.cadoles.com/wpetit/go-http-peering
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/go-chi/chi v4.0.1+incompatible
|
||||||
|
github.com/pborman/uuid v1.2.0
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/go-chi/chi v4.0.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA=
|
||||||
|
github.com/go-chi/chi v4.0.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||||
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||||
|
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
|
@ -0,0 +1,153 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
peers map[peering.PeerID]*peering.Peer
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Create(id peering.PeerID, attrs peering.PeerAttributes) (*peering.Peer, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.peers[id]; exists {
|
||||||
|
return nil, peering.ErrPeerExists
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := &peering.Peer{
|
||||||
|
PeerHeader: peering.PeerHeader{
|
||||||
|
ID: id,
|
||||||
|
Status: peering.StatusPending,
|
||||||
|
},
|
||||||
|
Attributes: attrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.peers[id] = peer
|
||||||
|
|
||||||
|
return copy(peer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(id peering.PeerID) (*peering.Peer, error) {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
|
||||||
|
peer, exists := s.peers[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(peer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) List() ([]peering.PeerHeader, error) {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
|
||||||
|
headers := make([]peering.PeerHeader, len(s.peers))
|
||||||
|
i := 0
|
||||||
|
for _, p := range s.peers {
|
||||||
|
headers[i] = p.PeerHeader
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateAttributes(id peering.PeerID, attrs peering.PeerAttributes) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
peer, exists := s.peers[id]
|
||||||
|
if !exists {
|
||||||
|
return peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Attributes = attrs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateLastContact(id peering.PeerID, remoteAddress string, ts time.Time) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
peer, exists := s.peers[id]
|
||||||
|
if !exists {
|
||||||
|
return peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.LastAddress = remoteAddress
|
||||||
|
peer.LastContact = ts
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdatePublicKey(id peering.PeerID, publicKey []byte) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
peer, exists := s.peers[id]
|
||||||
|
if !exists {
|
||||||
|
return peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.PublicKey = publicKey
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Delete(id peering.PeerID) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.peers[id]; !exists {
|
||||||
|
return peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.peers, id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Accept(id peering.PeerID) error {
|
||||||
|
return s.updateStatus(id, peering.StatusPeered)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Forget(id peering.PeerID) error {
|
||||||
|
return s.updateStatus(id, peering.StatusPending)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Reject(id peering.PeerID) error {
|
||||||
|
return s.updateStatus(id, peering.StatusRejected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) updateStatus(id peering.PeerID, status peering.PeerStatus) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
peer, exists := s.peers[id]
|
||||||
|
if !exists {
|
||||||
|
return peering.ErrPeerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Status = status
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore() *Store {
|
||||||
|
return &Store{
|
||||||
|
peers: make(map[peering.PeerID]*peering.Peer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy(p *peering.Peer) *peering.Peer {
|
||||||
|
copy := *p
|
||||||
|
return ©
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
**/*.go
|
||||||
|
!vendor/**.go {
|
||||||
|
prep: make test
|
||||||
|
}
|
||||||
|
|
||||||
|
doc/sequence-diagram/*.seq {
|
||||||
|
prep: make sequence-diagram
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package peering
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerID string
|
||||||
|
|
||||||
|
func NewPeerID() PeerID {
|
||||||
|
id := uuid.New()
|
||||||
|
return PeerID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusPending PeerStatus = iota
|
||||||
|
StatusPeered
|
||||||
|
StatusRejected
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerHeader struct {
|
||||||
|
ID PeerID
|
||||||
|
Status PeerStatus
|
||||||
|
LastAddress string
|
||||||
|
LastContact time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerAttributes map[string]interface{}
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
PeerHeader
|
||||||
|
Attributes PeerAttributes
|
||||||
|
PublicKey []byte
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package peering
|
||||||
|
|
||||||
|
import jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
|
const (
|
||||||
|
AdvertisePath = "/advertise"
|
||||||
|
UpdatePath = "/update"
|
||||||
|
PingPath = "/ping"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdvertisingRequest struct {
|
||||||
|
ID PeerID
|
||||||
|
Attributes PeerAttributes
|
||||||
|
PublicKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRequest struct {
|
||||||
|
Attributes PeerAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerClaims struct {
|
||||||
|
jwt.StandardClaims
|
||||||
|
BodySum []byte `json:"bodySum"`
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdvertiseHandlerBadRequest(t *testing.T) {
|
||||||
|
store := memory.NewStore()
|
||||||
|
handler := AdvertiseHandler(store)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", peering.AdvertisePath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if g, e := res.StatusCode, http.StatusBadRequest; g != e {
|
||||||
|
t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := store.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := len(peers), 0; g != e {
|
||||||
|
t.Errorf("len(peers): got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdvertiseHandlerInvalidPublicKeyFormat(t *testing.T) {
|
||||||
|
store := memory.NewStore()
|
||||||
|
handler := AdvertiseHandler(store)
|
||||||
|
|
||||||
|
advertising := &peering.AdvertisingRequest{
|
||||||
|
ID: peering.NewPeerID(),
|
||||||
|
PublicKey: []byte("Test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(advertising)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if g, e := res.StatusCode, http.StatusBadRequest; g != e {
|
||||||
|
t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := store.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := len(peers), 0; g != e {
|
||||||
|
t.Errorf("len(peers): got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdvertiseHandlerExistingPeer(t *testing.T) {
|
||||||
|
store := memory.NewStore()
|
||||||
|
handler := AdvertiseHandler(store)
|
||||||
|
|
||||||
|
pk := mustGeneratePrivateKey()
|
||||||
|
pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID := peering.NewPeerID()
|
||||||
|
|
||||||
|
advertising := &peering.AdvertisingRequest{
|
||||||
|
ID: peerID,
|
||||||
|
PublicKey: pem,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(advertising)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
req = httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if g, e := res.StatusCode, http.StatusConflict; g != e {
|
||||||
|
t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdvertiseHandlerValidRequest(t *testing.T) {
|
||||||
|
store := memory.NewStore()
|
||||||
|
handler := AdvertiseHandler(store)
|
||||||
|
|
||||||
|
pk := mustGeneratePrivateKey()
|
||||||
|
pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID := peering.NewPeerID()
|
||||||
|
|
||||||
|
advertising := &peering.AdvertisingRequest{
|
||||||
|
ID: peerID,
|
||||||
|
PublicKey: pem,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(advertising)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if g, e := res.StatusCode, http.StatusCreated; g != e {
|
||||||
|
t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(peerID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.PublicKey, advertising.PublicKey; !bytes.Equal(peer.PublicKey, advertising.PublicKey) {
|
||||||
|
t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidAdvertisingRequest = errors.New("invalid advertising request")
|
||||||
|
ErrInvalidUpdateRequest = errors.New("invalid update request")
|
||||||
|
ErrPeerRejected = errors.New("peer rejected")
|
||||||
|
ErrPeerIDAlreadyInUse = errors.New("peer id already in use")
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
)
|
||||||
|
|
||||||
|
func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
|
||||||
|
|
||||||
|
options := createOptions(funcs...)
|
||||||
|
logger := options.Logger
|
||||||
|
|
||||||
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
advertising := &peering.AdvertisingRequest{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(advertising); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !options.PeerIDValidator(advertising.ID) {
|
||||||
|
logger.Printf("[ERROR] %s", ErrInvalidAdvertisingRequest)
|
||||||
|
options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := crypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(advertising.ID)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
logger.Printf("[ERROR] %s", ErrPeerIDAlreadyInUse)
|
||||||
|
options.ErrorHandler(w, r, ErrPeerIDAlreadyInUse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != peering.ErrPeerNotFound {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := filterAttributes(options.PeerAttributes, advertising.Attributes)
|
||||||
|
|
||||||
|
peer, err = store.Create(advertising.ID, attrs)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.UpdatePublicKey(peer.ID, advertising.PublicKey); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
|
||||||
|
options := createOptions(funcs...)
|
||||||
|
logger := options.Logger
|
||||||
|
|
||||||
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
update := &peering.UpdateRequest{}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(update); err != nil {
|
||||||
|
options.ErrorHandler(w, r, ErrInvalidUpdateRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID, err := GetPeerID(r)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(peerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer == nil {
|
||||||
|
logger.Printf("[ERROR] %s", ErrUnauthorized)
|
||||||
|
options.ErrorHandler(w, r, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Status == peering.StatusRejected {
|
||||||
|
logger.Printf("[ERROR] %s", ErrPeerRejected)
|
||||||
|
options.ErrorHandler(w, r, ErrPeerRejected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := filterAttributes(options.PeerAttributes, update.Attributes)
|
||||||
|
if err := store.UpdateAttributes(peer.ID, attrs); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func PingHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc {
|
||||||
|
options := createOptions(funcs...)
|
||||||
|
logger := options.Logger
|
||||||
|
|
||||||
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
update := &peering.UpdateRequest{}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(update); err != nil {
|
||||||
|
options.ErrorHandler(w, r, ErrInvalidUpdateRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID, err := GetPeerID(r)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(peerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer == nil {
|
||||||
|
logger.Printf("[ERROR] %s", ErrUnauthorized)
|
||||||
|
options.ErrorHandler(w, r, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Status == peering.StatusRejected {
|
||||||
|
logger.Printf("[ERROR] %s", ErrPeerRejected)
|
||||||
|
options.ErrorHandler(w, r, ErrPeerRejected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
options.ErrorHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
switch err {
|
||||||
|
case ErrInvalidAdvertisingRequest:
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
case ErrPeerIDAlreadyInUse:
|
||||||
|
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
||||||
|
case ErrUnauthorized:
|
||||||
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
case ErrPeerRejected:
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultPeerIDValidator(id peering.PeerID) bool {
|
||||||
|
return string(id) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterAttributes(filters []string, attrs peering.PeerAttributes) peering.PeerAttributes {
|
||||||
|
filtered := peering.PeerAttributes{}
|
||||||
|
for _, key := range filters {
|
||||||
|
if _, exists := attrs[key]; exists {
|
||||||
|
filtered[key] = attrs[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthorizationType = "Bearer"
|
||||||
|
KeyPeerID ContextKey = "peerID"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidClaims = errors.New("invalid claims")
|
||||||
|
ErrInvalidChecksum = errors.New("invalid checksum")
|
||||||
|
ErrNotPeered = errors.New("not peered")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
func Authenticate(store peering.Store, funcs ...OptionFunc) func(http.Handler) http.Handler {
|
||||||
|
options := createOptions(funcs...)
|
||||||
|
logger := options.Logger
|
||||||
|
|
||||||
|
middleware := func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authorization := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
if authorization == "" {
|
||||||
|
sendError(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(authorization, " ", 2)
|
||||||
|
|
||||||
|
if len(parts) != 2 || parts[0] != AuthorizationType {
|
||||||
|
sendError(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(parts[1], &peering.PeerClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
claims, ok := token.Claims.(*peering.PeerClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidClaims
|
||||||
|
}
|
||||||
|
peerID := peering.PeerID(claims.Issuer)
|
||||||
|
peer, err := store.Get(peerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if peer.Status == peering.StatusRejected {
|
||||||
|
return nil, ErrPeerRejected
|
||||||
|
}
|
||||||
|
if peer.Status != peering.StatusPeered {
|
||||||
|
return nil, ErrNotPeered
|
||||||
|
}
|
||||||
|
publicKey, err := crypto.DecodePEMToPublicKey(peer.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return publicKey, nil
|
||||||
|
})
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
if err == ErrPeerRejected {
|
||||||
|
sendError(w, http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
sendError(w, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*peering.PeerClaims)
|
||||||
|
if !ok {
|
||||||
|
logger.Printf("[ERROR] %s", ErrInvalidClaims)
|
||||||
|
sendError(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
sendError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Body.Close(); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
sendError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := compareChecksum(body, claims.BodySum)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
sendError(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
logger.Printf("[ERROR] %s", ErrInvalidChecksum)
|
||||||
|
sendError(w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID := peering.PeerID(claims.Issuer)
|
||||||
|
|
||||||
|
if err := store.UpdateLastContact(peerID, r.RemoteAddr, time.Now()); err != nil {
|
||||||
|
logger.Printf("[ERROR] %s", err)
|
||||||
|
sendError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), KeyPeerID, peerID)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
return middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPeerID(r *http.Request) (peering.PeerID, error) {
|
||||||
|
peerID, ok := r.Context().Value(KeyPeerID).(peering.PeerID)
|
||||||
|
if !ok {
|
||||||
|
return "", ErrUnauthorized
|
||||||
|
}
|
||||||
|
return peerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendError(w http.ResponseWriter, status int) {
|
||||||
|
http.Error(w, http.StatusText(status), status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareChecksum(body []byte, sum []byte) (bool, error) {
|
||||||
|
sha := sha256.New()
|
||||||
|
_, err := sha.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return bytes.Equal(sum, sha.Sum(nil)), nil
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Printf(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
PeerAttributes []string
|
||||||
|
ErrorHandler ErrorHandler
|
||||||
|
PeerIDValidator func(peering.PeerID) bool
|
||||||
|
Logger Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Options)
|
||||||
|
|
||||||
|
type ErrorHandler func(http.ResponseWriter, *http.Request, error)
|
||||||
|
|
||||||
|
func WithPeerAttributes(attrs ...string) OptionFunc {
|
||||||
|
return func(options *Options) {
|
||||||
|
options.PeerAttributes = attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(logger Logger) OptionFunc {
|
||||||
|
return func(options *Options) {
|
||||||
|
options.Logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithErrorHandler(handler ErrorHandler) OptionFunc {
|
||||||
|
return func(options *Options) {
|
||||||
|
options.ErrorHandler = handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOptions() *Options {
|
||||||
|
logger := log.New(os.Stdout, "[go-http-peering] ", log.LstdFlags|log.Lshortfile)
|
||||||
|
return &Options{
|
||||||
|
PeerAttributes: []string{"Label"},
|
||||||
|
ErrorHandler: DefaultErrorHandler,
|
||||||
|
PeerIDValidator: DefaultPeerIDValidator,
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOptions(funcs ...OptionFunc) *Options {
|
||||||
|
options := defaultOptions()
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustGeneratePrivateKey() *rsa.PrivateKey {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return privateKey
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package peering
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPeerNotFound = errors.New("peer not found")
|
||||||
|
ErrPeerExists = errors.New("peer exists")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Create(id PeerID, attrs PeerAttributes) (*Peer, error)
|
||||||
|
Get(id PeerID) (*Peer, error)
|
||||||
|
Delete(id PeerID) error
|
||||||
|
List() ([]PeerHeader, error)
|
||||||
|
|
||||||
|
UpdatePublicKey(id PeerID, publicKey []byte) error
|
||||||
|
UpdateAttributes(id PeerID, attrs PeerAttributes) error
|
||||||
|
UpdateLastContact(id PeerID, remoteAddress string, ts time.Time) error
|
||||||
|
|
||||||
|
Accept(id PeerID) error
|
||||||
|
Forget(id PeerID) error
|
||||||
|
Reject(id PeerID) error
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdvertise(t *testing.T) {
|
||||||
|
|
||||||
|
if t.Skipped() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, pk, client, store := setup(t)
|
||||||
|
|
||||||
|
attrs := peering.PeerAttributes{}
|
||||||
|
if err := client.Advertise(attrs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.ID, id; g != e {
|
||||||
|
t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) {
|
||||||
|
t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTime time.Time
|
||||||
|
if peer.LastContact == defaultTime {
|
||||||
|
t.Error("peer.LastContact should not be time.Time zero value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.LastAddress == "" {
|
||||||
|
t.Error("peer.LastAddress should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.PublicKey, pem; !reflect.DeepEqual(g, e) {
|
||||||
|
t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPing(t *testing.T) {
|
||||||
|
|
||||||
|
if t.Skipped() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _, client, store := setup(t)
|
||||||
|
|
||||||
|
attrs := peering.PeerAttributes{}
|
||||||
|
if err := client.Advertise(attrs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastContact := peer.LastContact
|
||||||
|
|
||||||
|
if err := store.Accept(id); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Ping(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err = store.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.LastContact == lastContact {
|
||||||
|
t.Error("peer.LastContact should have been updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
|
||||||
|
if t.Skipped() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, pk, client, store := setup(t)
|
||||||
|
|
||||||
|
attrs := peering.PeerAttributes{}
|
||||||
|
|
||||||
|
if err := client.Advertise(attrs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Accept(id); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs["Label"] = "Foo Bar"
|
||||||
|
if err := client.UpdateAttributes(attrs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := store.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.ID, id; g != e {
|
||||||
|
t.Errorf("peer.ID: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) {
|
||||||
|
t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTime time.Time
|
||||||
|
if peer.LastContact == defaultTime {
|
||||||
|
t.Error("peer.LastContact should not be time.Time zero value")
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := crypto.EncodePublicKeyToPEM(pk.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g, e := peer.PublicKey, pem; !reflect.DeepEqual(g, e) {
|
||||||
|
t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
peering "forge.cadoles.com/wpetit/go-http-peering"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/client"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/memory"
|
||||||
|
"forge.cadoles.com/wpetit/go-http-peering/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustGeneratePrivateKey() *rsa.PrivateKey {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(store peering.Store) (int, error) {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
mux := createServerMux(store)
|
||||||
|
go http.Serve(listener, mux)
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createServerMux(store peering.Store) *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc(peering.AdvertisePath, server.AdvertiseHandler(store))
|
||||||
|
update := server.Authenticate(store)(server.UpdateHandler(store))
|
||||||
|
mux.Handle(peering.UpdatePath, update)
|
||||||
|
ping := server.Authenticate(store)(server.PingHandler(store))
|
||||||
|
mux.Handle(peering.PingPath, ping)
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(t *testing.T) (peering.PeerID, *rsa.PrivateKey, *client.Client, peering.Store) {
|
||||||
|
store := memory.NewStore()
|
||||||
|
|
||||||
|
port, err := startServer(store)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := mustGeneratePrivateKey()
|
||||||
|
id := peering.NewPeerID()
|
||||||
|
|
||||||
|
c := client.New(
|
||||||
|
client.WithBaseURL(fmt.Sprintf("http://127.0.0.1:%d", port)),
|
||||||
|
client.WithPrivateKey(pk),
|
||||||
|
client.WithPeerID(id),
|
||||||
|
)
|
||||||
|
|
||||||
|
return id, pk, c, store
|
||||||
|
}
|
Loading…
Reference in New Issue