diff --git a/README.md b/README.md index a5dc179..83874cc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ Additionally, these options control networking aspects for the bridge: These settings are optional and can be left blank. +The pairing code for the server is generated dynamically on first startup +and printed to the console (or systemd journal). Alternatively, you can specify +it using `Pin` in the config file to have it fixed. + License ======== diff --git a/bridge.go b/bridge.go index 31baf06..424aaf9 100644 --- a/bridge.go +++ b/bridge.go @@ -12,9 +12,11 @@ import ( "net/url" "context" + "crypto/rand" "encoding/json" "fmt" "log" + "math/big" "net" "net/http" "reflect" @@ -36,6 +38,9 @@ const ( // Store name for persisting zigbee2mqtt state Z2M_STATE_STORE = "z2m_state" + // Store name for server PIN code + Z2M_PIN_STORE = "z2m_pin" + // timeout for UpdateZ2MState Z2M_UPDATE_TIMEOUT = 3 * time.Second @@ -65,6 +70,7 @@ type Bridge struct { devices map[string]*BridgeDevice server *hap.Server store hap.Store + pin string // RWMutex protects hap init variables below hapInitMutex sync.RWMutex @@ -102,6 +108,51 @@ func NewBridge(ctx context.Context, storeDir string) *Bridge { return br } +// Sets the PIN code for the HAP server. +// If the given pin is empty, it will be read from the store, or failing that, +// one will be generated +func (br *Bridge) SetPin(pin string) (string, error) { + // if PIN was not explicitly specified, we re-use the existing one from store + if pin == "" { + if storePin, err := br.store.Get(Z2M_PIN_STORE); err == nil { + pin = string(storePin) + } + } + + savePin := pin == "" + + if pin == "" { + for { + rnd, err := rand.Int(rand.Reader, big.NewInt(99999999+1)) + if err != nil { + return "", fmt.Errorf("can't generate PIN: %v", err) + } + + // pad if necessary + pin = rnd.Text(10) + "00000000" + pin = pin[:8] + + // ensure it's not an insecure PIN + if !hap.InvalidPins[pin] { + break + } + } + } else if hap.InvalidPins[pin] { + return "", fmt.Errorf("insecure pin %s", pin) + } + + // persist the PIN + if savePin { + br.store.Set(Z2M_PIN_STORE, []byte(pin)) + } + + br.pin = pin + return pin, nil +} + +// Returns the PIN +func (br *Bridge) GetPin() string { return br.pin } + // Initializes the hap.Server and calls ListenAndServe(). // ListenAndServe() will block until the context is cancelled func (br *Bridge) StartHAP() error { @@ -109,6 +160,13 @@ func (br *Bridge) StartHAP() error { return fmt.Errorf("bridge accessory not created yet") } + // initialize PIN, either from store or dynamically generated + if br.pin == "" { + if _, err := br.SetPin(""); err != nil { + return err + } + } + br.hapInitMutex.RLock() if !br.hapInitDone { br.hapInitMutex.RUnlock() @@ -124,6 +182,8 @@ func (br *Bridge) StartHAP() error { return err } + br.server.Pin = br.pin + br.server.Addr = br.ListenAddr br.server.Ifaces = br.Interfaces diff --git a/cmd/hapz2m/main.go b/cmd/hapz2m/main.go index 971f37c..0b39e2b 100644 --- a/cmd/hapz2m/main.go +++ b/cmd/hapz2m/main.go @@ -36,6 +36,8 @@ type config struct { ListenAddr string Interfaces []string + Pin string + Server, Username, Password string } @@ -89,6 +91,10 @@ func main() { log.Fatalf("-quiet and -debug options are mutually-exclusive") } + if _, err := br.SetPin(cfg.Pin); err != nil { + log.Fatalf("cannot set PIN code: %v", err) + } + // validate ListenAddr if specified if cfg.ListenAddr != "" { _, _, err := net.SplitHostPort(cfg.ListenAddr) @@ -120,6 +126,9 @@ func main() { log.Println("hapz2m configured. starting HAP server...") + pin := br.GetPin() + log.Printf("server PIN is %s-%s", pin[:4], pin[4:]) + err = br.StartHAP() if err != nil { if err == http.ErrServerClosed {