package main //go:generate go-bindata --nocompress --pkg main static/... templates/... //go:generate go run scripts/sri.go --package main --output sri_auto.go static import ( "flag" "html/template" "io" "io/ioutil" "log" "net/http" "os" "git.autistici.org/ai3/go-common/serverutil" assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/gorilla/mux" "gopkg.in/yaml.v2" ) var ( addr = flag.String("addr", getenv("ADDR", ":3000"), "tcp `address` to listen on") configPath = flag.String("config", getenv("CONFIG", "/etc/float/dashboard.yml"), "dashboard config `file`") floatServicesPath = flag.String("services", getenv("SERVICES", "/etc/float/services.yml"), "float services `file`") tpl *template.Template config *Config services ServiceMap ) func getenv(k, dflt string) string { if s := os.Getenv(k); s != "" { return s } return dflt } // Config for the application. type Config struct { Domain string `yaml:"domain"` PublicDomain string `yaml:"public_domain"` LogoImagePath string `yaml:"logo_image"` HTTP *serverutil.ServerConfig `yaml:"http_server"` } func readYAML(path string, obj interface{}) error { data, err := ioutil.ReadFile(path) // nolint: gosec if err != nil { return err } return yaml.Unmarshal(data, obj) } // Read the program's configuration file. func mustReadConfig() *Config { var c Config if err := readYAML(*configPath, &c); err != nil { log.Fatalf("error reading configuration: %v", err) } return &c } // Read the configured services. func mustReadServices() ServiceMap { m := make(map[string]*Service) if err := readYAML(*floatServicesPath, &m); err != nil { log.Fatalf("error reading services file: %v", err) } for name, s := range m { s.Name = name } return m } // Parse the templates that are embedded with the binary (in bindata.go). func mustParseEmbeddedTemplates() *template.Template { root := template.New("").Funcs(template.FuncMap{ // Useful function to format something as YAML. "yaml": func(obj interface{}) string { if data, err := yaml.Marshal(obj); err == nil { return string(data) } return "" }, "sri_script": SRIScript, "sri_stylesheet": SRIStylesheet, }) files, err := AssetDir("templates") if err != nil { log.Fatalf("no asset dir for templates: %v", err) } for _, f := range files { b, err := Asset("templates/" + f) if err != nil { log.Fatalf("could not read embedded template %s: %v", f, err) } if _, err := root.New(f).Parse(string(b)); err != nil { log.Fatalf("error parsing template %s: %v", f, err) } } return root } // A relatively strict CSP. const contentSecurityPolicy = "default-src 'none'; img-src 'self' data:; script-src 'self'; style-src 'self'; font-src 'self';" func withDynamicHeaders(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Pragma", "no-cache") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Expires", "-1") w.Header().Set("X-Frame-Options", "NONE") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("X-Content-Type-Options", "nosniff") if w.Header().Get("Content-Security-Policy") == "" { w.Header().Set("Content-Security-Policy", contentSecurityPolicy) } h.ServeHTTP(w, r) }) } func renderTemplate(w io.Writer, name string, ctx map[string]interface{}) { if ctx == nil { ctx = make(map[string]interface{}) } ctx["Config"] = config ctx["Services"] = services if err := tpl.ExecuteTemplate(w, name, ctx); err != nil { log.Printf("template error: %v", err) } } func handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } renderTemplate(w, "index.html", nil) } func handleServiceDetails(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) s, ok := services[vars["service"]] if !ok { http.NotFound(w, r) return } renderTemplate(w, "service_details.html", map[string]interface{}{ "Service": s, }) } func handleContainerDetails(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) s, ok := services[vars["service"]] if !ok { http.NotFound(w, r) return } var c *Container for _, cc := range s.Containers { if cc.Name == vars["container"] { c = &cc break } } if c == nil { http.NotFound(w, r) return } renderTemplate(w, "container_details.html", map[string]interface{}{ "Service": s, "Container": c, }) } // Create the HTTP server. func makeServer() http.Handler { root := mux.NewRouter() root.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(&assetfs.AssetFS{ Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "static", }))) root.Handle("/service/{service}/details", withDynamicHeaders(http.HandlerFunc(handleServiceDetails))) root.Handle("/service/{service}/container/{container}/details", withDynamicHeaders(http.HandlerFunc(handleContainerDetails))) root.Handle("/", withDynamicHeaders(http.HandlerFunc(handleIndex))) return root } func main() { log.SetFlags(0) flag.Parse() tpl = mustParseEmbeddedTemplates() config = mustReadConfig() services = mustReadServices() h := makeServer() if err := serverutil.Serve(h, config.HTTP, *addr); err != nil { log.Fatal(err) } } type sortedStringList []string func (l sortedStringList) Len() int { return len(l) } func (l sortedStringList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l sortedStringList) Less(i, j int) bool { return l[i] < l[j] }