diff --git a/.env b/.env index 5eb2fc8..0b17750 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ KDOORPI_API_ALLOWED=http://127.0.0.1:3333/allowed KDOORPI_API_LONGPOLL=http://127.0.0.1:3333/longpoll KDOORPI_API_KEY=keykey +KDOORPI_MOCK_HW=true diff --git a/godoor.go b/godoor.go index eb6e127..64bd233 100644 --- a/godoor.go +++ b/godoor.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "context" "encoding/json" "fmt" @@ -9,6 +10,8 @@ import ( "net/http" "os" "os/signal" + "strings" + "sync" "syscall" ) import "time" @@ -37,6 +40,7 @@ type ValidUids map[string]bool // bool has no meaning type Config struct { door string uid_salt string + mock string api struct { allowed string longpoll string @@ -46,6 +50,9 @@ type Config struct { } var config Config +var globalLock sync.Mutex +var validUids ValidUids +var wiegand Wiegand func main() { @@ -60,16 +67,31 @@ func main() { config.api.swipe = os.Getenv("KDOORPI_API_SWIPE") config.api.key = os.Getenv("KDOORPI_API_KEY") config.uid_salt = os.Getenv("KDOORPI_UID_SALT") + config.mock = os.Getenv("KDOORPI_MOCK_HW") - //wiegand := WiegandSetup(wiegand_a, wiegand_b, wiegand_bit_timeout, solenoid) + if config.mock == "true" { + wiegand = &WiegandMock{} + } else { + wiegand = WiegandSetup(wiegand_a, wiegand_b, wiegand_bit_timeout, solenoid) + } - http.DefaultClient.Timeout = 60 + http.DefaultClient.Timeout = 120 * time.Second - reloadTokens() + for { + err := reloadTokens() + if err == nil { + break + } + time.Sleep(10 * time.Second) + } - //go wiegand.cardRunner(validUids) + go cardRunner(wiegand) - waitEvents() + go func() { + for { + waitEvents() + } + }() fmt.Printf("Sleeping\n") @@ -79,36 +101,101 @@ func main() { // cleanup } -func waitEvents() { - req, err := http.NewRequest(http.MethodGet, config.api.longpoll, nil) - if err != nil { - panic(err) - } - req.Header.Add("KEY", config.api.key) - resp, err := http.DefaultClient.Do(req) - if err != nil { - panic(err) - } - fmt.Printf("%v\n", resp) +func cardRunner(w Wiegand) { + for { + card, err := w.GetCardUid() + if err != nil { + continue + } - _, err = io.ReadAll(resp.Body) - if err != nil { - panic(err) - } + printCardId(card) + hashedHex := hashCardUid(card) + fmt.Println(hashedHex) - fmt.Printf("%v\n", resp) + globalLock.Lock() + ok := validUids[hashedHex] + globalLock.Unlock() + if ok { + fmt.Println("Opening door") + w.OpenDoor() + } else { + fmt.Println("Unknown card") + } + + } +} + +func ParseNextMessage(r *bufio.Reader) (string, error) { + var message string + + for { + s, err := r.ReadString('\n') + if err != nil { + return message, err + } + + message = message + s + nextBytes, err := r.ReadByte() + if err != nil { + return message, err + } + + if nextBytes == '\n' { + return message, nil + } + + r.UnreadByte() + } } -func reloadTokens() { - req, err := http.NewRequest(http.MethodGet, config.api.allowed, nil) +func waitEvents() error { + req, err := http.NewRequest(http.MethodGet, config.api.longpoll, nil) if err != nil { - panic(err) + return err } req.Header.Add("KEY", config.api.key) resp, err := http.DefaultClient.Do(req) if err != nil { - panic(err) + return err + } + fmt.Printf("%v\n", resp) + + reader := bufio.NewReader(resp.Body) + for { + msg, err := ParseNextMessage(reader) + if err != nil { + return err + } + + for _, line := range strings.Split(msg, "\n") { + data, found_data := strings.CutPrefix(line, "data:") + if !found_data { + continue + } + fmt.Printf("got server data: %q\n", data) + if strings.TrimSpace(data) == config.door { + wiegand.OpenDoor() + } + + go reloadTokens() + } + + } + + fmt.Printf("%v\n", resp) + return nil +} + +func reloadTokens() error { + req, err := http.NewRequest(http.MethodGet, config.api.allowed, nil) + if err != nil { + return err + } + req.Header.Add("KEY", config.api.key) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err } fmt.Printf("%v\n", resp) @@ -116,17 +203,21 @@ func reloadTokens() { body, err := io.ReadAll(resp.Body) if err != nil { - panic(err) + return err } err = json.Unmarshal(body, &cl) if err != nil { - panic(err) + return err } - validUids := make(ValidUids) + globalLock.Lock() + defer globalLock.Unlock() + validUids = make(ValidUids) for i, val := range cl.AllowedUids { fmt.Printf("%d: %+v\n", i, val.Token.UidHash) - validUids[val.Token.UidHash] = false + validUids[val.Token.UidHash] = true } + + return nil } diff --git a/godoor_server/godoor_server.go b/godoor_server/godoor_server.go index 558bb29..977e022 100644 --- a/godoor_server/godoor_server.go +++ b/godoor_server/godoor_server.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "os" + "time" ) type DoorBoyServer struct { @@ -36,7 +37,7 @@ func getAllowed(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") keys := cardList{ AllowedUids: []cardToken{ - {card{UidHash: "0a0b0c0d0e0f"}}, + {card{UidHash: "d72c87d0f077c7766f2985dfab30e8955c373a13a1e93d315203939f542ff86e73ee37c31f4c4b571f4719fa8e3589f12db8dcb57ea9f56764bb7d58f64cf705"}}, {card{UidHash: "112233445566"}}, {card{UidHash: "aabbccddeeff"}}, }, @@ -66,29 +67,42 @@ func (doorboyserver *DoorBoyServer) getLongPoll(w http.ResponseWriter, r *http.R } events := make(chan string) doorboyserver.longPollers[r.RemoteAddr] = events + defer delete(doorboyserver.longPollers, r.RemoteAddr) + err = writeAndFlush(w, []byte("data: watch-stream-opened\n\n")) if err != nil { - delete(doorboyserver.longPollers, r.RemoteAddr) return } d := <-events // get door open event _, err = w.Write([]byte("data: ")) if err != nil { - delete(doorboyserver.longPollers, r.RemoteAddr) return } _, err = w.Write([]byte(d)) if err != nil { - delete(doorboyserver.longPollers, r.RemoteAddr) return } err = writeAndFlush(w, []byte("\n\n")) if err != nil { - delete(doorboyserver.longPollers, r.RemoteAddr) return } - delete(doorboyserver.longPollers, r.RemoteAddr) +} +func (doorboyserver *DoorBoyServer) getLongPollJson(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + fmt.Println("Failed to get flusher for http response") + } + w.WriteHeader(http.StatusOK) + flusher.Flush() + respJson, _ := json.Marshal([]string{"123456789", "aabbccddeeff", "aabbcc"}) + for i := 1; i <= 10; i++ { + w.Write(respJson) + w.Write([]byte("\n")) + fmt.Println("send") + flusher.Flush() // Trigger "chunked" encoding and send a chunk... + time.Sleep(500 * time.Millisecond) + } } func (doobserver *DoorBoyServer) postOpenDoor(w http.ResponseWriter, r *http.Request) { @@ -105,6 +119,7 @@ func main() { http.HandleFunc("/", getRoot) http.HandleFunc("/allowed", getAllowed) http.HandleFunc("/longpoll", doorboyserver.getLongPoll) + http.HandleFunc("/jspoll", doorboyserver.getLongPollJson) http.HandleFunc("/open", doorboyserver.postOpenDoor) err := http.ListenAndServe(":3333", nil) diff --git a/wiegand.go b/wiegand.go index 6e6aee1..a6f6b5d 100644 --- a/wiegand.go +++ b/wiegand.go @@ -12,7 +12,12 @@ import ( "github.com/warthog618/gpiod" ) -type Wiegand struct { +type Wiegand interface { + GetCardUid() (uint64, error) + OpenDoor() +} + +type WiegandHW struct { aLine *gpiod.Line bLine *gpiod.Line bits [64]bool @@ -23,7 +28,7 @@ type Wiegand struct { solenoidLine *gpiod.Line } -func (w *Wiegand) OpenDoor() { +func (w *WiegandHW) OpenDoor() { fmt.Println("Open") w.solenoidLine.SetValue(1) d, _ := time.ParseDuration("500ms") @@ -39,65 +44,58 @@ func printCardId(card uint64) { fmt.Printf("\n") } -func (w *Wiegand) cardRunner(validUids ValidUids) { - for { - // Wait for bit timeout - fmt.Printf("Waiting for bit timeout\n") - <-w.bitTimeoutTimer.C - fmt.Printf("\n") - - if w.bitNr != 64 { - fmt.Printf("We got less than 64 bits: %d\n", w.bitNr) - } - - var card uint64 = 0 - for i := 63; i != 0; i-- { - if w.bits[i] == true { - card |= 1 << (63 - i) - } - } - - printCardId(card) - - b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, card) - hashed, err := scrypt.Key(b, []byte("hashsah"), 16384, 8, 1, 64) - if err != nil { - panic(err) - } - - hashedHex := hex.EncodeToString(hashed) - fmt.Println(hashedHex) - - _, ok := validUids[hashedHex] - if ok { - fmt.Println("Opening door") - w.OpenDoor() - } else { - fmt.Println("Unknown card") - } - - w.bitNr = 0 +func hashCardUid(card uint64) string { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, card) + hashed, err := scrypt.Key(b, []byte(config.uid_salt), 16384, 8, 1, 64) + if err != nil { + panic(err) // can only happen when scrypt params are garbage } + + hashedHex := hex.EncodeToString(hashed) + return hashedHex } -func (w *Wiegand) wiegandAEvent(evt gpiod.LineEvent) { +func (w *WiegandHW) GetCardUid() (uint64, error) { + // Wait for bit timeout + fmt.Printf("Waiting for bit timeout\n") + + <-w.bitTimeoutTimer.C + fmt.Printf("\n") + + defer func() { w.bitNr = 0 }() + + if w.bitNr != 64 { + return 0, fmt.Errorf("We got less than 64 bits: %d\n", w.bitNr) + } + + var card uint64 = 0 + for i := 63; i != 0; i-- { + if w.bits[i] == true { + card |= 1 << (63 - i) + } + } + + return card, nil +} + +func (w *WiegandHW) wiegandAEvent(evt gpiod.LineEvent) { w.bitTimeoutTimer.Reset(w.bitTimeout) w.bits[w.bitNr] = false fmt.Printf("0") w.bitNr += 1 } -func (w *Wiegand) wiegandBEvent(evt gpiod.LineEvent) { +func (w *WiegandHW) wiegandBEvent(evt gpiod.LineEvent) { w.bitTimeoutTimer.Reset(w.bitTimeout) w.bits[w.bitNr] = true fmt.Printf("1") w.bitNr += 1 } -func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *Wiegand { +func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *WiegandHW { - var wiegand Wiegand + var wiegand WiegandHW wiegand.bitTimeout = bitTimeout wiegand.bitTimeoutTimer = time.NewTimer(wiegand.bitTimeout) @@ -128,7 +126,7 @@ func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *Wiegand return &wiegand } -func (w *Wiegand) WiegandClose() { +func (w *WiegandHW) WiegandClose() { w.aLine.Close() w.bLine.Close() w.solenoidLine.Close() diff --git a/wiegand_mock.go b/wiegand_mock.go new file mode 100644 index 0000000..0eaef71 --- /dev/null +++ b/wiegand_mock.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "time" +) + +type WiegandMock struct { + mockUid uint64 +} + +func (*WiegandMock) OpenDoor() { + fmt.Println("Door is now open") + time.Sleep(500 * time.Millisecond) + fmt.Println("Door is now closed") +} + +func (w *WiegandMock) GetCardUid() (uint64, error) { + time.Sleep(1 * time.Second) + return w.mockUid, fmt.Errorf("err") +} + +func (w *WiegandMock) SetMockUid(mockUid uint64) { + w.mockUid = mockUid +}