Squashed commit of the following:

commit 4b3f7c6f45
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Tue Mar 14 12:36:03 2023 +0200

    Configuration file ~/.config/enervent-ctrl/configuration.yaml

    If the file does not exist, will be generated with default values.
    CLI flags override values from configuration file.

Closes gh-19
This commit is contained in:
Jarno Rankinen 2023-03-14 12:37:34 +02:00
parent d53e976a36
commit 02312c522b
3 changed files with 108 additions and 38 deletions

View File

@ -6,6 +6,7 @@ require (
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433 github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433
github.com/goburrow/modbus v0.1.0 github.com/goburrow/modbus v0.1.0
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
gopkg.in/yaml.v3 v3.0.1
) )
require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

View File

@ -10,3 +10,7 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 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 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"io/fs" "io/fs"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -17,6 +18,7 @@ import (
"github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL" "github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL"
"github.com/0ranki/https-go" "github.com/0ranki/https-go"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"gopkg.in/yaml.v3"
) )
// Remember to dereference the symbolic links under ./static/html // Remember to dereference the symbolic links under ./static/html
@ -26,15 +28,24 @@ import (
var static embed.FS var static embed.FS
var ( var (
version = "0.0.21" version = "0.0.22"
pingvin pingvinKL.PingvinKL pingvin pingvinKL.PingvinKL
DEBUG *bool config Conf
INTERVAL *int
ACCESS_LOG *bool
usernamehash [32]byte usernamehash [32]byte
passwordhash [32]byte passwordhash [32]byte
) )
type Conf struct {
Port int `yaml:"port"`
SslCertificate string `yaml:"ssl_certificate"`
SslPrivatekey string `yaml:"ssl_privatekey"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Interval int `yaml:"interval"`
LogAccess bool `yaml:"log_access"`
Debug bool `yaml:"debug"`
}
// HTTP Basic Authentication middleware for http.HandlerFunc // HTTP Basic Authentication middleware for http.HandlerFunc
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc { func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go // Based on https://www.alexedwards.net/blog/basic-authentication-in-go
@ -82,6 +93,7 @@ func authHandler(next http.Handler) http.HandlerFunc {
}) })
} }
// \/api/v1/coils endpoint
func coils(w http.ResponseWriter, r *http.Request) { func coils(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/") pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
@ -123,6 +135,7 @@ func coils(w http.ResponseWriter, r *http.Request) {
} }
} }
// \/api/v1/registers endpoint
func registers(w http.ResponseWriter, r *http.Request) { func registers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/") pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
@ -158,11 +171,13 @@ func registers(w http.ResponseWriter, r *http.Request) {
} }
} }
// \/status endpoint
func status(w http.ResponseWriter, r *http.Request) { func status(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(pingvin.Status) json.NewEncoder(w).Encode(pingvin.Status)
} }
// \/api/v1/temperature endpoint
func temperature(w http.ResponseWriter, r *http.Request) { func temperature(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/") pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
@ -174,6 +189,7 @@ func temperature(w http.ResponseWriter, r *http.Request) {
} }
} }
// Start the HTTP server
func serve(cert, key *string) { func serve(cert, key *string) {
log.Println("Starting pingvinAPI...") log.Println("Starting pingvinAPI...")
http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils)) http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils))
@ -190,7 +206,7 @@ func serve(cert, key *string) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if *ACCESS_LOG { if config.LogAccess {
logdst = os.Stdout logdst = os.Stdout
} }
handler := handlers.LoggingHandler(logdst, http.DefaultServeMux) handler := handlers.LoggingHandler(logdst, http.DefaultServeMux)
@ -200,15 +216,10 @@ func serve(cert, key *string) {
} }
} }
func generateCertificate(certpath, cert, key string) { // Generate self-signed SSL keypair
if _, err := os.Stat(certpath); err != nil { func generateCertificate(cert, key string) {
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} 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("Generating new self-signed SSL keypair")
log.Println("This may take a while...") log.Println("This may take a while...")
pub, priv, err := https.GenerateKeys(opts) pub, priv, err := https.GenerateKeys(opts)
if err != nil { if err != nil {
@ -226,47 +237,101 @@ func generateCertificate(certpath, cert, key string) {
log.Println("Wrote new SSL public key ", cert) log.Println("Wrote new SSL public key ", cert)
} }
func configure() (certfile, keyfile *string) { // Read & parse the configuration file
log.Println("Reading configuration") func parseConfigFile() {
// Get the user home directory path
homedir, err := os.UserHomeDir() homedir, err := os.UserHomeDir()
if err != nil { if err != nil {
log.Fatal("Could not determine user home directory") log.Fatal("Could not determine user home directory")
} }
certpath := homedir + "/.config/enervent-ctrl/" confpath := homedir + "/.config/enervent-ctrl"
DEBUG = flag.Bool("debug", false, "Enable debug logging") if _, err := os.Stat(confpath); err != nil {
INTERVAL = flag.Int("interval", 4, "Set the interval of background updates") log.Println("Generating configuration directory", confpath)
ACCESS_LOG = flag.Bool("httplog", false, "Enable HTTP access logging") if err := os.MkdirAll(confpath, 0700); err != nil {
log.Fatal("Failed to generate configuration directory:", err)
}
}
conffile := confpath + "/configuration.yaml"
yamldata, err := ioutil.ReadFile(conffile)
if err != nil {
log.Println("Configuration file", conffile, "not found")
log.Println("Generating", conffile, "with default values")
initDefaultConfig(confpath)
if yamldata, err = ioutil.ReadFile(conffile); err != nil {
log.Fatal("Error parsing configuration:", err)
}
}
err = yaml.Unmarshal(yamldata, &config)
if err != nil {
log.Fatal("Failed to parse YAML:", err)
}
}
// Write the default configuration to $HOME/.config/enervent-ctrl/configuration.yaml
func initDefaultConfig(confpath string) {
config = Conf{
8888,
confpath + "/certificate.pem",
confpath + "/privatekey.pem",
"pingvin",
"enervent",
4,
false,
false,
}
conffile := confpath + "/configuration.yaml"
confbytes, err := yaml.Marshal(&config)
if err != nil {
log.Println("Error writing default configuration:", err)
}
if err := os.WriteFile(conffile, confbytes, 0600); err != nil {
log.Fatal("Failed to write default configuration:", err)
}
}
// Read configuration. CLI flags take presedence over configuration file
func configure() {
log.Println("Reading configuration")
parseConfigFile()
debugflag := flag.Bool("debug", config.Debug, "Enable debug logging")
intervalflag := flag.Int("interval", config.Interval, "Set the interval of background updates")
logaccflag := flag.Bool("httplog", config.LogAccess, "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.") 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") certflag := flag.String("cert", config.SslCertificate, "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") keyflag := flag.String("key", config.SslPrivatekey, "Path to SSL private key to use for HTTPS")
username := flag.String("username", "pingvin", "Username for HTTP Basic Authentication") usernflag := flag.String("username", config.Username, "Username for HTTP Basic Authentication")
password := flag.String("password", "enervent", "Password for HTTP Basic Authentication") passwflag := flag.String("password", config.Password, "Password for HTTP Basic Authentication")
// TODO: log file flag // TODO: log file flag
flag.Parse() flag.Parse()
usernamehash = sha256.Sum256([]byte(*username)) config.Debug = *debugflag
passwordhash = sha256.Sum256([]byte(*password)) config.Interval = *intervalflag
// Check that certificate file exists config.LogAccess = *logaccflag
if _, err = os.Stat(*cert); err != nil || *generatecert { config.SslCertificate = *certflag
generateCertificate(certpath, *cert, *key) config.SslPrivatekey = *keyflag
config.Username = *usernflag
config.Password = *passwflag
usernamehash = sha256.Sum256([]byte(config.Username))
passwordhash = sha256.Sum256([]byte(config.Password))
// Check that certificate file exists, generate if needed
if _, err := os.Stat(config.SslCertificate); err != nil || *generatecert {
generateCertificate(config.SslCertificate, config.SslPrivatekey)
} }
if *DEBUG { // Enable debug if configured
if config.Debug {
log.Println("Debug logging enabled") log.Println("Debug logging enabled")
} }
if *ACCESS_LOG { // Enable HTTP access logging if configured
if config.LogAccess {
log.Println("HTTP Access logging enabled") log.Println("HTTP Access logging enabled")
} }
log.Println("Update interval set to", config.Interval, "seconds")
log.Println("Update interval set to", *INTERVAL, "seconds")
return cert, key
} }
func main() { func main() {
log.Println("enervent-ctrl version", version) log.Println("enervent-ctrl version", version)
cert, key := configure() configure()
pingvin = pingvinKL.New(*DEBUG) pingvin = pingvinKL.New(config.Debug)
pingvin.Update() pingvin.Update()
go pingvin.Monitor(*INTERVAL) go pingvin.Monitor(config.Interval)
serve(cert, key) serve(&config.SslCertificate, &config.SslPrivatekey)
pingvin.Quit() pingvin.Quit()
} }