123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- package snap
- import (
- "crypto/tls"
- "encoding/json"
- "fmt"
- "html/template"
- "io"
- "io/ioutil"
- "net/http"
- "os"
- "path"
- "strings"
- "time"
- "git.thirdmartini.com/pub/snap/pkg/autocert"
- "github.com/gorilla/mux"
- "git.thirdmartini.com/pub/fancylog"
- "git.thirdmartini.com/pub/snap/auth"
- )
- type Server struct {
- address string
- theme string
- debug bool
- fs http.FileSystem
- auth auth.Authenticator
- router *mux.Router
- 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
- }
- 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,
- vars: mux.Vars(r),
- }
- 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) {
- if s.debug {
- log.Debug("authenticated request with login ui handler: ", r.RequestURI)
- }
- rec, ok := auth.DoAuth(w, r)
- if !ok {
- log.Debug("authenticated request with login ui handler to login ", r.RequestURI)
- c := s.makeContext(rec, w, r)
- if loginHandler(c) {
- handle(c)
- }
- } else {
- c := s.makeContext(rec, w, r)
- handle(c)
- }
- }
- }
- func (s *Server) authenticated(auth auth.Authenticator, redirect string, handle func(c *Context)) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if s.debug {
- log.Debug("authenticated request: ", r.RequestURI)
- }
- rec, ok := auth.DoAuth(w, r)
- if !ok {
- if redirect != "" {
- http.Redirect(w, r, redirect, http.StatusSeeOther)
- } else {
- http.Error(w, "Not authorized", http.StatusUnauthorized)
- }
- } else {
- c := s.makeContext(rec, w, r)
- handle(c)
- }
- }
- }
- func (s *Server) wrapper(handle func(c *Context)) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if s.debug {
- log.Debug("request: ", r.RequestURI)
- }
- c := s.makeContext(nil, w, r)
- if s.auth != nil {
- if rec, ok := s.auth.DoAuth(w, r); ok {
- 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 {
- log.Println(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 {
- log.Fatal("loadTemplates", 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 {
- log.Warn(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 {
- log.Warn("Nil auth on", 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 {
- log.Fatal(err)
- }
- srv := &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{},
- }
- srv.TLSConfig.GetCertificate = kpr.GetCertificateFunc()
- return srv.ListenAndServeTLS("", "")
- }
- func (s *Server) ServeTLSRedirect(address string) error {
- srv := &http.Server{
- Addr: address,
- // Good practice: enforce timeouts for servers you create!
- WriteTimeout: 120 * time.Second,
- ReadTimeout: 120 * time.Second,
- }
- return srv.ListenAndServe()
- }
- // Serve serve content forever
- func (s *Server) Serve() error {
- srv := &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 srv.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) 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) Dump() {
- fmt.Printf(" Theme: %s\n", s.theme)
- fmt.Printf(" Templates: %s\n", s.templates)
- }
- 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),
- }
- return &s
- }
|