forked from k-space/godoor
Compare commits
25 Commits
v2
...
issue-11-k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728939cef3 | ||
| bd94037e29 | |||
| 7bd5f4184b | |||
| ab5c255b28 | |||
| 9bf71b347f | |||
|
|
2f4005a7ba | ||
|
|
8d4df68b95 | ||
| b2e810f0dd | |||
| a4b42715a4 | |||
| fe35e0ff21 | |||
| c6bacca63d | |||
| 0d9bf1a3f7 | |||
| 826df446d7 | |||
| 5e64dfa04a | |||
| aa37e8e291 | |||
| 6ad260078e | |||
| 2008c9f999 | |||
| 7e1ba5dd4d | |||
| 8c14e3bc41 | |||
| e0dacb1e4b | |||
|
|
647a90c0f2 | ||
|
|
92469f11b2 | ||
| a75aeeb8f9 | |||
|
|
d4f2624635 | ||
|
|
2259d3ed6b |
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
./.gitignore
|
||||||
6
.env
6
.env
@@ -1,6 +0,0 @@
|
|||||||
KDOORPI_DOOR=workshop
|
|
||||||
KDOORPI_API_ALLOWED=http://127.0.0.1:3333/allowed
|
|
||||||
KDOORPI_API_LONGPOLL=http://127.0.0.1:3333/longpoll
|
|
||||||
KDOORPI_API_SWIPE=http://127.0.0.1:3333/cardswipe
|
|
||||||
KDOORPI_API_KEY=keykey
|
|
||||||
KDOORPI_MOCK_HW=true
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@
|
|||||||
godoor
|
godoor
|
||||||
godoor_server/godoor_server
|
godoor_server/godoor_server
|
||||||
godoor_server/keys.json
|
godoor_server/keys.json
|
||||||
|
__debug_bin*
|
||||||
|
.env
|
||||||
|
.vscode
|
||||||
|
|||||||
28
.woodpecker/build.yaml
Normal file
28
.woodpecker/build.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
matrix:
|
||||||
|
ARCH:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: woodpeckerci/plugin-kaniko
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: ${ARCH}
|
||||||
|
tolerations:
|
||||||
|
- key: arch
|
||||||
|
operator: Equal
|
||||||
|
value: ${ARCH}
|
||||||
|
effect: NoSchedule
|
||||||
|
settings:
|
||||||
|
repo: ${CI_REPO}
|
||||||
|
registry: harbor.k-space.ee
|
||||||
|
tags: latest-${ARCH}
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
when:
|
||||||
|
- branch: master
|
||||||
30
.woodpecker/manifest.yaml
Normal file
30
.woodpecker/manifest.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
skip_clone: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: manifest
|
||||||
|
image: mirror.gcr.io/mplatform/manifest-tool:alpine-v2.1.6
|
||||||
|
secrets:
|
||||||
|
- docker_username
|
||||||
|
- docker_password
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
cat << EOF > spec.yaml
|
||||||
|
image: "harbor.k-space.ee/${CI_REPO}:latest"
|
||||||
|
manifests:
|
||||||
|
- image: "harbor.k-space.ee/${CI_REPO}:latest-amd64"
|
||||||
|
platform:
|
||||||
|
architecture: amd64
|
||||||
|
os: linux
|
||||||
|
- image: "harbor.k-space.ee/${CI_REPO}:latest-arm64"
|
||||||
|
platform:
|
||||||
|
architecture: arm64
|
||||||
|
os: linux
|
||||||
|
EOF
|
||||||
|
- /manifest-tool --username $docker_username --password $docker_password push from-spec spec.yaml > stdout
|
||||||
|
- cat stdout
|
||||||
|
when:
|
||||||
|
- branch: master
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM mirror.gcr.io/library/golang:1.25-alpine AS build
|
||||||
|
RUN apk add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /godoor
|
||||||
|
|
||||||
|
# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# fully static build (takes ~300s to build):
|
||||||
|
ENV GOBUILDLDFLAGS="-linkmode 'external' -extldflags '-static'"
|
||||||
|
RUN go build -tags netgo .
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
WORKDIR /
|
||||||
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=build /godoor/godoor /godoor
|
||||||
|
|
||||||
|
ENTRYPOINT ["/godoor"]
|
||||||
2
Makefile
2
Makefile
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
GOBUILDLDFLAGS="-X main.Version=`git describe --dirty`"
|
GOBUILDLDFLAGS="-X main.Version=`git describe --always --dirty`"
|
||||||
#GOBUILDLDFLAGS="-X main.Version=`git describe --dirty` -linkmode 'external' -extldflags '-static'"
|
#GOBUILDLDFLAGS="-X main.Version=`git describe --dirty` -linkmode 'external' -extldflags '-static'"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
65
README.md
65
README.md
@@ -1,14 +1,65 @@
|
|||||||
# GoDoor
|
# GoDoor
|
||||||
|
|
||||||
[kdoorpi](https://git.k-space.ee/arti/kdoorpi) but in Go
|
Go rewrite of [kdoorpi](https://git.k-space.ee/arti/kdoorpi). Uses
|
||||||
|
[libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about/)
|
||||||
|
native Go library to access the general purpose pins in cross platform way.
|
||||||
|
|
||||||
Uses Go native [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about/)
|
# Developing
|
||||||
library to access the gpio pins in cross platform way.
|
|
||||||
|
|
||||||
# Cross compile
|
Either build directly on Raspberry Pi or build locally and copy the binary
|
||||||
|
|
||||||
GOOS=linux GOARCH=arm64 go build
|
```
|
||||||
|
GOOS=linux GOARCH=arm64 go build
|
||||||
|
scp godoor rpi4b:
|
||||||
|
```
|
||||||
|
|
||||||
# Test deploy
|
# Deployment
|
||||||
|
|
||||||
scp godoor rpi4b:
|
The CI system should automatically build docker image and push it to [Harbor](https://harbor.k-space.ee)
|
||||||
|
|
||||||
|
Refer to
|
||||||
|
[Ansible playbook](https://git.k-space.ee/k-space/kube/src/branch/master/ansible-doors.yml)
|
||||||
|
on how to deploy on all door controllers
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
For end users door can be opened by:
|
||||||
|
|
||||||
|
* [Web interface](https://inventory.k-space.ee/m/doorboy)
|
||||||
|
* Push button
|
||||||
|
* Enrolling keyfob and swiping keyfob at door cardreader
|
||||||
|
* Issuing `/open-...-door` command in #members channel
|
||||||
|
|
||||||
|
For emergency cases:
|
||||||
|
|
||||||
|
* Log into door controller via SSH and issue `killall -sUSR1 godoor`
|
||||||
|
|
||||||
|
# Door system architecture
|
||||||
|
|
||||||
|
* Web UI / Slack commands - https://git.k-space.ee/k-space/inventory-app
|
||||||
|
* Card List provider - https://git.k-space.ee/k-space/doorboy-proxy
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
┌──────────────────┐ │ Card Reader │ ...
|
||||||
|
│ Slack /open-door │ └───────┬──────┘
|
||||||
|
└─────────────────┬┘ │
|
||||||
|
│ ┌──────────────┐
|
||||||
|
┌──────────────┐ │ ┌────│ RPI/w godoor │ ...
|
||||||
|
│ Web Open door│ │ │ └──────────────┘
|
||||||
|
└─────────┬────┘ │ │Swipe ▲
|
||||||
|
│ │ │Event │ Open event
|
||||||
|
▼ ▼ ▼ │ Card list
|
||||||
|
┌───────────────┐ ┌─────────┴─────┐
|
||||||
|
│ inventory-app │ │ doorboy-proxy │
|
||||||
|
│ doorboy.py │ └───────────────┘
|
||||||
|
└───────────────┘ ▲
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ ┌───────────┐
|
||||||
|
└──────────►│ MongoDB │
|
||||||
|
└───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
https://asciiflow.com/ diagram
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -1,12 +1,11 @@
|
|||||||
module godoor
|
module godoor
|
||||||
|
|
||||||
go 1.18
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/prometheus/client_golang v1.16.0
|
github.com/prometheus/client_golang v1.16.0
|
||||||
github.com/warthog618/gpiod v0.8.0
|
github.com/warthog618/gpiod v0.8.2
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
golang.org/x/crypto v0.12.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -14,9 +13,9 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.4.0 // indirect
|
||||||
github.com/prometheus/common v0.42.0 // indirect
|
github.com/prometheus/common v0.44.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.11.1 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
43
go.sum
43
go.sum
@@ -3,38 +3,43 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/pilebones/go-udev v0.0.0-20180820235104-043677e09b13 h1:Y+ynP+0QIjUejN2tsuIlWOJG1CThJy6amRuWlBL94Vg=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/warthog618/gpiod v0.8.0 h1:qxH9XVvWHpTxzWFSndBcujFyNH5zVRzHM63tcmm85o4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/warthog618/gpiod v0.8.0/go.mod h1:a7Csa+IJtDBZ39++zC/6Srjo01qWejt/5velrDWuNkY=
|
github.com/warthog618/go-gpiosim v0.1.0 h1:2rTMTcKUVZxpUuvRKsagnKAbKpd3Bwffp87xywEDVGI=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
github.com/warthog618/go-gpiosim v0.1.0/go.mod h1:Ngx/LYI5toxHr4E+Vm6vTgCnt0of0tktsSuMUEJ2wCI=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
github.com/warthog618/gpiod v0.8.2 h1:2HgQ9pNowPp7W77sXhX5ut5Tqq1WoS3t7bXYDxtYvxc=
|
||||||
|
github.com/warthog618/gpiod v0.8.2/go.mod h1:O7BNpHjCn/4YS5yFVmoFZAlY1LuYuQ8vhPf0iy/qdi4=
|
||||||
|
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||||
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
313
godoor.go
313
godoor.go
@@ -6,10 +6,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -22,31 +18,31 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
"godoor/hash"
|
"godoor/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
const wiegand_a_default = 17
|
const (
|
||||||
const wiegand_b_default = 18
|
wiegand_a_default = 17
|
||||||
const wiegand_bit_timeout = time.Millisecond * 8
|
wiegand_b_default = 18
|
||||||
const solenoid_default = 21
|
wiegand_bit_timeout = time.Millisecond * 8
|
||||||
|
solenoid_default = 21
|
||||||
|
)
|
||||||
|
|
||||||
type card struct {
|
type upstreamUpdate struct {
|
||||||
UidHash string `json:"uid_hash"`
|
AllowedHashes []string `json:"allowed_hashes"`
|
||||||
}
|
|
||||||
|
|
||||||
type cardList struct {
|
|
||||||
AllowedUids []struct {
|
|
||||||
Token card `json:"token"`
|
|
||||||
} `json:"allowed_uids"`
|
|
||||||
KeepOpenUntil *time.Time `json:"keep_open_until,omitempty"`
|
KeepOpenUntil *time.Time `json:"keep_open_until,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidUids map[string]bool // bool has no meaning
|
type ValidHashesT map[string]bool // bool has no meaning
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
door string
|
door string
|
||||||
uidSalt string
|
uidSalt string
|
||||||
doorOpenTime int
|
doorOpenTime time.Duration
|
||||||
mock bool
|
mock bool
|
||||||
api struct {
|
api struct {
|
||||||
allowed string
|
allowed string
|
||||||
@@ -86,11 +82,13 @@ var Commit = func() string {
|
|||||||
|
|
||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
var config Config
|
var (
|
||||||
var globalLock sync.Mutex
|
config Config
|
||||||
var validUids ValidUids
|
VALID_HASHES_LOCK sync.Mutex
|
||||||
var wiegand Wiegand
|
VALID_HASHES = make(ValidHashesT)
|
||||||
var keepDoorOpen KeepDoorOpen
|
wiegand Wiegand
|
||||||
|
keepDoorOpen KeepDoorOpen
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
godoorBuildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
godoorBuildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
@@ -125,11 +123,12 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
loadConfig()
|
loadConfig()
|
||||||
|
log.Printf("Door name: %s\n", config.door)
|
||||||
|
|
||||||
godoorBuildInfo.WithLabelValues(Version, Commit).Set(1)
|
godoorBuildInfo.WithLabelValues(Version, Commit).Set(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
setup()
|
setup(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -141,22 +140,22 @@ func main() {
|
|||||||
func loadConfig() {
|
func loadConfig() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
log.Println("Loading .env config")
|
|
||||||
err = godotenv.Load()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to load .env config, using internal defaults")
|
|
||||||
}
|
|
||||||
|
|
||||||
config.door = os.Getenv("KDOORPI_DOOR")
|
config.door = os.Getenv("KDOORPI_DOOR")
|
||||||
config.api.allowed = os.Getenv("KDOORPI_API_ALLOWED")
|
config.api.allowed = os.Getenv("KDOORPI_API_ALLOWED")
|
||||||
config.api.longpoll = os.Getenv("KDOORPI_API_LONGPOLL")
|
config.api.longpoll = os.Getenv("KDOORPI_API_LONGPOLL")
|
||||||
config.api.swipe = os.Getenv("KDOORPI_API_SWIPE")
|
config.api.swipe = os.Getenv("KDOORPI_API_SWIPE")
|
||||||
config.api.key = os.Getenv("KDOORPI_API_KEY")
|
config.api.key = os.Getenv("KDOORPI_API_KEY")
|
||||||
config.uidSalt = os.Getenv("KDOORPI_UID_SALT")
|
config.uidSalt = os.Getenv("KDOORPI_UID_SALT")
|
||||||
config.doorOpenTime, err = strconv.Atoi(os.Getenv("KDOORPI_OPEN_TIME"))
|
|
||||||
if err != nil {
|
config.doorOpenTime = 8 * time.Second
|
||||||
config.doorOpenTime = 3
|
if doorOpenTimeStr, ok := os.LookupEnv("KDOORPI_OPEN_TIME"); ok {
|
||||||
|
if openTime, err := time.ParseDuration(doorOpenTimeStr); err != nil {
|
||||||
|
log.Printf("parsing KDOORPI_OPEN_TIME: %v, keeping default %v", err, config.doorOpenTime)
|
||||||
|
} else {
|
||||||
|
config.doorOpenTime = openTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, config.mock = os.LookupEnv("KDOORPI_MOCK_HW")
|
_, config.mock = os.LookupEnv("KDOORPI_MOCK_HW")
|
||||||
config.prometheusMetricsBind = os.Getenv("KDOORPI_PROMETHEUS_METRICS_BIND")
|
config.prometheusMetricsBind = os.Getenv("KDOORPI_PROMETHEUS_METRICS_BIND")
|
||||||
if config.prometheusMetricsBind == "" {
|
if config.prometheusMetricsBind == "" {
|
||||||
@@ -177,9 +176,7 @@ func loadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup() {
|
func setup(ctx context.Context) {
|
||||||
log.Println("Started Setup")
|
|
||||||
|
|
||||||
if config.mock {
|
if config.mock {
|
||||||
log.Println("MOCK mode enabled")
|
log.Println("MOCK mode enabled")
|
||||||
if config.door == "" {
|
if config.door == "" {
|
||||||
@@ -195,30 +192,9 @@ func setup() {
|
|||||||
|
|
||||||
http.DefaultClient.Timeout = 120 * time.Second
|
http.DefaultClient.Timeout = 120 * time.Second
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
err := waitEvents()
|
|
||||||
if err != nil {
|
|
||||||
apiFailuresCount.WithLabelValues("longpoll", config.api.longpoll).Inc()
|
|
||||||
log.Printf("LongPoll for events failed: %v", err)
|
|
||||||
log.Println("Will try to LongPoll again in 120 seconds")
|
|
||||||
time.Sleep(120 * time.Second)
|
|
||||||
go func() {
|
|
||||||
err := reloadTokens()
|
|
||||||
if err != nil {
|
|
||||||
apiFailuresCount.WithLabelValues("allowed", config.api.allowed).Inc()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Println("Initialized longpoll event loop")
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
log.Println("Start initial token population")
|
log.Println("Start initial token population")
|
||||||
err := reloadTokens()
|
err := reloadInfo()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -230,11 +206,94 @@ func setup() {
|
|||||||
|
|
||||||
log.Println("Initial token population success")
|
log.Println("Initial token population success")
|
||||||
|
|
||||||
|
allowedPollInterval := 15 * time.Second
|
||||||
|
if intervalStr, ok := os.LookupEnv("KDOORPI_ALLOWED_POLL_INTERVAL"); ok {
|
||||||
|
if interval, err := time.ParseDuration(intervalStr); err != nil {
|
||||||
|
log.Printf("parsing KDOORPI_ALLOWED_POLL_INTERVAL: %v, keeping default %v", err, allowedPollInterval)
|
||||||
|
} else if interval <= 0 {
|
||||||
|
// time.NewTicker panics on a non-positive duration, so a malformed
|
||||||
|
// setting like "0" or "-1s" would crash the controller. Keep the
|
||||||
|
// safe default instead.
|
||||||
|
log.Printf("KDOORPI_ALLOWED_POLL_INTERVAL must be positive, got %v, keeping default %v", interval, allowedPollInterval)
|
||||||
|
} else {
|
||||||
|
allowedPollInterval = interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(allowedPollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
pollAllowedOnce()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
err := waitEvents()
|
||||||
|
if err != nil {
|
||||||
|
apiFailuresCount.WithLabelValues("longpoll", config.api.longpoll).Inc()
|
||||||
|
log.Printf("LongPoll for events failed: %v", err)
|
||||||
|
log.Println("Will try to LongPoll again soon")
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
go func() {
|
||||||
|
err := reloadInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("reloadTokens failed: %q", err)
|
||||||
|
apiFailuresCount.WithLabelValues("allowed", config.api.allowed).Inc()
|
||||||
|
// Intentionally no cancelKeepOpenDoor() here: the hold
|
||||||
|
// fail-safe is owned by the fixed-interval periodic poll
|
||||||
|
// above. Cancelling from this best-effort, per-iteration
|
||||||
|
// refresh too would only add door flapping. Worst-case
|
||||||
|
// stuck-open after proxy loss is ~reloadInfoTimeout + one
|
||||||
|
// poll interval (a hung-but-connected proxy blocks the
|
||||||
|
// in-flight reloadInfo until its request timeout).
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go listenSig1(ctx, wiegand)
|
||||||
|
|
||||||
|
log.Println("Initialized longpoll event loop")
|
||||||
|
|
||||||
go cardRunner(wiegand)
|
go cardRunner(wiegand)
|
||||||
|
|
||||||
log.Println("Setup completed")
|
log.Println("Setup completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pollAllowedOnce() {
|
||||||
|
err := reloadInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Periodic reloadInfo failed: %v", err)
|
||||||
|
apiFailuresCount.WithLabelValues("allowed", config.api.allowed).Inc()
|
||||||
|
// Fail safe: we can no longer confirm the door should stay
|
||||||
|
// held open, so close it now rather than trusting a stale
|
||||||
|
// deadline (up to 6h away). A later successful poll re-opens
|
||||||
|
// it if the hold is still active server-side.
|
||||||
|
cancelKeepOpenDoor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenSig1(ctx context.Context, wiegand Wiegand) {
|
||||||
|
usrSig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(usrSig, syscall.SIGUSR1)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-usrSig:
|
||||||
|
err := OpenAndCloseDoor(wiegand)
|
||||||
|
log.Printf("Emergecy opening door as prompted by SIGUSR1")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("opening door: %v", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runHttpServer() {
|
func runHttpServer() {
|
||||||
http.Handle("/metrics", promhttp.Handler())
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
log.Printf("Running prometheus metrics on http://%s/metrics", config.prometheusMetricsBind)
|
log.Printf("Running prometheus metrics on http://%s/metrics", config.prometheusMetricsBind)
|
||||||
@@ -247,14 +306,30 @@ func OpenAndCloseDoor(w Wiegand) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if keepDoorOpen.until.After(time.Now()) {
|
keepDoorOpenLock.Lock()
|
||||||
|
keepOpenUntil := keepDoorOpen.until
|
||||||
|
keepDoorOpenLock.Unlock()
|
||||||
|
if keepOpenUntil.After(time.Now()) {
|
||||||
fmt.Println("Door is already open")
|
fmt.Println("Door is already open")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Door is now open")
|
fmt.Println("Door is now open")
|
||||||
|
|
||||||
time.Sleep(time.Duration(config.doorOpenTime) * time.Second)
|
time.Sleep(config.doorOpenTime)
|
||||||
|
|
||||||
|
// A hold may have been armed while we were sleeping. Re-check under the
|
||||||
|
// lock and skip the close if so, holding the lock across the close decision
|
||||||
|
// so an in-flight updateKeepOpenDoor cannot slip a hold in between the
|
||||||
|
// check and the close. Otherwise this pulse would close a held-open door
|
||||||
|
// and subsequent polls, seeing the timer as already valid, would never
|
||||||
|
// reopen it, locking the door for the whole hold duration.
|
||||||
|
keepDoorOpenLock.Lock()
|
||||||
|
defer keepDoorOpenLock.Unlock()
|
||||||
|
if keepDoorOpen.until.After(time.Now()) {
|
||||||
|
fmt.Println("Door is held open, leaving open")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err = CloseDoor(w)
|
err = CloseDoor(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -270,8 +345,7 @@ func OpenDoor(w Wiegand) error {
|
|||||||
if open {
|
if open {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w.OpenDoor()
|
return w.OpenDoor()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloseDoor(w Wiegand) error {
|
func CloseDoor(w Wiegand) error {
|
||||||
@@ -279,8 +353,7 @@ func CloseDoor(w Wiegand) error {
|
|||||||
if !open {
|
if !open {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w.CloseDoor()
|
return w.CloseDoor()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cardRunner(w Wiegand) {
|
func cardRunner(w Wiegand) {
|
||||||
@@ -290,19 +363,19 @@ func cardRunner(w Wiegand) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
printCardId(card)
|
// printCardId(card)
|
||||||
hashedHex := hash.HashCardUid(card)
|
hashedHex := hash.HashCardUid(card)
|
||||||
log.Println(hashedHex)
|
log.Println(hashedHex)
|
||||||
|
|
||||||
globalLock.Lock()
|
VALID_HASHES_LOCK.Lock()
|
||||||
ok := validUids[hashedHex]
|
ok := VALID_HASHES[hashedHex]
|
||||||
globalLock.Unlock()
|
VALID_HASHES_LOCK.Unlock()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := sendSwipeEvent(hashedHex, ok)
|
err := sendSwipeEvent(hashedHex, ok)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiFailuresCount.WithLabelValues("swipe", config.api.swipe).Inc()
|
apiFailuresCount.WithLabelValues("swipe", config.api.swipe).Inc()
|
||||||
log.Println("Failed to send swipe event: %v", err)
|
log.Printf("Failed to send swipe event: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -343,7 +416,6 @@ func ParseNextMessage(r *bufio.Reader) (string, error) {
|
|||||||
|
|
||||||
r.UnreadByte()
|
r.UnreadByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitEvents() error {
|
func waitEvents() error {
|
||||||
@@ -369,7 +441,7 @@ func waitEvents() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range strings.Split(msg, "\n") {
|
for _, line := range strings.Split(msg, "\n") {
|
||||||
data, found_data := strings.CutPrefix(line, "data:")
|
data, found_data := strings.CutPrefix(line, "data: ")
|
||||||
if !found_data {
|
if !found_data {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -384,51 +456,76 @@ func waitEvents() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%v\n", resp)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadTokens() error {
|
// reloadInfoLock serializes reloadInfo so the periodic-poll and longpoll-driven
|
||||||
req, err := http.NewRequest(http.MethodGet, config.api.allowed, nil)
|
// callers cannot apply /allowed responses out of order (a slow stale response
|
||||||
|
// must not overwrite a newer one and, e.g., cancel a just-armed hold).
|
||||||
|
var reloadInfoLock sync.Mutex
|
||||||
|
|
||||||
|
// reloadInfoTimeout bounds a single /allowed fetch. The shared http client has a
|
||||||
|
// 120s timeout (needed by the longpoll), but the allow-list/keep-open fetch must
|
||||||
|
// be quick: a hung-but-connected proxy otherwise blocks the fail-safe close for
|
||||||
|
// up to 120s. This caps the worst-case stuck-open window to ~this + one interval.
|
||||||
|
const reloadInfoTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
func reloadInfo() error {
|
||||||
|
reloadInfoLock.Lock()
|
||||||
|
defer reloadInfoLock.Unlock()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), reloadInfoTimeout)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, config.api.allowed, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Add("KEY", config.api.key)
|
req.Header.Add("KEY", config.api.key)
|
||||||
|
req.Header.Add("DOOR_NAME", config.door)
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
log.Printf("%v\n", resp)
|
log.Printf("%v\n", resp)
|
||||||
|
// Treat a non-200 as a failure rather than parsing a possibly-cached or
|
||||||
|
// error body: otherwise a stale future keep_open_until would defeat the
|
||||||
|
// fail-safe, or a body lacking it would wrongly cancel a live hold. The
|
||||||
|
// caller's error path then drives the documented fail-safe close.
|
||||||
|
return fmt.Errorf("allowed endpoint returned status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cl cardList
|
var info upstreamUpdate
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, &cl)
|
err = json.Unmarshal(body, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
globalLock.Lock()
|
// Update Allowed hashes
|
||||||
defer globalLock.Unlock()
|
validHashesPre := make(ValidHashesT)
|
||||||
validUids = make(ValidUids)
|
for _, hash := range info.AllowedHashes {
|
||||||
var totalCardCount int = 0
|
validHashesPre[hash] = true
|
||||||
for i, val := range cl.AllowedUids {
|
|
||||||
//log.Printf("%d: %+v\n", i, val.Token.UidHash)
|
|
||||||
validUids[val.Token.UidHash] = true
|
|
||||||
totalCardCount = i
|
|
||||||
}
|
}
|
||||||
log.Printf("Got %d cards from server", totalCardCount)
|
|
||||||
nrCardsInAllowList.Set(float64(totalCardCount))
|
|
||||||
|
|
||||||
if cl.KeepOpenUntil != nil {
|
VALID_HASHES_LOCK.Lock()
|
||||||
updateKeepOpenDoor(*cl.KeepOpenUntil)
|
VALID_HASHES = validHashesPre
|
||||||
|
VALID_HASHES_LOCK.Unlock()
|
||||||
|
|
||||||
|
log.Printf("Got %d cards from server", len(info.AllowedHashes))
|
||||||
|
nrCardsInAllowList.Set(float64(len(info.AllowedHashes)))
|
||||||
|
|
||||||
|
// Update Keep open
|
||||||
|
|
||||||
|
if info.KeepOpenUntil != nil {
|
||||||
|
updateKeepOpenDoor(*info.KeepOpenUntil)
|
||||||
|
} else {
|
||||||
|
cancelKeepOpenDoor()
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSyncTimestamp.SetToCurrentTime()
|
lastSyncTimestamp.SetToCurrentTime()
|
||||||
@@ -436,39 +533,12 @@ func reloadTokens() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateKeepOpenDoor(newKeepOpenTime time.Time) {
|
func sendSwipeEvent(cardUidHash string, approved bool) error {
|
||||||
|
swipeEvent := map[string]any{
|
||||||
// is there one active?
|
|
||||||
if keepDoorOpen.timer != nil {
|
|
||||||
keepDoorOpen.timer.Stop()
|
|
||||||
keepDoorOpen = KeepDoorOpen{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newKeepOpenTime.After(time.Now()) {
|
|
||||||
log.Printf("Keeping door open until %v", newKeepOpenTime)
|
|
||||||
OpenDoor(wiegand)
|
|
||||||
timer := time.AfterFunc(time.Until(newKeepOpenTime), handleKeepDoorOpenCloseCleanup)
|
|
||||||
keepDoorOpen = KeepDoorOpen{
|
|
||||||
timer: timer,
|
|
||||||
until: newKeepOpenTime,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CloseDoor(wiegand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleKeepDoorOpenCloseCleanup() {
|
|
||||||
fmt.Println("Keep door open time is reached!")
|
|
||||||
CloseDoor(wiegand)
|
|
||||||
keepDoorOpen = KeepDoorOpen{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendSwipeEvent(cardUidHash string, success bool) error {
|
|
||||||
swipeEvent := map[string]string{
|
|
||||||
"uid_hash": cardUidHash,
|
"uid_hash": cardUidHash,
|
||||||
"door": config.door,
|
"door": config.door,
|
||||||
"timestamp": time.Now().Format(time.DateTime),
|
"timestamp": time.Now().Format(time.RFC3339),
|
||||||
"success": fmt.Sprint(success),
|
"approved": approved,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(swipeEvent)
|
data, err := json.Marshal(swipeEvent)
|
||||||
@@ -481,6 +551,7 @@ func sendSwipeEvent(cardUidHash string, success bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Add("KEY", config.api.key)
|
req.Header.Add("KEY", config.api.key)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
7
godoor_server/README.md
Normal file
7
godoor_server/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Local Development Mock server
|
||||||
|
|
||||||
|
Just for simple local testing
|
||||||
|
|
||||||
|
# NOT TO BE USED IN PROD!
|
||||||
|
|
||||||
|
Move it into a seperate repo if before making it more useful
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
// really stupid test door server
|
// really stupid test door server
|
||||||
|
// Only for local testing
|
||||||
|
// I WIL HUNT YOU DOWN IT YOU TRY TO USE IT IN PROD!
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -217,7 +219,7 @@ func (doobserver *DoorBoyServer) postKeepDoorOpen(w http.ResponseWriter, r *http
|
|||||||
rs, _ := io.ReadAll(r.Body)
|
rs, _ := io.ReadAll(r.Body)
|
||||||
parsedTime, err := time.Parse(time.RFC3339, strings.TrimSpace(string(rs)))
|
parsedTime, err := time.Parse(time.RFC3339, strings.TrimSpace(string(rs)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error with parsing time: %v", err)
|
fmt.Printf("Error with parsing time: %v", err)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
keepOpenUntil = &parsedTime
|
keepOpenUntil = &parsedTime
|
||||||
@@ -229,6 +231,9 @@ func main() {
|
|||||||
|
|
||||||
var dumpKeys []cardToken
|
var dumpKeys []cardToken
|
||||||
savedKeys, err := os.ReadFile("keys.json")
|
savedKeys, err := os.ReadFile("keys.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading saved keys from keys.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(savedKeys, &dumpKeys)
|
err = json.Unmarshal(savedKeys, &dumpKeys)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -268,7 +273,7 @@ func main() {
|
|||||||
keyLock.Lock()
|
keyLock.Lock()
|
||||||
data, err := json.Marshal(keys)
|
data, err := json.Marshal(keys)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
os.WriteFile("keys.json", data, 0600)
|
os.WriteFile("keys.json", data, 0o600)
|
||||||
log.Println("Saved keys successfully!")
|
log.Println("Saved keys successfully!")
|
||||||
}
|
}
|
||||||
keyLock.Unlock()
|
keyLock.Unlock()
|
||||||
|
|||||||
97
keepopen.go
Normal file
97
keepopen.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var keepDoorOpenLock sync.Mutex
|
||||||
|
|
||||||
|
// keepDoorOpenGen identifies the currently armed hold timer. It is bumped (under
|
||||||
|
// keepDoorOpenLock) every time a new timer is installed so that a superseded
|
||||||
|
// timer's callback can recognise it is stale and do nothing.
|
||||||
|
var keepDoorOpenGen uint64
|
||||||
|
|
||||||
|
func updateKeepOpenDoor(newKeepOpenTime time.Time) {
|
||||||
|
keepDoorOpenLock.Lock()
|
||||||
|
defer keepDoorOpenLock.Unlock()
|
||||||
|
|
||||||
|
// Hold unchanged since the last poll: keep the existing timer rather than
|
||||||
|
// rebuilding it every poll, which would float the close time forward by up
|
||||||
|
// to one poll interval and re-pulse OpenDoor needlessly.
|
||||||
|
if keepDoorOpen.timer != nil && newKeepOpenTime.Equal(keepDoorOpen.until) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// is there one active?
|
||||||
|
if keepDoorOpen.timer != nil {
|
||||||
|
keepDoorOpen.timer.Stop()
|
||||||
|
keepDoorOpen = KeepDoorOpen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newKeepOpenTime.After(time.Now()) {
|
||||||
|
log.Printf("Keeping door open until %v", newKeepOpenTime)
|
||||||
|
if err := OpenDoor(wiegand); err != nil {
|
||||||
|
// Don't commit the hold if the relay didn't actually open: leaving
|
||||||
|
// keepDoorOpen empty (timer nil) means the next poll retries instead
|
||||||
|
// of latching the Equal early-return on a door that never opened.
|
||||||
|
log.Printf("ERROR opening door for hold: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keepDoorOpenGen++
|
||||||
|
gen := keepDoorOpenGen
|
||||||
|
timer := time.AfterFunc(time.Until(newKeepOpenTime), func() {
|
||||||
|
handleKeepDoorOpenCloseCleanup(gen)
|
||||||
|
})
|
||||||
|
keepDoorOpen = KeepDoorOpen{
|
||||||
|
timer: timer,
|
||||||
|
until: newKeepOpenTime,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := CloseDoor(wiegand); err != nil {
|
||||||
|
log.Printf("ERROR closing door: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelKeepOpenDoor() {
|
||||||
|
keepDoorOpenLock.Lock()
|
||||||
|
defer keepDoorOpenLock.Unlock()
|
||||||
|
|
||||||
|
if keepDoorOpen.timer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keepDoorOpen.timer.Stop()
|
||||||
|
if err := CloseDoor(wiegand); err != nil {
|
||||||
|
// Keep keepDoorOpen non-nil so the next poll's cancel retries the close;
|
||||||
|
// clearing it now would strand the door OPEN with no retry path.
|
||||||
|
log.Printf("ERROR closing door on hold cancel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keepDoorOpen = KeepDoorOpen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleKeepDoorOpenCloseCleanup(gen uint64) {
|
||||||
|
keepDoorOpenLock.Lock()
|
||||||
|
defer keepDoorOpenLock.Unlock()
|
||||||
|
|
||||||
|
// Timer.Stop() can return after this callback has already started and is
|
||||||
|
// blocked here on the lock; by the time we acquire it, updateKeepOpenDoor
|
||||||
|
// may have installed a newer hold. Only act if we are still the current
|
||||||
|
// generation, otherwise we would close the door and clear a live hold.
|
||||||
|
if gen != keepDoorOpenGen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Keep door open time is reached!")
|
||||||
|
if err := CloseDoor(wiegand); err != nil {
|
||||||
|
// Leave keepDoorOpen intact so the next poll (which will see
|
||||||
|
// keep_open_until=null and call cancelKeepOpenDoor) retries the close
|
||||||
|
// instead of stranding the door OPEN.
|
||||||
|
log.Printf("ERROR closing door at hold expiry: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keepDoorOpen = KeepDoorOpen{}
|
||||||
|
}
|
||||||
22
wiegand.go
22
wiegand.go
@@ -57,6 +57,7 @@ func (w *WiegandHW) IsDoorOpen() (bool, error) {
|
|||||||
defer w.lock.RUnlock()
|
defer w.lock.RUnlock()
|
||||||
return w.isDoorOpen()
|
return w.isDoorOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WiegandHW) isDoorOpen() (bool, error) {
|
func (w *WiegandHW) isDoorOpen() (bool, error) {
|
||||||
i, err := w.solenoidLine.Value()
|
i, err := w.solenoidLine.Value()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,19 +91,18 @@ func (w *WiegandHW) GetCardUid() (uint64, error) {
|
|||||||
func (w *WiegandHW) wiegandAEvent(evt gpiod.LineEvent) {
|
func (w *WiegandHW) wiegandAEvent(evt gpiod.LineEvent) {
|
||||||
w.bitTimeoutTimer.Reset(w.bitTimeout)
|
w.bitTimeoutTimer.Reset(w.bitTimeout)
|
||||||
w.bits[w.bitNr] = true
|
w.bits[w.bitNr] = true
|
||||||
//fmt.Printf("1")
|
// fmt.Printf("1")
|
||||||
w.bitNr += 1
|
w.bitNr += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WiegandHW) wiegandBEvent(evt gpiod.LineEvent) {
|
func (w *WiegandHW) wiegandBEvent(evt gpiod.LineEvent) {
|
||||||
w.bitTimeoutTimer.Reset(w.bitTimeout)
|
w.bitTimeoutTimer.Reset(w.bitTimeout)
|
||||||
w.bits[w.bitNr] = false
|
w.bits[w.bitNr] = false
|
||||||
//fmt.Printf("0")
|
// fmt.Printf("0")
|
||||||
w.bitNr += 1
|
w.bitNr += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *WiegandHW {
|
func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *WiegandHW {
|
||||||
|
|
||||||
log.Printf("Wiegand GPIO-s: A:%d B:%d Solenoid:%d", a, b, solenoid)
|
log.Printf("Wiegand GPIO-s: A:%d B:%d Solenoid:%d", a, b, solenoid)
|
||||||
var wiegand WiegandHW
|
var wiegand WiegandHW
|
||||||
wiegand.bitTimeout = bitTimeout
|
wiegand.bitTimeout = bitTimeout
|
||||||
@@ -113,14 +113,14 @@ func WiegandSetup(a int, b int, bitTimeout time.Duration, solenoid int) *Wiegand
|
|||||||
}
|
}
|
||||||
|
|
||||||
wa, err := gpiod.RequestLine("gpiochip0", a, gpiod.AsInput,
|
wa, err := gpiod.RequestLine("gpiochip0", a, gpiod.AsInput,
|
||||||
gpiod.WithFallingEdge, gpiod.WithEventHandler(wiegand.wiegandAEvent))
|
gpiod.WithFallingEdge, gpiod.LineBiasPullUp, gpiod.WithEventHandler(wiegand.wiegandAEvent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
wiegand.aLine = wa
|
wiegand.aLine = wa
|
||||||
|
|
||||||
wb, err := gpiod.RequestLine("gpiochip0", b, gpiod.AsInput,
|
wb, err := gpiod.RequestLine("gpiochip0", b, gpiod.AsInput,
|
||||||
gpiod.WithFallingEdge, gpiod.WithEventHandler(wiegand.wiegandBEvent))
|
gpiod.WithFallingEdge, gpiod.LineBiasPullUp, gpiod.WithEventHandler(wiegand.wiegandBEvent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -141,9 +141,9 @@ func (w *WiegandHW) WiegandClose() {
|
|||||||
w.solenoidLine.Close()
|
w.solenoidLine.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func printCardId(card uint64) {
|
// func printCardId(card uint64) {
|
||||||
for i := 0; i < 7; i++ {
|
// for i := 0; i < 7; i++ {
|
||||||
fmt.Printf("%02x", (card>>(8*i))&0xff)
|
// fmt.Printf("%02x", (card>>(8*i))&0xff)
|
||||||
}
|
// }
|
||||||
fmt.Printf("\n")
|
// fmt.Printf("\n")
|
||||||
}
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user