package snap import ( "context" "crypto/tls" "encoding/json" "fmt" "html/template" "io" "io/ioutil" "net/http" "os" "path" "strings" "time" "git.twelvetwelve.org/library/core/log" "git.twelvetwelve.org/library/snap/pkg/autocert" "github.com/gorilla/mux" "git.twelvetwelve.org/library/snap/auth" ) type Server struct { log log.Logger address string theme string debug bool fs http.FileSystem auth auth.Authenticator router *mux.Router http *http.Server templates string cachedTmpl *template.Template templateFuncs template.FuncMap meta map[string]string // testModeEnabled if test mode is enabled we will not render the template but jsut return the // json object testModeEnabled bool version int64 } type SnapBaseContent struct { Theme string Content interface{} } func noescape(str string) template.HTML { return template.HTML(str) } var builtinFuncMap = template.FuncMap{ "noescape": noescape, } func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http.Request) *Context { c := &Context{ r: r, w: w, srv: s, auth: auth, } return c } func (s *Server) withLoginHandler(auth auth.Authenticator, loginHandler func(c *Context) bool, handle func(c *Context)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rec, code := auth.DoAuth(w, r) switch code { case http.StatusOK: c := s.makeContext(rec, w, r) handle(c) return case http.StatusUnauthorized: c := s.makeContext(rec, w, r) if loginHandler(c) { handle(c) } return default: http.Error(w, http.StatusText(code), code) } } } func (s *Server) authenticated(auth auth.Authenticator, redirect string, handle func(c *Context)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rec, code := auth.DoAuth(w, r) switch code { case http.StatusOK: c := s.makeContext(rec, w, r) handle(c) return case http.StatusUnauthorized: if redirect != "" { http.Redirect(w, r, redirect, http.StatusSeeOther) } else { http.Error(w, "Not authorized", http.StatusUnauthorized) } return default: http.Error(w, http.StatusText(code), code) } } } func (s *Server) wrapper(handle func(c *Context)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := s.makeContext(nil, w, r) if s.auth != nil { if rec, code := s.auth.DoAuth(w, r); code == http.StatusOK { c.auth = rec } } handle(c) // discard the rest of the body content io.Copy(ioutil.Discard, r.Body) defer r.Body.Close() } } // This is a bit different then the standard template.parseFiles code in that it gives us hiarchial templates // // header.html // mydirectory/service.html ... func (s *Server) parseTemplates(t *template.Template, filenames ...string) (*template.Template, error) { //if err := t.checkCanParse(); err != nil { // return nil, err //} if len(filenames) == 0 { // Not really a problem, but be consistent. return nil, fmt.Errorf("html/template: no files named in call to ParseFiles") } for _, filename := range filenames { f, err := s.fs.Open(filename) if err != nil { return nil, err } b, err := ioutil.ReadAll(f) f.Close() if err != nil { return nil, err } data := string(b) name := strings.TrimPrefix(filename, s.templates+"/") // First template becomes return value if not already defined, // and we use that one for subsequent New calls to associate // all the templates together. Also, if this file has the same name // as t, this file becomes the contents of t, so // t, err := New(name).Funcs(xxx).ParseFiles(name) // works. Otherwise we create a new template associated with t. var tmpl *template.Template if t == nil { t = template.New(name).Funcs(s.templateFuncs) } if name == t.Name() { tmpl = t } else { tmpl = t.New(name) } _, err = tmpl.Parse(data) if err != nil { return nil, err } } return t, nil } func Walk(fs http.FileSystem, base string, walkFunc func(path string, info os.FileInfo, err error) error) error { f, err := fs.Open(base) if err != nil { return err } defer f.Close() s, err := f.Stat() if err != nil { return err } if s.IsDir() { // its a directory, recurse files, err := f.Readdir(1024) if err != nil { return err } for _, cf := range files { if err = Walk(fs, path.Join(base, cf.Name()), walkFunc); err != nil { return err } } return nil } return walkFunc(base, s, nil) } func (s *Server) LoadTemplatesFS(fs http.FileSystem, base string) (*template.Template, error) { tmpl := template.New("").Funcs(s.templateFuncs) err := Walk(fs, base, func(path string, info os.FileInfo, err error) error { if strings.Contains(path, ".html") { _, err := s.parseTemplates(tmpl, path) if err != nil { s.log.Printf("%+v\n", err) } } return err }) if err != nil { return nil, err } return tmpl, nil } func (s *Server) loadTemplates() *template.Template { tmpl, err := s.LoadTemplatesFS(s.fs, s.templates) if err != nil { s.log.Fatalf("loadTemplates %v %v\n", err, s.templates) } return tmpl } func (s *Server) getTemplates() *template.Template { if s.debug { return s.loadTemplates() } if s.cachedTmpl == nil { s.cachedTmpl = s.loadTemplates() } return s.cachedTmpl } func (s *Server) render(w http.ResponseWriter, tmpl string, content interface{}) { if s.testModeEnabled { msg, err := json.Marshal(content) if err != nil { s.renderError(w, 400, "Internal Server Error") } s.reply(w, string(msg)) return } err := s.getTemplates().ExecuteTemplate(w, tmpl, content) if err != nil { s.log.Warnf("%s\n", err) } } func (s *Server) reply(w http.ResponseWriter, msg string) { w.Write([]byte(msg)) } func (s *Server) renderError(w http.ResponseWriter, code int, msg string) { w.WriteHeader(code) w.Write([]byte(msg)) } func (s *Server) HandleFuncAuthenticated(path, redirect string, f func(c *Context)) *mux.Route { if s.auth == nil { return nil } return s.router.HandleFunc(path, s.authenticated(s.auth, redirect, f)) } func (s *Server) HandleFuncAuthenticatedWithLogin(path string, loginHandler func(c *Context) bool, contentHandler func(c *Context)) *mux.Route { if s.auth == nil { return nil } return s.router.HandleFunc(path, s.withLoginHandler(s.auth, loginHandler, contentHandler)) } func (s *Server) HandleFuncCustomAuth(auth auth.Authenticator, path, redirect string, f func(c *Context)) *mux.Route { if auth == nil { s.log.Warnf("Nil auth on %s\n", path) return nil } return s.router.HandleFunc(path, s.authenticated(auth, redirect, f)) } func (s *Server) HandleFunc(path string, f func(c *Context)) *mux.Route { return s.router.HandleFunc(path, s.wrapper(f)) } func (s *Server) AddRoute(path string, r *RouteBuilder) *mux.Route { return s.router.HandleFunc(path, r.BuildRoute(s)) } func (s *Server) SetDebug(enable bool) { s.debug = enable } func (s *Server) EnableStatus(path string) { } func (s *Server) SetTheme(themePath string) { s.theme = path.Clean(themePath) } func (s *Server) SetTemplatePath(tmplPath string) { s.templates = path.Clean(tmplPath) } func (s *Server) Router() *mux.Router { return s.router } func (s *Server) ServeTLS(keyPath string, certPath string) error { kpr, err := autocert.NewManager(certPath, keyPath) if err != nil { s.log.Fatalf("%v\n", err) } s.http = &http.Server{ Handler: s.router, Addr: s.address, // Good practice: enforce timeouts for servers you create! WriteTimeout: 120 * time.Second, ReadTimeout: 120 * time.Second, TLSConfig: &tls.Config{}, } s.http.TLSConfig.GetCertificate = kpr.GetCertificateFunc() return s.http.ListenAndServeTLS("", "") } func (s *Server) ServeTLSRedirect(address string) error { s.http = &http.Server{ Addr: address, // Good practice: enforce timeouts for servers you create! WriteTimeout: 120 * time.Second, ReadTimeout: 120 * time.Second, } return s.http.ListenAndServe() } // Serve content forever func (s *Server) Serve() error { s.http = &http.Server{ Handler: s.router, Addr: s.address, // Good practice: enforce timeouts for servers you create! WriteTimeout: 120 * time.Second, ReadTimeout: 120 * time.Second, } return s.http.ListenAndServe() } func (s *Server) WithStaticFiles(prefix string) *Server { s.router.PathPrefix(prefix).Handler(http.FileServer(s.fs)) return s } func (s *Server) WithTheme(themeURL string) *Server { s.theme = path.Clean(themeURL) return s } func (s *Server) EnableTestMode(enable bool) *Server { s.testModeEnabled = enable return s } func (s *Server) WithRootFileSystem(fs http.FileSystem) *Server { s.fs = fs return s } func (s *Server) WithDebug(debugURL string) *Server { sub := s.router.PathPrefix(debugURL).Subrouter() setupDebugHandler(sub) return s } func (s *Server) WithMetadata(meta map[string]string) *Server { s.meta = meta return s } func (s *Server) WithTemplateFuncs(funcs template.FuncMap) *Server { for k, f := range funcs { s.templateFuncs[k] = f } return s } func (s *Server) WithAuth(auth auth.Authenticator) *Server { s.auth = auth return s } func (s *Server) WithVersion(version int64) *Server { s.version = version return s } func (s *Server) WithHealthCheck(version, date string, status func() (bool, string)) { s.HandleFunc("/_health", func(c *Context) { ok, msg := status() hc := struct { Version string Date string Status string }{ Version: version, Date: date, Status: msg, } if ok { c.ReplyObject(&hc) return } c.ErrorObject(http.StatusServiceUnavailable, &hc) }) } func (s *Server) SetLogger(logger log.Logger) { s.log = logger } func (s *Server) Dump() { fmt.Printf(" Theme: %s\n", s.theme) fmt.Printf(" Templates: %s\n", s.templates) } func (s *Server) Shutdown(ctx context.Context) { if s.http != nil { s.http.Shutdown(ctx) } } func New(address string, path string, auth auth.Authenticator) *Server { s := Server{ router: mux.NewRouter(), auth: auth, address: address, fs: http.FileSystem(http.Dir(path)), templates: "/templates", templateFuncs: builtinFuncMap, theme: "/static/css/default.css", meta: make(map[string]string), log: log.WithName("SNAP"), version: time.Now().Unix(), } return &s }