drivers/sesame/registration.go
raw ยท 4918 bytes
package sesame
import (
"context"
"errors"
"fmt"
"log"
"sync"
"time"
)
// RegistrationState never exposes a token or key. Connected means GATT is ready;
// Ready means the initial notification has arrived and Register may be clicked.
type RegistrationState struct {
Target Target `json:"target"`
Phase string `json:"phase"`
Connected bool `json:"connected"`
Error string `json:"error,omitempty"`
}
type registrationResult struct {
client *Client
err error
}
type registrationRequest struct {
ctx context.Context
persist func(Credential) error
reply chan registrationResult
}
// RegistrationConnection owns an unregistered connection while the user decides
// whether to register. Only its worker reads notifications or writes commands.
type RegistrationConnection struct {
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
state RegistrationState
requests chan registrationRequest
done chan struct{}
}
func PrepareRegistration(ctx context.Context, target Target, dial Dialer) *RegistrationConnection {
life, cancel := context.WithCancel(ctx)
p := &RegistrationConnection{ctx: life, cancel: cancel, state: RegistrationState{Target: target, Phase: "connecting"}, requests: make(chan registrationRequest), done: make(chan struct{})}
go p.run(target, dial)
return p
}
func (p *RegistrationConnection) State() RegistrationState {
p.mu.RLock()
defer p.mu.RUnlock()
return p.state
}
func (p *RegistrationConnection) set(target Target, phase string, connected bool, err error) {
p.mu.Lock()
defer p.mu.Unlock()
p.state = RegistrationState{Target: target, Phase: phase, Connected: connected}
if err != nil {
p.state.Error = err.Error()
}
if err != nil {
log.Printf("Sesame %s on %s: registration %s: %v", target.UUID, target.Adapter, phase, err)
} else {
log.Printf("Sesame %s on %s: registration %s", target.UUID, target.Adapter, phase)
}
}
func (p *RegistrationConnection) Close() { p.cancel(); <-p.done }
// Register never dials. Once accepted, wait for the bounded exchange to finish,
// even if the HTTP request is canceled, so a live client cannot be orphaned.
func (p *RegistrationConnection) Register(ctx context.Context, persist func(Credential) error) (*Client, error) {
if p.State().Phase != "ready" {
return nil, errors.New("lock is not ready to register; wait for its initial token or scan again")
}
req := registrationRequest{ctx: ctx, persist: persist, reply: make(chan registrationResult, 1)}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-p.done:
return nil, errors.New("registration connection ended; scan again")
case p.requests <- req:
}
result := <-req.reply
return result.client, result.err
}
func (p *RegistrationConnection) run(target Target, dial Dialer) {
defer close(p.done)
defer p.cancel()
setup, cancel := context.WithTimeout(p.ctx, setupTimeout)
l, actual, err := dial(setup, target)
cancel()
if err != nil {
p.set(target, "failed", false, fmt.Errorf("connect for Sesame registration: %w", err))
return
}
handedOff := false
defer func() {
if !handedOff {
l.Close()
}
}()
target = actual
if actual.Registered {
p.set(target, "failed", false, errors.New("lock already registered: import its local key instead"))
return
}
p.set(target, "waiting-token", true, nil)
s := &session{link: l}
initial, done := context.WithTimeout(p.ctx, 10*time.Second)
token, err := s.initial(initial)
done()
if err != nil {
p.set(target, "failed", false, fmt.Errorf("wait for Sesame registration initial token: %w", err))
return
}
p.set(target, "ready", true, nil)
for {
select {
case <-p.ctx.Done():
p.set(target, "disconnected", false, p.ctx.Err())
return
case err := <-l.Done():
if err == nil {
err = errors.New("Sesame connection closed")
}
p.set(target, "disconnected", false, err)
return
case fragment, ok := <-l.Packets():
if !ok {
p.set(target, "disconnected", false, errors.New("Sesame notifications closed"))
return
}
payload, err := s.decode(fragment)
if err == nil && payload != nil {
err = s.handle(payload)
}
if err != nil {
p.set(target, "failed", false, err)
return
}
case req := <-p.requests:
p.set(target, "registering", true, nil)
command, finish := context.WithTimeout(req.ctx, 10*time.Second)
stop := context.AfterFunc(p.ctx, finish)
credential, err := s.register(command, token, req.persist)
stop()
finish()
if err != nil {
p.set(target, "failed", false, err)
req.reply <- registrationResult{err: err}
return // Never replay a possibly committed registration.
}
target.Registered = true
client := NewClient(target, credential, dial)
client.initial = s
client.state = s.state
p.set(target, "registered", true, nil)
handedOff = true
req.reply <- registrationResult{client: client}
return
}
}
}