This commit is contained in:
2022-01-08 11:40:11 -06:00
parent 6fd10c616d
commit 84ebae8a29
8 changed files with 928 additions and 0 deletions

47
cmd/tpapi/config.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"os"
"time"
"github.com/hashicorp/logutils"
"github.com/prometheus/client_golang/prometheus"
)
type configStructure struct {
// time configuration
TimeFormat string `json:"time_format"`
TimeZone *time.Location
TimeZoneUTC *time.Location
// logging
LogLevel int `json:"log_level"`
Log *logutils.LevelFilter `json:"log_level_fileter"`
// webserver
WebSrvPort int `json:"webserver_port"`
WebSrvIP string `json:"webserver_ip"`
WebSrvReadTimeout int `json:"webserver_read_timeout"`
WebSrvWriteTimeout int `json:"webserver_write_timeout"`
WebSrvIdleTimeout int `json:"webserver_idle_timeout"`
// prometheus
Prometheus configPrometheus
}
type configPrometheus struct {
NumGroupCreated prometheus.Gauge
NumGroupChanged prometheus.Gauge
NumGroupDeleted prometheus.Gauge
}
var config = configStructure{
TimeFormat: "2006-01-02 15:04:05",
Log: &logutils.LevelFilter{
Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"},
Writer: os.Stderr,
},
WebSrvReadTimeout: 2,
WebSrvWriteTimeout: 10,
WebSrvIdleTimeout: 2,
}

215
cmd/tpapi/httpServer.go Normal file
View File

@@ -0,0 +1,215 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net"
"strconv"
"strings"
"time"
"net/http"
"tplink/internal/tplink"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func httpAccessLog(req *http.Request) {
log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI)
}
func crossSiteOrigin(w http.ResponseWriter) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
}
func httpServer(host string, port int) {
path := http.NewServeMux()
connection := &http.Server{
Addr: host + ":" + strconv.FormatInt(int64(port), 10),
Handler: path,
ReadTimeout: time.Duration(config.WebSrvReadTimeout) * time.Second,
WriteTimeout: time.Duration(config.WebSrvWriteTimeout) * time.Second,
IdleTimeout: time.Duration(config.WebSrvIdleTimeout) * time.Second,
}
// metrics
path.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
promhttp.Handler().ServeHTTP(w, r)
})
// api
path.HandleFunc("/api/v1/tplink", webTPLink)
// healthcheck
path.HandleFunc("/healthcheck", webHealthCheck)
// root
path.HandleFunc("/", webRoot)
if err := connection.ListenAndServe(); err != nil {
log.Fatalf("[ERROR] %s\n", err)
}
}
func webRoot(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
if strings.ToLower(r.Method) == "get" {
tmpltWebRoot(w)
} else {
log.Printf("[DEBUG] Request to '/' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
}
}
func webHealthCheck(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
if strings.ToLower(r.Method) == "get" {
tmpltHealthCheck(w)
} else {
log.Printf("[DEBUG] Request to '/healthcheck' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
}
}
func webTPLink(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
if strings.ToLower(r.Method) == "get" {
keys := r.URL.Query()
if len(keys) == 0 {
log.Printf("[INFO] Required parameters missing: no host, state, or deviceid was provided.\n")
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for host, state, or deviceid was provided.")
return
}
var (
host string
hostSet bool
state bool
stateSet bool
id int
)
for k, v := range keys {
log.Printf("%s: %v\n", k, v)
switch strings.ToLower(k) {
case "host":
if len(v) != 1 {
log.Printf("[INFO] Host was defined multiple times.\n")
tmpltError(w, http.StatusBadRequest, "Required parameter host was defined multiple times.")
return
}
_, err := net.LookupHost(v[0])
if err != nil {
log.Printf("[INFO] Unable to resolve provided hostname: %s\n", v[0])
tmpltError(w, http.StatusBadRequest, "Unable to resolve provided hostname.")
return
}
hostSet = true
host = v[0]
case "state":
if len(v) != 1 {
log.Printf("[INFO] State was defined multiple times.\n")
tmpltError(w, http.StatusBadRequest, "Required parameter state was defined multiple times.")
return
}
s, err := strconv.ParseBool(v[0])
if err != nil {
log.Printf("[INFO] State is not boolean: %s\n", v[0])
tmpltError(w, http.StatusBadRequest, "Required parameter state is not boolean.")
return
}
stateSet = true
state = s
case "deviceid":
if len(v) != 1 {
log.Printf("[INFO] Deviceid was defined multiple times.\n")
tmpltError(w, http.StatusBadRequest, "Required parameter device was defined multiple times.")
return
}
d, err := strconv.Atoi(v[0])
if err != nil {
log.Printf("[INFO] Deviceid is not an integer: %s\n", v[0])
tmpltError(w, http.StatusBadRequest, "Required parameter deviceid is not an integer.")
return
}
id = d
}
}
if !hostSet {
log.Printf("[INFO] Required parameter missing: no host was provided.\n")
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for host provided.")
return
}
if !stateSet {
log.Printf("[INFO] Required parameter missing: no state was provided.\n")
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for state provided.")
return
}
t := tplink.Tplink{
Host: host,
SwitchID: id,
}
if err := t.ChangeStateMultiSwitch(state); err != nil {
log.Printf("[INFO] Unable to change device state: %v\n", err)
tmpltError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to change device state: %v\n", err))
return
}
sysInfo, err := t.SystemInfo()
if err != nil {
log.Printf("[INFO] Unable to get info from target device: %v\n", err)
tmpltError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to read info from device: %v\n", err))
return
}
var ds int
if len(sysInfo.System.GetSysinfo.Children) == 0 {
ds = sysInfo.System.GetSysinfo.RelayState
} else {
ds = sysInfo.System.GetSysinfo.Children[id].State
}
var fs string
if ds == 0 {
fs = "OFF"
} else {
fs = "ON"
}
o := struct {
Status string `json:"status" yaml:"status"`
Host string `json:"host" yaml:"host"`
DeviceID int `json:"device_id" yaml:"deviceID"`
}{
Status: fs,
Host: host,
DeviceID: id,
}
w.Header().Add("Content-Type", "application/json")
output, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
}
w.Write(output) //nolint:errcheck
} else if strings.ToLower(r.Method) == "options" {
return
} else {
log.Printf("[DEBUG] Request to '%s' was made using the wrong method: expected %s, got %s", "GET|OPTIONS", r.URL.Path, strings.ToUpper(r.Method))
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
}
}

View File

@@ -0,0 +1,67 @@
package main
import (
"encoding/json"
"log"
"net/http"
)
func tmpltError(w http.ResponseWriter, s int, m string) {
var (
output []byte
o = struct {
Error bool `json:"error" yaml:"error"`
ErrorMsg string `json:"errorMessage" yaml:"errorMessage"`
}{
Error: true,
ErrorMsg: m,
}
err error
)
w.Header().Add("Content-Type", "application/json")
output, err = json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
}
w.WriteHeader(s)
w.Write(output) //nolint:errcheck
}
func tmpltWebRoot(w http.ResponseWriter) {
o := struct {
Application string `json:"application" yaml:"application"`
Description string `json:"description" yaml:"description"`
Version string `json:"version" yaml:"version"`
}{
Application: "TP-Link Web API",
Description: "API for automated calls to TP-Link devices",
Version: "v1.0.0",
}
w.Header().Add("Content-Type", "application/json")
output, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
}
w.Write(output) //nolint:errcheck
}
func tmpltHealthCheck(w http.ResponseWriter) {
o := struct {
WebServer bool `json:"webServerActive" yaml:"webServerActive"`
Status string `json:"status" yaml:"status"`
}{
WebServer: true,
Status: "healthy",
}
output, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf("[TRACE] Unable to marshal status message: %v.", err)
}
w.Header().Add("Content-Type", "application/json")
w.Write(output) //nolint:errcheck
}

