package app import ( "encoding/json" "net" "net/http" "strings" ) // HA ingress authenticates the user. Only its gateway may reach this listener; // standalone mode binds to loopback instead. func Handler(locks []*Lock, readers []ReaderConfig, ingress bool) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /api/locks", func(w http.ResponseWriter, r *http.Request) { statuses := make([]LockStatus, 0, len(locks)) for _, l := range locks { statuses = append(statuses, l.Status()) } readerViews := make([]ReaderView, 0, len(readers)) for _, reader := range readers { v := ReaderView{ReaderConfig: reader} if _, err := Group(Assigned(locks, reader.ID)); err != nil { v.Warning = err.Error() } readerViews = append(readerViews, v) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Locks []LockStatus `json:"locks"` Readers []ReaderView `json:"readers"` }{statuses, readerViews}) }) mux.HandleFunc("POST /api/locks/{id}/{action}", func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Homekey-Action") != "pairing" { http.Error(w, "missing action header", http.StatusForbidden) return } for _, l := range locks { if l.Config.ID == r.PathValue("id") { switch r.PathValue("action") { case "pair": if err := l.Pair(); err != nil { http.Error(w, err.Error(), http.StatusConflict) return } case "cancel": l.CancelPair() default: http.NotFound(w, r) return } w.WriteHeader(http.StatusNoContent) return } } http.NotFound(w, r) }) mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(page)) }) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host, _, err := net.SplitHostPort(r.RemoteAddr) if ingress && (err != nil || host != "172.30.32.2") { http.Error(w, "ingress only", http.StatusForbidden) return } if origin := r.Header.Get("Origin"); !ingress && origin != "" && !strings.HasSuffix(origin, "://"+r.Host) { http.Error(w, "invalid origin", http.StatusForbidden) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'self'") mux.ServeHTTP(w, r) }) } type ReaderView struct { ReaderConfig Warning string `json:"warning,omitempty"` } const page = `
Add or edit locks and readers in the app's Configuration tab, then restart. Keep each lock ID unchanged after pairing.
Pair opens a five-minute HomeKit setup window. Add the lock in Apple Home using the displayed code.