Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Synapse client #5

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
19 changes: 14 additions & 5 deletions TODO.org
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ SG-Proto Orgfile

** Unorganized
*** Organize TODOs

*** Cloud (self?) deploy service

*** Better testing of REST resource security.
*** More auth.Login tests
*** Ledger?
Expand Down Expand Up @@ -122,7 +120,6 @@ SG-Proto Orgfile
*** Bad usernames cannot be looked up for expired Sessions
**** This is just a reverse lookup.
**** Can't find example of why this is a bug.
*** If database is closed, can't clean up rivers
*** No auth timeout / river / notifs closure
*** User set type
*** Think about true vs false users in groups -- use slice in API?
Expand Down Expand Up @@ -213,7 +210,8 @@ SG-Proto Orgfile
*** Thoroughly test ws package
*** client package uses custom HTTP client instead of global

* v0.0.2

* v0.0.3
** TODO More auth.Login tests
** TODO Test HandleDeleteToken (URL encoding, etc.)
** TODO Don't check whether token is valid in REST since this is in mw.
Expand Down Expand Up @@ -248,6 +246,16 @@ SG-Proto Orgfile
** TODO Migration tests
** TODO Don't deliver AppVeyor binary unless the branch is merged
** TODO Figure out travisCI build artifacts / releases
** TODO Update changelog v0.0.1
** TODO Update CONTRIBUTING.md etc

* v0.0.2
** DONE Add GET /admin/tickets?count=<n>
CLOSED: [2017-04-10 Mon 08:39]
** DONE Make stream/river errExists clearer
CLOSED: [2017-04-10 Mon 08:30]
** DONE If database is closed, can't clean up rivers
CLOSED: [2017-05-08 Mon 14:58]

* v0.0.1
** DONE Bugs
Expand Down Expand Up @@ -620,4 +628,5 @@ SG-Proto Orgfile
CLOSED: [2017-03-16 Thu 09:59]
*** DONE Unit tests
CLOSED: [2017-03-16 Thu 09:59]
** TODO Deploy
** DONE Deploy
CLOSED: [2017-04-30 Sun 12:16]
188 changes: 188 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package client

import (
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"

"github.com/synapse-garden/sg-proto/auth"
"github.com/synapse-garden/sg-proto/incept"
"github.com/synapse-garden/sg-proto/rest"
"github.com/synapse-garden/sg-proto/stream"
"github.com/synapse-garden/sg-proto/users"

ws "golang.org/x/net/websocket"
)

// Client is a client for an SG backend.
type Client struct {
State State
APIKey string
Backend *url.URL
}

// Info sets the given info based on the backend's /source.
func (c *Client) Info(i *rest.SourceInfo) error {
return DecodeGet(i, c.Backend.String()+"/source")
}

// GetTickets returns GET /admin/tickets?count=n
func (c *Client) GetTickets(into *[]incept.Ticket, n int) error {
if c.APIKey == "" {
return errors.New("client must have a valid admin API key")
}

str := c.Backend.String() + "/admin/tickets"
if n > 1 {
str = fmt.Sprintf("%s?count=%d", str, n)
}
return DecodeGet(into, str, AdminHeader(c.APIKey))
}

// CreateTickets creates incept tickets using the given API key.
func (c *Client) CreateTickets(into *[]incept.Ticket, n int) error {
if c.APIKey == "" {
return errors.New("client must have a valid admin API key")
}

str := c.Backend.String() + "/admin/tickets"
if n > 1 {
str = fmt.Sprintf("%s?count=%d", str, n)
}
return DecodePost(into, str, nil, AdminHeader(c.APIKey))
}

// CreateLogin creates a user with the given name and password (which is
// hashed before sending) and unmarshals the response into the given *Login.
func (c *Client) CreateLogin(
l *auth.Login,
ticket, name, pw string,
) error {
b := sha256.Sum256([]byte(pw))
return DecodePost(l,
c.Backend.String()+"/incept/"+ticket,
&auth.Login{
User: users.User{Name: name},
PWHash: b[:],
},
)
}

