// vim: shiftwidth=4 tabstop=4 noexpandtab package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "net/netip" "text/template" ) type RuntimeConfig struct { Devices DevicesConfig Web WebConfig } type DevicesConfig struct { Hs100 []Hs100Conf } type WebConfig struct { Listen netip.AddrPort } // main() contains the control flow of this program. func main() { configPath := parseFlags() config := parseConfig(configPath) http.HandleFunc("/", index(config.Devices)) http.HandleFunc("/api", api()) http.HandleFunc("/webiot.css", css()) fmt.Printf("Serving at http://%s\n", config.Web.Listen) log.Fatal(http.ListenAndServe(config.Web.Listen.String(), nil)) } // parseFlags() handles command line interface (CLI) flags. func parseFlags() string { var r string // return value 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 { // read config file and ensure proper JSON formatting data := mustRead(path) if !json.Valid(data) { log.Fatalf("%s contains invalid JSON!", path) } // read to RuntimeConfig struct and handle errors 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(config DevicesConfig) func(http.ResponseWriter, *http.Request) { // prepare HTML html := mustRender("index.html.tmpl", config) return func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, html) } } func css() func(http.ResponseWriter, *http.Request) { // read CSS file css := string(mustRead("./libweb/libweb.css")) return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") fmt.Fprint(w, css) } } // mustRead() reads a file and panics if this is not possible. func mustRead(path string) []byte { data, err := ioutil.ReadFile(path) if err != nil { log.Fatalf("Could not read '%s'!", path) } return data } // mustRender() renders a template file with the given data and panics if this // is not possible. func mustRender(filepath string, data interface{}) string { file := mustRead(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 // read parameters and handle errors 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 } // set WiFi plug 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] }