penguin/homekey-go

homekey-go

internal/app/pairing.go

raw ยท 2850 bytes

package app

import (
	"crypto/rand"
	"fmt"
	"github.com/brutella/hap"
	"github.com/go-chi/chi"
	"homekey.local/provisioner/internal/hkserver"
	"homekey.local/provisioner/internal/physical"
	"homekey.local/provisioner/internal/provision"
	"math/big"
	"net/http"
	"sync"
	"time"
)

type Lock struct {
	Physical *physical.Manager
	Config   LockConfig
	Store    *provision.Store
	Device   *hkserver.Device
	pairing  sync.Mutex
	until    time.Time
	pin      string
}

func NewLock(c LockConfig, store *provision.Store, ifaces []string) (*Lock, error) {
	// PIN exists only in process memory. It is never printed or persisted.
	var pin string
	for {
		n, err := rand.Int(rand.Reader, big.NewInt(100000000))
		if err != nil {
			return nil, err
		}
		pin = fmt.Sprintf("%08d", n)
		if !hap.InvalidPins[pin] {
			break
		}
	}
	d, err := hkserver.New(store, hkserver.Config{Name: c.Name, Serial: c.ID, Finish: c.Finish, PIN: pin, Address: fmt.Sprintf(":%d", c.Port), Interfaces: ifaces})
	if err != nil {
		return nil, err
	}
	l := &Lock{Config: c, Store: store, Device: d, pin: pin}
	mux := d.Server.ServeMux().(*chi.Mux)
	var original http.Handler
	var find func(chi.Routes)
	find = func(r chi.Routes) {
		for _, route := range r.Routes() {
			if route.Pattern == "/pair-setup" {
				original = route.Handlers["POST"]
			}
			if route.SubRoutes != nil {
				find(route.SubRoutes)
			}
		}
	}
	find(mux)
	if original == nil {
		return nil, fmt.Errorf("HAP pair-setup route missing")
	}
	mux.Method("POST", "/pair-setup", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		l.pairing.Lock()
		defer l.pairing.Unlock()
		if !time.Now().Before(l.until) || store.Summary().PairedControllers > 0 {
			w.Header().Set("Content-Type", hap.HTTPContentTypePairingTLV8)
			w.Write([]byte{6, 1, 2, 7, 1, 6}) // M2 / unavailable
			return
		}
		original.ServeHTTP(w, r)
		if store.Summary().PairedControllers > 0 {
			l.until = time.Time{}
		}
	}))
	return l, nil
}
func (l *Lock) Pair() error {
	l.pairing.Lock()
	defer l.pairing.Unlock()
	if l.Store.Summary().PairedControllers > 0 {
		return fmt.Errorf("lock is already paired")
	}
	if !time.Now().Before(l.until) {
		l.until = time.Now().Add(5 * time.Minute)
	}
	return nil
}
func (l *Lock) CancelPair() { l.pairing.Lock(); defer l.pairing.Unlock(); l.until = time.Time{} }

type LockStatus struct {
	LockConfig
	Summary provision.Summary `json:"summary"`
	PIN     string            `json:"pin,omitempty"`
	Until   time.Time         `json:"until,omitempty"`
}

func (l *Lock) Status() LockStatus {
	l.pairing.Lock()
	defer l.pairing.Unlock()
	s := LockStatus{LockConfig: l.Config, Summary: l.Store.Summary()}
	if s.Summary.PairedControllers == 0 && time.Now().Before(l.until) {
		// Follow the format shown in home app XXXX-XXXX
		s.PIN = l.pin[:4] + "-" + l.pin[4:]
		s.Until = l.until
	}
	return s
}