// VerifyAdmin checks that the Client's APIKey is a valid Admin key.
func (c *Client) VerifyAdmin(key string) error {
var ok bool
return DecodeGet(&ok, c.Backend.String()+"/admin/verify",
AdminHeader(key))
}

// Login uses the given *auth.Login's Token to get a Session.
func (c *Client) Login(l *auth.Login) error {
s := c.State.Session
return DecodePost(s, c.Backend.String()+"/tokens", l)
}

// Logout deletes the given *auth.Login's Token.
func (c *Client) Logout() error {
if sesh := c.State.Session; sesh == nil {
return errors.New("nil session")
} else if t := sesh.Token; t == nil {
return errors.New("nil session token")
} else {
return Delete(c.Backend.String()+"/tokens",
AuthHeader(auth.BearerType, t))
}
}

// GetProfile gets the User for the given Session.
func (c *Client) GetProfile(u *users.User) error {
s := c.State.Session
return DecodeGet(u, c.Backend.String()+"/profile",
AuthHeader(auth.BearerType, s.Token))
}

// DeleteProfile deletes the Session owner's profile.
func (c *Client) DeleteProfile() error {
s := c.State.Session
return Delete(c.Backend.String()+"/profile",
AuthHeader(auth.BearerType, s.Token))
}

// CreateStream creates a new Stream belonging to the Session owner.
func (c *Client) CreateStream(str *stream.Stream) error {
if sesh := c.State.Session; sesh == nil {
return errors.New("nil session")
} else if t := sesh.Token; t == nil {
return errors.New("nil session token")
} else {
return DecodePost(str,
c.Backend.String()+"/streams",
str,
AuthHeader(auth.BearerType, t),
)
}
}

// GetStream gets a Stream by ID.
func (c *Client) GetStream(str *stream.Stream, id string) error {
if sesh := c.State.Session; sesh == nil {
return errors.New("nil session")
} else if t := sesh.Token; t == nil {
return errors.New("nil session token")
} else {
return DecodeGet(str,
c.Backend.String()+"/streams/"+id,
AuthHeader(auth.BearerType, t),
)
}
}

// AllStreams gets the User's owned Streams.
func (c *Client) AllStreams(strs *[]*stream.Stream, filters ...Param) error {
if sesh := c.State.Session; sesh == nil {
return errors.New("nil session")
} else if t := sesh.Token; t == nil {
return errors.New("nil session token")
} else {
return DecodeGet(strs, fmt.Sprintf(
"%s/streams%s",
c.Backend,
ApplyParams(filters...)),
AuthHeader(auth.BearerType, t),
)
}
}

// GetStreamWS opens and returns a *golang.org/x/net/websocket.Conn.
func (c *Client) GetStreamWS(id string) (*ws.Conn, error) {
s := c.State.Session
if c.State.Session == nil {
return nil, fmt.Errorf("cannot get stream with a nil Session")
}
backend := *c.Backend
switch backend.Scheme {
case "http":
backend.Scheme = "ws"
case "https":
backend.Scheme = "wss"
}

backend.Path += "/streams/" + id + "/start"
var conf *tls.Config
if t, ok := customClient.Transport.(*http.Transport); ok {
conf = t.TLSClientConfig
}

wsToken := base64.RawURLEncoding.EncodeToString(s.Token)

return ws.DialConfig(&ws.Config{
Location: &backend,
Origin: &url.URL{},
TlsConfig: conf,
Version: ws.ProtocolVersionHybi13,
Protocol: []string{"Bearer+" + wsToken},
})
}
145 changes: 145 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package client_test

import (
"crypto/sha256"
"time"

uuid "github.com/satori/go.uuid"
"github.com/synapse-garden/sg-proto/auth"
"github.com/synapse-garden/sg-proto/incept"
"github.com/synapse-garden/sg-proto/rest"
"github.com/synapse-garden/sg-proto/users"

. "gopkg.in/check.v1"
)

func (s *ClientSuite) TestInfo(c *C) {
info := new(rest.SourceInfo)
c.Assert(s.cli.Info(info), IsNil)
c.Check(info, DeepEquals, &src)
}