96
cmd/tpapi/init.go Normal file
View File

@@ -0,0 +1,96 @@
package main
import (
"flag"
"log"
"os"
"strconv"
"github.com/hashicorp/logutils"
)
// getEnvString returns string from environment variable
func getEnvString(env, def string) (val string) { //nolint:deadcode
val = os.Getenv(env)
if val == "" {
return def
}
return
}
// getEnvInt returns int from environment variable
func getEnvInt(env string, def int) (ret int) {
val := os.Getenv(env)
if val == "" {
return def
}
ret, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env)
}
return
}
func setLogLevel(l int) {
switch {
case l <= 20:
config.Log.SetMinLevel(logutils.LogLevel("ERROR"))
case l > 20 && l <= 40:
config.Log.SetMinLevel(logutils.LogLevel("WARNING"))
case l > 40 && l <= 60:
config.Log.SetMinLevel(logutils.LogLevel("INFO"))
case l > 60 && l <= 80:
config.Log.SetMinLevel(logutils.LogLevel("DEBUG"))
case l > 80:
config.Log.SetMinLevel(logutils.LogLevel("TRACE"))
}
}
func initialize() {
var tz string
// log configuration
flag.IntVar(&config.LogLevel,
"log",
getEnvInt("LOG_LEVEL", 0),
"(LOG_LEVEL) Set the log verbosity.",
)
// locality configuration
flag.StringVar(&tz,
"timezone",
getEnvString("TIMEZONE", "America/Chicago"),
"(TIMEZONE) Timezone for the sunrise/sunset calculation.",
)
// local webserver configuration
flag.IntVar(&config.WebSrvPort,
"http-port",
getEnvInt("HTTP_PORT", 8080),
"(HTTP_PORT) Listen port for internal webserver")
flag.StringVar(&config.WebSrvIP,
"http-ip",
getEnvString("HTTP_IP", "0.0.0.0"),
"(HTTP_IP) Listen ip for internal webserver")
flag.IntVar(&config.WebSrvIdleTimeout,
"http-idle-timeout",
getEnvInt("HTTP_IDLE_TIMEOUT", 2),
"(HTTP_IDLE_TIMEOUT) Idle timeout for internal webserver")
flag.IntVar(&config.WebSrvReadTimeout,
"http-read-timeout",
getEnvInt("HTTP_READ_TIMEOUT", 5),
"(HTTP_READ_TIMEOUT) Read timeout for internal webserver")
flag.IntVar(&config.WebSrvWriteTimeout,
"http-write-timeout",
getEnvInt("HTTP_WRITE_TIMEOUT", 2),
"(HTTP_WRITE_TIMEOUT) Write timeout for internal webserver")
flag.Parse()
setLogLevel(config.LogLevel)
log.SetOutput(config.Log)
log.Printf("[DEBUG] Initialization complete.")
}

28
cmd/tpapi/main.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
)
func forever() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
sig := <-c
log.Printf("[INFO] shutting down, detected signal: %s", sig)
}
func main() {
initialize()
defer func() {
log.Println("[DEBUG] shutdown sequence complete")
}()
go httpServer(config.WebSrvIP, config.WebSrvPort)
forever()
}