penguin/homekey-go

homekey-go

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
		}
	}
}