func (s *ClientSuite) TestCreateTickets(c *C) {
ak := s.cli.APIKey
s.cli.APIKey = ""

ts := make([]incept.Ticket, 0)
c.Check(s.cli.CreateTickets(&ts, 5), ErrorMatches, `client must have a valid admin API key`)
c.Check(ts, DeepEquals, []incept.Ticket{})

s.cli.APIKey = ak
c.Assert(s.cli.CreateTickets(&ts, 5), IsNil)
c.Check(len(ts), Equals, 5)
for _, t := range ts {
c.Check(len(t), Equals, 16)
uu, err := uuid.FromBytes(t.Bytes())
c.Check(err, IsNil)
c.Check(uuid.Equal(uu, uuid.UUID(t)), Equals, true)
// It's a valid UUID
}
}

func (s *ClientSuite) TestCreateLogin(c *C) {
var ts []incept.Ticket
c.Assert(s.cli.CreateTickets(&ts, 1), IsNil)
c.Check(len(ts), Equals, 1)

l := new(auth.Login)
ticket := ts[0].String()
c.Assert(s.cli.CreateLogin(l, ticket, "bodie", "hello"), IsNil)
c.Check(l, DeepEquals, &auth.Login{
User: users.User{
Name: "bodie",
Coin: 0,
},
})

// Does it work?
bs := sha256.Sum256([]byte("hello"))
c.Assert(s.cli.Login(&auth.Login{
User: users.User{Name: "bodie"},
PWHash: bs[:],
}), IsNil)
sesh := s.cli.State.Session
c.Check(sesh.Expiration, Not(Equals), time.Time{})
c.Check(sesh.Token, NotNil)
}

func (s *ClientSuite) TestLogin(c *C) {
var ts []incept.Ticket
c.Assert(s.cli.CreateTickets(&ts, 1), IsNil)
c.Check(len(ts), Equals, 1)

l := new(auth.Login)
ticket := ts[0].String()
c.Assert(s.cli.CreateLogin(l, ticket, "bodie", "hello"), IsNil)
c.Check(l, DeepEquals, &auth.Login{
User: users.User{
Name: "bodie",
Coin: 0,
},
})

bs := sha256.Sum256([]byte("hello"))
c.Assert(s.cli.Login(&auth.Login{
User: users.User{Name: "bodie"},
PWHash: bs[:],
}), IsNil)
sesh := s.cli.State.Session
c.Check(sesh.Expiration, Not(Equals), time.Time{})
}

func (s *ClientSuite) TestGetProfile(c *C) {
var ts []incept.Ticket
c.Assert(s.cli.CreateTickets(&ts, 1), IsNil)
c.Check(len(ts), Equals, 1)

l := new(auth.Login)
ticket := ts[0].String()
c.Assert(s.cli.CreateLogin(l, ticket, "bodie", "hello"), IsNil)
c.Check(l, DeepEquals, &auth.Login{
User: users.User{
Name: "bodie",
Coin: 0,
},
})

bs := sha256.Sum256([]byte("hello"))
c.Assert(s.cli.Login(&auth.Login{
User: users.User{Name: "bodie"},
PWHash: bs[:],
}), IsNil)

u := new(users.User)
c.Assert(s.cli.GetProfile(u), IsNil)
c.Check(*u, DeepEquals, l.User)
}

func (s *ClientSuite) TestDeleteProfile(c *C) {
var ts []incept.Ticket
c.Assert(s.cli.CreateTickets(&ts, 1), IsNil)
c.Check(len(ts), Equals, 1)

l := new(auth.Login)
ticket := ts[0].String()
c.Assert(s.cli.CreateLogin(l, ticket, "bodie", "hello"), IsNil)
c.Check(l, DeepEquals, &auth.Login{
User: users.User{
Name: "bodie",
Coin: 0,
},
})

bs := sha256.Sum256([]byte("hello"))
c.Assert(s.cli.Login(&auth.Login{
User: users.User{Name: "bodie"},
PWHash: bs[:],
}), IsNil)

u := new(users.User)
c.Assert(s.cli.GetProfile(u), IsNil)
c.Check(*u, DeepEquals, l.User)

c.Assert(s.cli.DeleteProfile(), IsNil)
c.Check(s.db.View(users.CheckNotExist(u.Name)), IsNil)
}
Loading