From 0e10c9d925be968bb167358dd69a3cadd0419e89 Mon Sep 17 00:00:00 2001 From: Jarno Rankinen Date: Sun, 12 Mar 2023 22:30:53 +0200 Subject: [PATCH] gh-7 Added HTTP basic authentication to REST API. Default credentials pingvin:enervent. Still need to implement logging failed auth requests. A log file option is also a good idea. --- enervent-ctrl-go/main.go | 85 +++++++++++++++++++++------ homeassistant/homeassistant-rest.yaml | 24 ++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/enervent-ctrl-go/main.go b/enervent-ctrl-go/main.go index e226595..4ee62e3 100644 --- a/enervent-ctrl-go/main.go +++ b/enervent-ctrl-go/main.go @@ -1,6 +1,8 @@ package main import ( + "crypto/sha256" + "crypto/subtle" "embed" "encoding/json" "flag" @@ -24,13 +26,54 @@ import ( var static embed.FS var ( - version = "0.0.19" - pingvin pingvinKL.PingvinKL - DEBUG *bool - INTERVAL *int - ACCESS_LOG *bool + version = "0.0.20" + pingvin pingvinKL.PingvinKL + DEBUG *bool + INTERVAL *int + ACCESS_LOG *bool + usernamehash [32]byte + passwordhash [32]byte ) +// HTTP Basic Authentication middleware for http.HandlerFunc +func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc { + // Based on https://www.alexedwards.net/blog/basic-authentication-in-go + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if ok { + userHash := sha256.Sum256([]byte(user)) + passHash := sha256.Sum256([]byte(pass)) + usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1) + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + } + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} + +// HTTP Basic Authentication middleware for http.Handler +func authHandler(next http.Handler) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if ok { + userHash := sha256.Sum256([]byte(user)) + passHash := sha256.Sum256([]byte(pass)) + usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1) + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + } + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} + func coils(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/") @@ -123,24 +166,27 @@ func temperature(w http.ResponseWriter, r *http.Request) { } } -func listen(cert, key *string) { +func serve(cert, key *string) { log.Println("Starting pingvinAPI...") - http.HandleFunc("/api/v1/coils/", coils) - http.HandleFunc("/api/v1/registers/", registers) - http.HandleFunc("/api/v1/status", status) - http.HandleFunc("/api/v1/temperature/", temperature) + http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils)) + http.HandleFunc("/api/v1/status", authHandlerFunc(status)) + http.HandleFunc("/api/v1/registers/", authHandlerFunc(registers)) + http.HandleFunc("/api/v1/temperature/", authHandlerFunc(temperature)) html, err := fs.Sub(static, "static/html") if err != nil { log.Fatal(err) } htmlroot := http.FileServer(http.FS(html)) - http.Handle("/", htmlroot) - if *ACCESS_LOG { - handler := handlers.LoggingHandler(os.Stdout, http.DefaultServeMux) - err = http.ListenAndServeTLS(":8888", *cert, *key, handler) - } else { - err = http.ListenAndServeTLS(":8888", *cert, *key, nil) + http.HandleFunc("/", authHandler(htmlroot)) + logdst, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModeAppend) + if err != nil { + log.Fatal(err) } + if *ACCESS_LOG { + logdst = os.Stdout + } + handler := handlers.LoggingHandler(logdst, http.DefaultServeMux) + err = http.ListenAndServeTLS(":8888", *cert, *key, handler) if err != nil { log.Fatal(err) } @@ -185,8 +231,12 @@ func configure() (certfile, keyfile *string) { generatecert := flag.Bool("regenerate-certs", false, "Generate a new SSL certificate. A new one is generated on startup as `~/.config/enervent-ctrl/server.crt` if it doesn't exist.") cert := flag.String("cert", certpath+"certificate.pem", "Path to SSL public key to use for HTTPS") key := flag.String("key", certpath+"privatekey.pem", "Path to SSL private key to use for HTTPS") + username := flag.String("username", "pingvin", "Username for HTTP Basic Authentication") + password := flag.String("password", "enervent", "Password for HTTP Basic Authentication") // TODO: log file flag flag.Parse() + usernamehash = sha256.Sum256([]byte(*username)) + passwordhash = sha256.Sum256([]byte(*password)) // Check that certificate file exists if _, err = os.Stat(*cert); err != nil || *generatecert { generateCertificate(certpath, *cert, *key) @@ -197,6 +247,7 @@ func configure() (certfile, keyfile *string) { if *ACCESS_LOG { log.Println("HTTP Access logging enabled") } + log.Println("Update interval set to", *INTERVAL, "seconds") return cert, key } @@ -207,6 +258,6 @@ func main() { pingvin = pingvinKL.New(*DEBUG) pingvin.Update() go pingvin.Monitor(*INTERVAL) - listen(cert, key) + serve(cert, key) pingvin.Quit() } diff --git a/homeassistant/homeassistant-rest.yaml b/homeassistant/homeassistant-rest.yaml index 63461d0..087ae4f 100644 --- a/homeassistant/homeassistant-rest.yaml +++ b/homeassistant/homeassistant-rest.yaml @@ -2,6 +2,8 @@ rest: - resource: https://IP_ADDRESS:8888/api/v1/status scan_interval: 5 verify_ssl: false + username: pingvin + password: enervent sensor: - name: "Penguin operating mode" value_template: "{{ value_json['op_mode'] }}" @@ -106,47 +108,69 @@ rest_command: method: POST icon: mdi:fan-auto verify_ssl: false + username: pingvin + password: enervent penguin_circulation_manual: url: https://IP_ADDRESS:8888/api/v1/coils/11/0 method: POST icon: mdi:fan verify_ssl: false + username: pingvin + password: enervent penguin_boost_on: url: https://192.168.0.210:8888/api/v1/coils/10/1 method: POST verify_ssl: false + username: pingvin + password: enervent penguin_boost_off: url: https://192.168.0.210:8888/api/v1/coils/10/0 method: POST verify_ssl: false + username: pingvin + password: enervent penguin_overpressure_toggle: url: https://IP_ADDRESS:8888/api/v1/coils/3 method: POST icon: mdi:arrow-expand-all verify_ssl: false + username: pingvin + password: enervent penguin_max_heating_on: url: https://IP_ADDRESS:8888/api/v1/coils/6/1 method: POST icon: mdi:heat-wave verify_ssl: false + username: pingvin + password: enervent penguin_max_heating_off: url: https://IP_ADDRESS:8888/api/v1/coils/6/0 method: POST icon: mdi:scent-off verify_ssl: false + username: pingvin + password: enervent penguin_max_cooling_on: url: https://192.168.0.210:8888/api/v1/coils/7/1 method: POST verify_ssl: false + username: pingvin + password: enervent penguin_max_cooling_off: url: https://192.168.0.210:8888/api/v1/coils/7/0 method: POST verify_ssl: false + username: pingvin + password: enervent penguin_temperature_up: url: https://192.168.0.210:8888/api/v1/temperature/up method: POST verify_ssl: false + username: pingvin + password: enervent penguin_temperature_down: url: https://192.168.0.210:8888/api/v1/temperature/down method: POST verify_ssl: false + username: pingvin + password: enervent