package client import ( "bytes" "crypto/rsa" "crypto/sha256" "encoding/json" "errors" "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") 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 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.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().Unix(), 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(server.ClientTokenHeader, tokenStr) c.addServerToken(r) 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 (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} }