2023-01-28 23:28:37 +02:00
package main
import (
2023-03-12 22:30:53 +02:00
"crypto/sha256"
"crypto/subtle"
2023-02-02 11:44:30 +02:00
"embed"
2023-01-29 23:31:21 +02:00
"encoding/json"
2023-03-03 10:09:43 +02:00
"flag"
2023-02-02 11:44:30 +02:00
"io/fs"
2023-01-29 23:31:21 +02:00
"log"
"net/http"
2023-03-03 10:09:43 +02:00
"os"
2023-02-24 22:52:13 +02:00
"strconv"
"strings"
2023-03-12 12:07:36 +02:00
"time"
2023-01-29 14:42:03 +02:00
2023-01-28 23:28:37 +02:00
"github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL"
2023-03-14 09:37:07 +02:00
"github.com/0ranki/https-go"
2023-03-03 10:09:43 +02:00
"github.com/gorilla/handlers"
2023-01-28 23:28:37 +02:00
)
2023-02-02 13:38:31 +02:00
// Remember to dereference the symbolic links under ./static/html
// prior to building the binary e.g. by using tar
2023-02-02 11:44:30 +02:00
//go:embed static/html/*
var static embed . FS
2023-01-29 23:31:21 +02:00
var (
2023-03-14 09:37:07 +02:00
version = "0.0.21"
2023-03-12 22:30:53 +02:00
pingvin pingvinKL . PingvinKL
DEBUG * bool
INTERVAL * int
ACCESS_LOG * bool
usernamehash [ 32 ] byte
passwordhash [ 32 ] byte
2023-01-29 23:31:21 +02:00
)
2023-03-12 22:30:53 +02:00
// 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
}
}
2023-03-14 09:37:07 +02:00
if len ( user ) == 0 {
user = "-"
}
log . Println ( "Authentication failed: IP:" , r . RemoteAddr , "URI:" , r . RequestURI , "username:" , user )
2023-03-12 22:30:53 +02:00
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
}
}
2023-03-14 09:37:07 +02:00
if len ( user ) == 0 {
user = "-"
}
log . Println ( "Authentication failed: IP:" , r . RemoteAddr , "URI:" , r . RequestURI , "username:" , user )
2023-03-12 22:30:53 +02:00
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="restricted", charset="UTF-8" ` )
http . Error ( w , "Unauthorized" , http . StatusUnauthorized )
} )
}
2023-01-29 23:31:21 +02:00
func coils ( w http . ResponseWriter , r * http . Request ) {
2023-01-29 23:43:36 +02:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2023-02-24 22:52:13 +02:00
pathparams := strings . Split ( strings . TrimPrefix ( r . URL . Path , "/api/v1/coils/" ) , "/" )
if len ( pathparams [ 0 ] ) == 0 {
json . NewEncoder ( w ) . Encode ( pingvin . Coils )
} else if len ( pathparams [ 0 ] ) > 0 && r . Method == "GET" && len ( pathparams ) < 2 { // && r.Method == "POST"
intaddr , err := strconv . Atoi ( pathparams [ 0 ] )
if err != nil {
log . Println ( "ERROR: Could not parse coil address" , pathparams [ 0 ] )
log . Println ( err )
return
}
pingvin . ReadCoil ( uint16 ( intaddr ) )
json . NewEncoder ( w ) . Encode ( pingvin . Coils [ intaddr ] )
} else if len ( pathparams [ 0 ] ) > 0 && r . Method == "POST" && len ( pathparams ) == 2 {
intaddr , err := strconv . Atoi ( pathparams [ 0 ] )
if err != nil {
log . Println ( "ERROR: Could not parse coil address" , pathparams [ 0 ] )
log . Println ( err )
return
}
boolval , err := strconv . ParseBool ( pathparams [ 1 ] )
if err != nil {
log . Println ( "ERROR: Could not parse coil value" , pathparams [ 1 ] )
log . Println ( err )
return
}
pingvin . WriteCoil ( uint16 ( intaddr ) , boolval )
json . NewEncoder ( w ) . Encode ( pingvin . Coils [ intaddr ] )
2023-03-03 08:08:36 +02:00
} else if len ( pathparams [ 0 ] ) > 0 && r . Method == "POST" && len ( pathparams ) == 1 {
intaddr , err := strconv . Atoi ( pathparams [ 0 ] )
if err != nil {
log . Println ( "ERROR: Could not parse coil address" , pathparams [ 0 ] )
log . Println ( err )
return
}
pingvin . WriteCoil ( uint16 ( intaddr ) , ! pingvin . Coils [ intaddr ] . Value )
json . NewEncoder ( w ) . Encode ( pingvin . Coils [ intaddr ] )
2023-02-24 22:52:13 +02:00
}
2023-01-29 23:31:21 +02:00
}
2023-02-01 22:12:56 +02:00
func registers ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2023-03-02 14:24:05 +02:00
pathparams := strings . Split ( strings . TrimPrefix ( r . URL . Path , "/api/v1/registers/" ) , "/" )
if len ( pathparams [ 0 ] ) == 0 {
json . NewEncoder ( w ) . Encode ( pingvin . Registers )
} else if len ( pathparams [ 0 ] ) > 0 && r . Method == "GET" && len ( pathparams ) < 2 { // && r.Method == "POST"
intaddr , err := strconv . Atoi ( pathparams [ 0 ] )
if err != nil {
log . Println ( "ERROR: Could not parse register address" , pathparams [ 0 ] )
log . Println ( err )
return
}
pingvin . ReadRegister ( uint16 ( intaddr ) )
json . NewEncoder ( w ) . Encode ( pingvin . Registers [ intaddr ] )
} else if len ( pathparams [ 0 ] ) > 0 && r . Method == "POST" && len ( pathparams ) == 2 {
intaddr , err := strconv . Atoi ( pathparams [ 0 ] )
if err != nil {
log . Println ( "ERROR: Could not parse register address" , pathparams [ 0 ] )
log . Println ( err )
return
}
intval , err := strconv . Atoi ( pathparams [ 1 ] )
if err != nil {
log . Println ( "ERROR: Could not parse register value" , pathparams [ 1 ] )
log . Println ( err )
return
}
_ , err = pingvin . WriteRegister ( uint16 ( intaddr ) , uint16 ( intval ) )
if err != nil {
log . Println ( err )
}
json . NewEncoder ( w ) . Encode ( pingvin . Registers [ intaddr ] )
2023-02-01 22:19:57 +02:00
}
2023-02-01 22:12:56 +02:00
}
2023-02-18 21:52:53 +02:00
func status ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( pingvin . Status )
}
2023-03-09 16:15:54 +02:00
func temperature ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
pathparams := strings . Split ( strings . TrimPrefix ( r . URL . Path , "/api/v1/temperature/" ) , "/" )
if len ( pathparams [ 0 ] ) > 0 && r . Method == "POST" && len ( pathparams ) == 1 {
pingvin . Temperature ( pathparams [ 0 ] )
json . NewEncoder ( w ) . Encode ( pingvin . Registers [ 135 ] )
} else {
return
}
}
2023-03-12 22:30:53 +02:00
func serve ( cert , key * string ) {
2023-01-29 23:31:21 +02:00
log . Println ( "Starting pingvinAPI..." )
2023-03-12 22:30:53 +02:00
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 ) )
2023-02-02 11:44:30 +02:00
html , err := fs . Sub ( static , "static/html" )
if err != nil {
log . Fatal ( err )
}
htmlroot := http . FileServer ( http . FS ( html ) )
2023-03-12 22:30:53 +02:00
http . HandleFunc ( "/" , authHandler ( htmlroot ) )
logdst , err := os . OpenFile ( os . DevNull , os . O_WRONLY , os . ModeAppend )
if err != nil {
log . Fatal ( err )
}
2023-03-03 10:09:43 +02:00
if * ACCESS_LOG {
2023-03-12 22:30:53 +02:00
logdst = os . Stdout
2023-03-03 10:09:43 +02:00
}
2023-03-12 22:30:53 +02:00
handler := handlers . LoggingHandler ( logdst , http . DefaultServeMux )
err = http . ListenAndServeTLS ( ":8888" , * cert , * key , handler )
2023-02-01 21:52:55 +02:00
if err != nil {
log . Fatal ( err )
}
2023-01-29 23:31:21 +02:00
}
2023-03-12 12:07:36 +02:00
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 )
2023-03-14 09:37:07 +02:00
log . Println ( "This may take a while..." )
2023-03-12 12:07:36 +02:00
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 )
}
2023-03-12 12:25:53 +02:00
func configure ( ) ( certfile , keyfile * string ) {
2023-03-03 10:09:43 +02:00
log . Println ( "Reading configuration" )
2023-03-12 12:07:36 +02:00
// 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/"
2023-03-12 12:25:53 +02:00
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" )
2023-03-12 22:30:53 +02:00
username := flag . String ( "username" , "pingvin" , "Username for HTTP Basic Authentication" )
password := flag . String ( "password" , "enervent" , "Password for HTTP Basic Authentication" )
2023-03-12 12:25:53 +02:00
// TODO: log file flag
flag . Parse ( )
2023-03-12 22:30:53 +02:00
usernamehash = sha256 . Sum256 ( [ ] byte ( * username ) )
passwordhash = sha256 . Sum256 ( [ ] byte ( * password ) )
2023-03-12 12:07:36 +02:00
// Check that certificate file exists
2023-03-12 12:25:53 +02:00
if _ , err = os . Stat ( * cert ) ; err != nil || * generatecert {
generateCertificate ( certpath , * cert , * key )
2023-03-12 12:07:36 +02:00
}
2023-03-03 10:09:43 +02:00
if * DEBUG {
log . Println ( "Debug logging enabled" )
}
if * ACCESS_LOG {
log . Println ( "HTTP Access logging enabled" )
}
2023-03-12 22:30:53 +02:00
2023-03-03 10:09:43 +02:00
log . Println ( "Update interval set to" , * INTERVAL , "seconds" )
2023-03-12 12:25:53 +02:00
return cert , key
2023-03-03 10:09:43 +02:00
}
2023-01-28 23:28:37 +02:00
func main ( ) {
2023-01-29 23:31:21 +02:00
log . Println ( "enervent-ctrl version" , version )
2023-03-12 12:25:53 +02:00
cert , key := configure ( )
2023-03-03 10:09:43 +02:00
pingvin = pingvinKL . New ( * DEBUG )
2023-01-29 21:56:22 +02:00
pingvin . Update ( )
2023-03-03 10:09:43 +02:00
go pingvin . Monitor ( * INTERVAL )
2023-03-12 22:30:53 +02:00
serve ( cert , key )
2023-03-09 22:17:24 +02:00
pingvin . Quit ( )
2023-01-28 23:28:37 +02:00
}