package main import ( "bytes" "encoding/json" "embed" "flag" "fmt" "log" "net/http" "net/netip" "os" "text/template" ) //go:embed simple.css/simple.css templates/index.html var static embed.FS type RuntimeConfig struct { Devices DevicesConfig Web WebConfig } type DevicesConfig struct { Hs100 []Hs100Conf } type WebConfig struct { Listen netip.AddrPort } func main() { configPath := parseFlags() c := parseConfig(configPath) http.HandleFunc("/", index(c.Devices)) http.HandleFunc("/api", api()) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(static)))) fmt.Printf("Serving at http://%s\n", c.Web.Listen) log.Fatal(http.ListenAndServe(c.Web.Listen.String(), nil)) } // parseFlags() handles command line interface (CLI) flags. func parseFlags() string { var r string flag.StringVar(&r, "c", "/etc/webiot/config.json", "path to configuration file") flag.Parse() return r } // parseConfig() parses and validates the runtime configuration file and // returns it as Go datastructure. func parseConfig(path string) RuntimeConfig { data, err := os.ReadFile(path) if err != nil { log.Fatalf("Could not read '%s'!", path) } if !json.Valid(data) { log.Fatalf("%s contains invalid JSON!", path) } config := RuntimeConfig{} err = json.Unmarshal(data, &config) if err != nil { log.Fatalf("Could not parse configuration file:\n%s\n", err) } return config } // index() returns a HTTP handler for the index page. func index(devices DevicesConfig) func(http.ResponseWriter, *http.Request) { tmpl, err := template.ParseFS(static, "templates/index.html") if err != nil { log.Fatal(err) } return func(w http.ResponseWriter, r *http.Request) { err = tmpl.Execute(w, devices) if err != nil { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } } } // mustRender() renders a template file with the given data and panics if this // is not possible. func mustRender(filepath string, data interface{}) string { file, err := os.ReadFile(filepath) if err != nil { log.Fatalf("Could not read '%s'!", filepath) } tmpl, err := template.New(filepath).Parse(string(file)) var buffer bytes.Buffer err = tmpl.Execute(&buffer, data) if err != nil { fmt.Println(err) log.Fatalf("Could not execute template for %s!", filepath) } return buffer.String() } // api() returns the HTTP handler for the API endpoint. func api() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // TODO assert correct HTTP method errHost, host := assertSingleParam(r, "host") errState, state := assertSingleParam(r, "state") if (errHost != nil) || (errState != nil) { http.Error(w, "Provide exactly one host and one state parameter!", 400) return } err := set(host, state) if err != nil { http.Error(w, "Could not set WiFi plug.", 500) } else { fmt.Fprint(w, "ok") } } } // assertSingleParam() returns the value of given URL key and panics if there // is not exactly one match for this key. func assertSingleParam(r *http.Request, key string) (error, string) { values := r.URL.Query()[key] if len(values) != 1 { return fmt.Errorf("Provide exactly one '%s' parameter!", key), "" } return nil, values[0] }