diff --git a/go.mod b/go.mod index 87f27b6..59fe3cf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter go 1.23.4 require ( - gitea.libretechconsulting.com/rmcguire/go-app v0.4.2 + gitea.libretechconsulting.com/rmcguire/go-app v0.5.0 github.com/go-resty/resty/v2 v2.16.5 github.com/gorilla/schema v1.4.1 github.com/rs/zerolog v1.33.0 diff --git a/go.sum b/go.sum index 3d4a75f..112d59c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ gitea.libretechconsulting.com/rmcguire/go-app v0.4.1 h1:gjDg2M/j1AdMCtkXqQnLCo6j gitea.libretechconsulting.com/rmcguire/go-app v0.4.1/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs= gitea.libretechconsulting.com/rmcguire/go-app v0.4.2 h1:LQxVLXEHruY32GaMsS5K/tMdjS5kvw6reUh25gshn40= gitea.libretechconsulting.com/rmcguire/go-app v0.4.2/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs= +gitea.libretechconsulting.com/rmcguire/go-app v0.5.0 h1:5yYyaXXN5KcxMIPBYLZKztvKGMlYol3+oqzUnkvHBaQ= +gitea.libretechconsulting.com/rmcguire/go-app v0.5.0/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= diff --git a/main.go b/main.go index 59280bb..8bd3392 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sys/unix" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient" + "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/ambienthttp" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config" ) @@ -46,6 +47,8 @@ func main() { HandlerFunc: aw.GetAWNHandlerFunc(ctx), }, }, + CustomListener: ambienthttp.NewLFStrippingListener(ctx, + awConfig.HTTP.Listen), // Necessary to fix certain bad AWN firmware HealthChecks: []srv.HealthCheckFunc{ // TODO: Implement func(ctx context.Context) error { diff --git a/pkg/ambient/ambienthttp/ambienthttp.go b/pkg/ambient/ambienthttp/ambienthttp.go new file mode 100644 index 0000000..6517e54 --- /dev/null +++ b/pkg/ambient/ambienthttp/ambienthttp.go @@ -0,0 +1,101 @@ +// This package exists purely to override the net.Listener used +// by the application's http server. This is necessary for certain versions +// of firmware which errantly put an 0x0a (LF) following PASSKEY for +// AmbientWeather type http reporting. +// +// This needs to be fixed upstream by Ambient Weather and is a complete +// hack that should never be necessary. Without this, the http server +// will silently crank back an HTTP:400 +package ambienthttp + +import ( + "bufio" + "bytes" + "context" + "io" + "net" + "regexp" + + "github.com/rs/zerolog" +) + +// Invalid Request Pattern +var badReqURI = regexp.MustCompile(`PASSKEY=[^\n&]{16,}$`) + +// Listener encapsulates LFStrippingConn to perform +// infuriating strip of newline character present after PASSKEY +// sent errantly by specific versions of firmware sending updates +// in AmbientWeather protocol +type LFStrippingListener struct { + ctx context.Context + net.Listener +} + +type LFStrippingConn struct { + net.Conn + reader io.Reader +} + +func (l *LFStrippingListener) WrapConn(conn net.Conn) net.Conn { + buf := new(bytes.Buffer) + reader := io.TeeReader(conn, buf) + + scanner := bufio.NewScanner(reader) + var newData []byte + for scanner.Scan() { + line := scanner.Bytes() + newData = append(newData, line...) + + // Only restore newline if not a bad request + if !badReqURI.Match(line) { + newData = append(newData, '\n') + } + } + if scanner.Err() != nil { + zerolog.Ctx(l.ctx).Err(scanner.Err()).Send() + } + + // Use a multi-reader to prepend the modified request + finalReader := io.MultiReader(bytes.NewReader(newData), conn) + + return &LFStrippingConn{ + Conn: conn, + reader: finalReader, + } +} + +func NewLFStrippingListener(ctx context.Context, listen string) net.Listener { + // Create the underlying TCP listener + rawListener, err := net.Listen("tcp", listen) + if err != nil { + panic(err) + } + return &LFStrippingListener{ + Listener: rawListener, + ctx: ctx, + } +} + +// Accept waits for and returns the next connection to the listener. +func (l *LFStrippingListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return l.WrapConn(conn), nil +} + +// Close closes the listener. +// Any blocked Accept operations will be unblocked and return errors. +func (l *LFStrippingListener) Close() error { + return l.Listener.Close() +} + +// Addr returns the listener's network address. +func (l *LFStrippingListener) Addr() net.Addr { + return l.Listener.Addr() +} + +func (c *LFStrippingConn) Read(b []byte) (int, error) { + return c.reader.Read(b) +}