go-http-peering/client/client.go

211 lines
4.6 KiB
Go

package client
import (
"bytes"
"crypto/rsa"
"crypto/sha256"
"encoding/json"
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
peering "forge.cadoles.com/Cadoles/go-http-peering"
"forge.cadoles.com/Cadoles/go-http-peering/crypto"
"forge.cadoles.com/Cadoles/go-http-peering/server"
)
var (
ErrInvalidPrivateKey = errors.New("invalid private key")
ErrUnexpectedResponse = errors.New("unexpected response")
ErrUnauthorized = errors.New("unauthorized")
ErrRejected = errors.New("rejected")
ErrInvalidServerToken = errors.New("invalid server token")
)
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{
Attributes: attrs,
PublicKey: publicKey,
}
res, err := c.Post(url, data)
if err != nil {
return err
}
switch res.StatusCode {
case http.StatusCreated:
return nil
case http.StatusConflict:
return peering.ErrPeerExists
default:
return errors.Wrapf(ErrUnexpectedResponse, "unexpected http status code '%d'", res.StatusCode)
}
}
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 errors.Wrapf(ErrUnexpectedResponse, "unexpected http status code '%d'", res.StatusCode)
}
}
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 errors.Wrapf(ErrUnexpectedResponse, "unexpected http status code '%d'", res.StatusCode)
}
}
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.addClientToken(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.addClientToken(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) addServerToken(r *http.Request) {
r.Header.Set(
server.ServerTokenHeader,
c.options.ServerToken,
)
}
func (c *Client) addClientToken(r *http.Request, body []byte) error {
bodySum, err := c.createBodySum(body)
if err != nil {
return err
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.ClientTokenClaims{
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Add(time.Minute * -1).Unix(),
ExpiresAt: time.Now().Add(time.Minute * 5).Unix(),
},
BodySum: bodySum,
})
tokenStr, err := token.SignedString(c.options.PrivateKey)
if err != nil {
return err
}
r.Header.Set(server.ClientTokenHeader, tokenStr)
c.addServerToken(r)
return nil
}
func (c *Client) createBodySum(body []byte) ([]byte, error) {
if body == nil || len(body) == 0 {
body = []byte("")
}
sha := sha256.New()
_, err := sha.Write(body)
if err != nil {
return nil, err
}
return sha.Sum(nil), nil
}
func (c *Client) PeerID(serverPublicKey *rsa.PublicKey) (peering.PeerID, error) {
token, err := jwt.ParseWithClaims(
c.options.ServerToken,
&peering.ServerTokenClaims{},
func(token *jwt.Token) (interface{}, error) {
return serverPublicKey, nil
},
)
if err != nil {
return "", err
}
if !token.Valid {
return "", ErrInvalidServerToken
}
serverClaims, ok := token.Claims.(*peering.ServerTokenClaims)
if !ok {
return "", ErrInvalidServerToken
}
return serverClaims.PeerID, nil
}
func New(funcs ...OptionFunc) *Client {
options := createOptions(funcs...)
return &Client{options}
}