173 lines
4.4 KiB
Go
173 lines
4.4 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"flag"
|
||
|
|
"log"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"os/signal"
|
||
|
|
"sync"
|
||
|
|
"syscall"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"frame"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
workerCount = 4
|
||
|
|
|
||
|
|
// connLimit bounds concurrent inbound connections so a connection flood
|
||
|
|
// (auth handshake parked, or clips queued behind busy workers) cannot
|
||
|
|
// exhaust goroutines or file descriptors.
|
||
|
|
connLimit = 16
|
||
|
|
|
||
|
|
// clipChanCap gives in-flight ingest handlers somewhere to park a decoded
|
||
|
|
// clip without holding the connection's socket open for the workers.
|
||
|
|
clipChanCap = 32
|
||
|
|
|
||
|
|
shutdownGrace = 5 * time.Second
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
var (
|
||
|
|
listenAddr string
|
||
|
|
terpAddr string
|
||
|
|
threshold int
|
||
|
|
passphrase string
|
||
|
|
authToken string
|
||
|
|
pbkdf2Iter int
|
||
|
|
)
|
||
|
|
|
||
|
|
flag.StringVar(&listenAddr, "listen", ":8083", "address to listen for incoming clips")
|
||
|
|
flag.StringVar(&terpAddr, "terp", "localhost:8081", "address to forward clips with motion to terp")
|
||
|
|
flag.IntVar(&threshold, "threshold", 30, "pixel-value sensitivity (0-255) for motion detection comparison")
|
||
|
|
flag.StringVar(&passphrase, "passphrase", "", "decryption passphrase (empty = no encryption)")
|
||
|
|
flag.StringVar(&authToken, "auth-token", "", "shared pipeline auth token (or OKO_AUTH_TOKEN)")
|
||
|
|
flag.IntVar(&pbkdf2Iter, "pbkdf2-iters", frame.DefaultPBKDF2Iter, "PBKDF2 iterations for clip encryption (1..1000000)")
|
||
|
|
flag.Parse()
|
||
|
|
|
||
|
|
if threshold < 0 || threshold > 255 {
|
||
|
|
log.Fatalf("threshold must be in [0, 255], got %d", threshold)
|
||
|
|
}
|
||
|
|
if pbkdf2Iter < 1 || pbkdf2Iter > frame.MaxPBKDF2Iter {
|
||
|
|
log.Fatalf("pbkdf2-iters must be in [1, %d], got %d", frame.MaxPBKDF2Iter, pbkdf2Iter)
|
||
|
|
}
|
||
|
|
frame.SetDefaultPBKDF2Iter(pbkdf2Iter)
|
||
|
|
|
||
|
|
tok := authToken
|
||
|
|
if tok == "" {
|
||
|
|
tok = os.Getenv("OKO_AUTH_TOKEN")
|
||
|
|
}
|
||
|
|
if tok == "" {
|
||
|
|
log.Fatal("authentication required: set -auth-token or OKO_AUTH_TOKEN")
|
||
|
|
}
|
||
|
|
frame.SetAuthToken(tok)
|
||
|
|
|
||
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
clipChan := make(chan frame.Clip, clipChanCap)
|
||
|
|
connSem := make(chan struct{}, connLimit)
|
||
|
|
|
||
|
|
ln, err := net.Listen("tcp", listenAddr)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("Listen %s: %v", listenAddr, err)
|
||
|
|
}
|
||
|
|
log.Printf("Listening on %s", listenAddr)
|
||
|
|
|
||
|
|
var acceptWg sync.WaitGroup
|
||
|
|
rejected := frame.NewMalformedAttempts()
|
||
|
|
go func() {
|
||
|
|
<-ctx.Done()
|
||
|
|
ln.Close()
|
||
|
|
}()
|
||
|
|
go func() {
|
||
|
|
for {
|
||
|
|
conn, err := ln.Accept()
|
||
|
|
if err != nil {
|
||
|
|
if ctx.Err() != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
log.Printf("Accept error: %v", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case connSem <- struct{}{}:
|
||
|
|
default:
|
||
|
|
log.Printf("Rejecting connection from %s: too many concurrent connections", conn.RemoteAddr())
|
||
|
|
conn.Close()
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
acceptWg.Add(1)
|
||
|
|
go func(c net.Conn) {
|
||
|
|
defer func() { <-connSem }()
|
||
|
|
defer acceptWg.Done()
|
||
|
|
frame.HandleClipConn(c, rejected, func(clp frame.Clip) {
|
||
|
|
clipChan <- clp
|
||
|
|
})
|
||
|
|
}(conn)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
// Bounded worker pool, so a burst of clips cannot spin up unbounded
|
||
|
|
// goroutines or saturate every core with motion passes.
|
||
|
|
var procWg sync.WaitGroup
|
||
|
|
procWg.Add(workerCount)
|
||
|
|
for w := 0; w < workerCount; w++ {
|
||
|
|
go func() {
|
||
|
|
defer procWg.Done()
|
||
|
|
for clp := range clipChan {
|
||
|
|
processClip(clp, terpAddr, threshold, passphrase)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
<-ctx.Done()
|
||
|
|
log.Println("Shutting down...")
|
||
|
|
ln.Close()
|
||
|
|
if !frame.WaitGroupTimeout(&acceptWg, shutdownGrace) {
|
||
|
|
log.Println("Inbound handlers did not drain; closing channel anyway")
|
||
|
|
}
|
||
|
|
close(clipChan)
|
||
|
|
if !frame.WaitGroupTimeout(&procWg, shutdownGrace) {
|
||
|
|
log.Println("Workers still busy (downstream retry?); exiting anyway")
|
||
|
|
}
|
||
|
|
if n := rejected.Total(); n > 0 {
|
||
|
|
log.Printf("Rejected %d malformed clips from the network", n)
|
||
|
|
}
|
||
|
|
log.Println("Exiting")
|
||
|
|
}
|
||
|
|
|
||
|
|
// processClip decrypts, drops clips with no motion, re-encrypts, and forwards
|
||
|
|
// survivors to terp.
|
||
|
|
func processClip(clp frame.Clip, terpAddr string, threshold int, passphrase string) {
|
||
|
|
if passphrase != "" {
|
||
|
|
if err := clp.Decrypt(passphrase); err != nil {
|
||
|
|
log.Printf("Decrypt error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := clp.CountChangedPixels(uint8(threshold)); err != nil {
|
||
|
|
log.Printf("CountChangedPixels: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if clp.GetHighestMotion() == 0 {
|
||
|
|
log.Printf("Dropped no-motion clip (source %s)", clp.SourceData)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if passphrase != "" {
|
||
|
|
if err := clp.Encrypt(passphrase); err != nil {
|
||
|
|
log.Printf("Encrypt error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := clp.SendClipRetry(terpAddr, 6); err != nil {
|
||
|
|
log.Printf("Forward to terp failed, clip dropped: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
log.Printf("Forwarded clip to terp (motion: %d%%)", clp.GetHighestMotion())
|
||
|
|
}
|