Files
T
gronod 7bed99cc4c fix: keep robot listeners if health port is busy (v0.1.2)
ServeHealth bind failures (common on HAOS when OTBR owns 8080) are
logged and ignored. Lookup, firmware, and XMPP stay up. Log each
successful listen address so add-on logs show the sockets.
2026-09-24 16:17:30 +00:00

108 lines
2.4 KiB
Go

package httpx
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net"
"net/http"
"time"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
)
// ServeLookup runs the POST /lookup.do listener until ctx is cancelled.
func ServeLookup(ctx context.Context, cfg config.Config) error {
mux := http.NewServeMux()
mux.HandleFunc("/lookup.do", newLookupHandler(cfg))
addr := net.JoinHostPort(cfg.BindAddress, fmt.Sprintf("%d", cfg.PortLookup))
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("%s: %w", addr, err)
}
slog.Info("lookup listening", "addr", addr)
srv := &http.Server{
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.SetKeepAlivesEnabled(false)
errCh := make(chan error, 1)
go func() {
errCh <- srv.Serve(ln)
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
return nil
case err := <-errCh:
return err
}
}
func newLookupHandler(cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Connection", "close")
r.Body = http.MaxBytesReader(w, r.Body, 64*1024)
defer r.Body.Close()
var req lookupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
slog.Debug("lookup request decode failed", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if req.Todo != "FindBest" {
writeLookupFail(w)
return
}
var port int
switch req.Service {
case "EcoMsgNew":
port = cfg.PortXMPP
case "EcoUpdate":
port = cfg.PortFirmware
default:
writeLookupFail(w)
return
}
resp := lookupResponse{Result: "ok", IP: cfg.AdvertiseIP.String(), Port: port}
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(resp)
_, _ = w.Write(data)
}
}
type lookupRequest struct {
Service string `json:"service"`
Todo string `json:"todo"`
}
type lookupResponse struct {
Result string `json:"result"`
IP string `json:"ip"`
Port int `json:"port"`
}
func writeLookupFail(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"result":"fail"}`))
}