Squashed commit of the following:
commit468bc33f7e
Author: Jarno Rankinen <jarno@oranki.net> Date: Tue Mar 14 09:37:07 2023 +0200 gh-7 Log failed authentications. Forked rocketlaunchr/https-go to add CN to the generated certifcate commit0e10c9d925
Author: Jarno Rankinen <jarno@oranki.net> Date: Sun Mar 12 22:30:53 2023 +0200 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. commita3165db631
Author: Jarno Rankinen <jarno@oranki.net> Date: Sun Mar 12 12:41:41 2023 +0200 gh-7 Updated Home Assistant rest example to https commit54b555c523
Author: Jarno Rankinen <jarno@oranki.net> Date: Sun Mar 12 12:25:53 2023 +0200 gh-7 Enabled SSL encryption commit2738b60386
Author: Jarno Rankinen <jarno@oranki.net> Date: Sun Mar 12 12:07:36 2023 +0200 gh-7 Implemented automatic self-signed SSL generation on startup under ~/.config/enervent-ctrl Closes gh-7
This commit is contained in:
parent
0311605bae
commit
d53e976a36
|
@ -3,10 +3,13 @@ module github.com/0ranki/enervent-ctrl/enervent-ctrl-go
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433
|
||||
github.com/goburrow/modbus v0.1.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
)
|
||||
|
||||
require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/goburrow/serial v0.1.0 // indirect
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433 h1:QT2IRJnhIdCSr26LJktnZnBpHdiLfTrUFzLSdP3h9Wo=
|
||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433/go.mod h1:r4Jb05+PuiVKHDYwSsSBuSz4LpOlC2DgOY4N58+K8Hk=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro=
|
||||
|
@ -6,3 +8,5 @@ github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA
|
|||
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
|
@ -10,8 +12,10 @@ import (
|
|||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL"
|
||||
"github.com/0ranki/https-go"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
|
@ -22,13 +26,62 @@ import (
|
|||
var static embed.FS
|
||||
|
||||
var (
|
||||
version = "0.0.17"
|
||||
version = "0.0.21"
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
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/"), "/")
|
||||
|
@ -121,50 +174,99 @@ func temperature(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func listen() {
|
||||
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.ListenAndServe(":8888", handler)
|
||||
} else {
|
||||
err = http.ListenAndServe(":8888", 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)
|
||||
}
|
||||
}
|
||||
|
||||
func configure() {
|
||||
func generateCertificate(certpath, cert, key string) {
|
||||
if _, err := os.Stat(certpath); err != nil {
|
||||
log.Println("Generating configuration directory", certpath)
|
||||
if err := os.MkdirAll(certpath, 0750); err != nil {
|
||||
log.Fatal("Failed to generate configuration directory:", err)
|
||||
}
|
||||
}
|
||||
opts := https.GenerateOptions{Host: "enervent-ctrl.local", RSABits: 4096, ValidFor: 10 * 365 * 24 * time.Hour}
|
||||
log.Println("Generating new self-signed SSL keypair to ", certpath)
|
||||
log.Println("This may take a while...")
|
||||
pub, priv, err := https.GenerateKeys(opts)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating SSL certificate: ", err)
|
||||
}
|
||||
pingvin.Debug.Println("Certificate:\n", string(pub))
|
||||
pingvin.Debug.Println("Key:\n", string(priv))
|
||||
if err := os.WriteFile(key, priv, 0600); err != nil {
|
||||
log.Fatal("Error writing private key ", key, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL private key ", cert)
|
||||
if err := os.WriteFile(cert, pub, 0644); err != nil {
|
||||
log.Fatal("Error writing certificate ", cert, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL public key ", cert)
|
||||
}
|
||||
|
||||
func configure() (certfile, keyfile *string) {
|
||||
log.Println("Reading configuration")
|
||||
// Get the user home directory path
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("Could not determine user home directory")
|
||||
}
|
||||
certpath := homedir + "/.config/enervent-ctrl/"
|
||||
DEBUG = flag.Bool("debug", false, "Enable debug logging")
|
||||
INTERVAL = flag.Int("interval", 4, "Set the interval of background updates")
|
||||
ACCESS_LOG = flag.Bool("httplog", false, "Enable HTTP access logging")
|
||||
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)
|
||||
}
|
||||
if *DEBUG {
|
||||
log.Println("Debug logging enabled")
|
||||
}
|
||||
if *ACCESS_LOG {
|
||||
log.Println("HTTP Access logging enabled")
|
||||
}
|
||||
|
||||
log.Println("Update interval set to", *INTERVAL, "seconds")
|
||||
return cert, key
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("enervent-ctrl version", version)
|
||||
configure()
|
||||
cert, key := configure()
|
||||
pingvin = pingvinKL.New(*DEBUG)
|
||||
pingvin.Update()
|
||||
go pingvin.Monitor(*INTERVAL)
|
||||
listen()
|
||||
serve(cert, key)
|
||||
pingvin.Quit()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
rest:
|
||||
- resource: http://IP_ADDRESS:8888/api/v1/status
|
||||
- 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'] }}"
|
||||
|
@ -101,40 +104,73 @@ template:
|
|||
|
||||
rest_command:
|
||||
penguin_circulation_adaptive:
|
||||
url: http://IP_ADDRESS:8888/api/v1/coils/11/1
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/11/1
|
||||
method: POST
|
||||
icon: mdi:fan-auto
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_circulation_manual:
|
||||
url: http://IP_ADDRESS:8888/api/v1/coils/11/0
|
||||
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: http://192.168.0.210:8888/api/v1/coils/10/1
|
||||
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: http://192.168.0.210:8888/api/v1/coils/10/0
|
||||
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: http://IP_ADDRESS:8888/api/v1/coils/3
|
||||
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: http://IP_ADDRESS:8888/api/v1/coils/6/1
|
||||
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: http://IP_ADDRESS:8888/api/v1/coils/6/0
|
||||
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: http://192.168.0.210:8888/api/v1/coils/7/1
|
||||
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: http://192.168.0.210:8888/api/v1/coils/7/0
|
||||
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: http://192.168.0.210:8888/api/v1/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: http://192.168.0.210:8888/api/v1/temperature/down
|
||||
url: https://192.168.0.210:8888/api/v1/temperature/down
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
|
|
Loading…
Reference in New Issue