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.
This commit is contained in:
parent
a3165db631
commit
0e10c9d925
|
@ -1,6 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
@ -24,13 +26,54 @@ import (
|
||||||
var static embed.FS
|
var static embed.FS
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
pingvin pingvinKL.PingvinKL
|
pingvin pingvinKL.PingvinKL
|
||||||
DEBUG *bool
|
DEBUG *bool
|
||||||
INTERVAL *int
|
INTERVAL *int
|
||||||
ACCESS_LOG *bool
|
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) {
|
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,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...")
|
log.Println("Starting pingvinAPI...")
|
||||||
http.HandleFunc("/api/v1/coils/", coils)
|
http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils))
|
||||||
http.HandleFunc("/api/v1/registers/", registers)
|
http.HandleFunc("/api/v1/status", authHandlerFunc(status))
|
||||||
http.HandleFunc("/api/v1/status", status)
|
http.HandleFunc("/api/v1/registers/", authHandlerFunc(registers))
|
||||||
http.HandleFunc("/api/v1/temperature/", temperature)
|
http.HandleFunc("/api/v1/temperature/", authHandlerFunc(temperature))
|
||||||
html, err := fs.Sub(static, "static/html")
|
html, err := fs.Sub(static, "static/html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
htmlroot := http.FileServer(http.FS(html))
|
htmlroot := http.FileServer(http.FS(html))
|
||||||
http.Handle("/", htmlroot)
|
http.HandleFunc("/", authHandler(htmlroot))
|
||||||
if *ACCESS_LOG {
|
logdst, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModeAppend)
|
||||||
handler := handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)
|
if err != nil {
|
||||||
err = http.ListenAndServeTLS(":8888", *cert, *key, handler)
|
log.Fatal(err)
|
||||||
} else {
|
|
||||||
err = http.ListenAndServeTLS(":8888", *cert, *key, nil)
|
|
||||||
}
|
}
|
||||||
|
if *ACCESS_LOG {
|
||||||
|
logdst = os.Stdout
|
||||||
|
}
|
||||||
|
handler := handlers.LoggingHandler(logdst, http.DefaultServeMux)
|
||||||
|
err = http.ListenAndServeTLS(":8888", *cert, *key, handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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.")
|
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")
|
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")
|
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
|
// TODO: log file flag
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
usernamehash = sha256.Sum256([]byte(*username))
|
||||||
|
passwordhash = sha256.Sum256([]byte(*password))
|
||||||
// Check that certificate file exists
|
// Check that certificate file exists
|
||||||
if _, err = os.Stat(*cert); err != nil || *generatecert {
|
if _, err = os.Stat(*cert); err != nil || *generatecert {
|
||||||
generateCertificate(certpath, *cert, *key)
|
generateCertificate(certpath, *cert, *key)
|
||||||
|
@ -197,6 +247,7 @@ func configure() (certfile, keyfile *string) {
|
||||||
if *ACCESS_LOG {
|
if *ACCESS_LOG {
|
||||||
log.Println("HTTP Access logging enabled")
|
log.Println("HTTP Access logging enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Update interval set to", *INTERVAL, "seconds")
|
log.Println("Update interval set to", *INTERVAL, "seconds")
|
||||||
return cert, key
|
return cert, key
|
||||||
}
|
}
|
||||||
|
@ -207,6 +258,6 @@ func main() {
|
||||||
pingvin = pingvinKL.New(*DEBUG)
|
pingvin = pingvinKL.New(*DEBUG)
|
||||||
pingvin.Update()
|
pingvin.Update()
|
||||||
go pingvin.Monitor(*INTERVAL)
|
go pingvin.Monitor(*INTERVAL)
|
||||||
listen(cert, key)
|
serve(cert, key)
|
||||||
pingvin.Quit()
|
pingvin.Quit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ rest:
|
||||||
- resource: https://IP_ADDRESS:8888/api/v1/status
|
- resource: https://IP_ADDRESS:8888/api/v1/status
|
||||||
scan_interval: 5
|
scan_interval: 5
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
sensor:
|
sensor:
|
||||||
- name: "Penguin operating mode"
|
- name: "Penguin operating mode"
|
||||||
value_template: "{{ value_json['op_mode'] }}"
|
value_template: "{{ value_json['op_mode'] }}"
|
||||||
|
@ -106,47 +108,69 @@ rest_command:
|
||||||
method: POST
|
method: POST
|
||||||
icon: mdi:fan-auto
|
icon: mdi:fan-auto
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_circulation_manual:
|
penguin_circulation_manual:
|
||||||
url: https://IP_ADDRESS:8888/api/v1/coils/11/0
|
url: https://IP_ADDRESS:8888/api/v1/coils/11/0
|
||||||
method: POST
|
method: POST
|
||||||
icon: mdi:fan
|
icon: mdi:fan
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_boost_on:
|
penguin_boost_on:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/10/1
|
url: https://192.168.0.210:8888/api/v1/coils/10/1
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_boost_off:
|
penguin_boost_off:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/10/0
|
url: https://192.168.0.210:8888/api/v1/coils/10/0
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_overpressure_toggle:
|
penguin_overpressure_toggle:
|
||||||
url: https://IP_ADDRESS:8888/api/v1/coils/3
|
url: https://IP_ADDRESS:8888/api/v1/coils/3
|
||||||
method: POST
|
method: POST
|
||||||
icon: mdi:arrow-expand-all
|
icon: mdi:arrow-expand-all
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_max_heating_on:
|
penguin_max_heating_on:
|
||||||
url: https://IP_ADDRESS:8888/api/v1/coils/6/1
|
url: https://IP_ADDRESS:8888/api/v1/coils/6/1
|
||||||
method: POST
|
method: POST
|
||||||
icon: mdi:heat-wave
|
icon: mdi:heat-wave
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_max_heating_off:
|
penguin_max_heating_off:
|
||||||
url: https://IP_ADDRESS:8888/api/v1/coils/6/0
|
url: https://IP_ADDRESS:8888/api/v1/coils/6/0
|
||||||
method: POST
|
method: POST
|
||||||
icon: mdi:scent-off
|
icon: mdi:scent-off
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_max_cooling_on:
|
penguin_max_cooling_on:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/7/1
|
url: https://192.168.0.210:8888/api/v1/coils/7/1
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_max_cooling_off:
|
penguin_max_cooling_off:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/7/0
|
url: https://192.168.0.210:8888/api/v1/coils/7/0
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_temperature_up:
|
penguin_temperature_up:
|
||||||
url: https://192.168.0.210:8888/api/v1/temperature/up
|
url: https://192.168.0.210:8888/api/v1/temperature/up
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
penguin_temperature_down:
|
penguin_temperature_down:
|
||||||
url: https://192.168.0.210:8888/api/v1/temperature/down
|
url: https://192.168.0.210:8888/api/v1/temperature/down
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
|
username: pingvin
|
||||||
|
password: enervent
|
||||||
|
|
Loading…
Reference in New Issue