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