snap/server.go

449 lines
9.9 KiB
Go
Raw Normal View History

2018-01-24 02:26:26 +00:00
package snap
import (
2024-03-21 18:09:05 +00:00
"context"
2021-06-18 19:43:52 +00:00
"crypto/tls"
"encoding/json"
"fmt"
2018-01-24 02:26:26 +00:00
"html/template"
"io"
"io/ioutil"
2018-01-24 02:26:26 +00:00
"net/http"
"os"
2021-01-19 03:00:46 +00:00
"path"
2018-01-24 02:26:26 +00:00
"strings"
"time"
2018-02-11 00:17:14 +00:00
2023-05-23 14:09:57 +00:00
"git.twelvetwelve.org/library/core/log"
2018-02-07 22:25:00 +00:00
2023-05-23 14:09:57 +00:00
"git.twelvetwelve.org/library/snap/pkg/autocert"
"github.com/gorilla/mux"
2023-05-23 14:09:57 +00:00
"git.twelvetwelve.org/library/snap/auth"
2018-01-24 02:26:26 +00:00
)
type Server struct {
2024-03-21 18:09:05 +00:00
address string
theme string
debug bool
fs http.FileSystem
auth auth.Authenticator
router *mux.Router
http *http.Server
2021-05-29 21:14:15 +00:00
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
2018-01-24 02:26:26 +00:00
}
type SnapBaseContent struct {
2018-03-24 17:31:10 +00:00
Theme string
Content interface{}
}
2021-05-29 21:14:15 +00:00
func noescape(str string) template.HTML {
return template.HTML(str)
}
var builtinFuncMap = template.FuncMap{
"noescape": noescape,
}
2018-03-24 17:31:10 +00:00
func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http.Request) *Context {
c := &Context{
2018-03-24 17:31:10 +00:00
r: r,
w: w,
srv: s,
auth: auth,
}
return c
}
2021-05-29 21:14:15 +00:00
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:
2021-05-29 21:14:15 +00:00
c := s.makeContext(rec, w, r)
if loginHandler(c) {
handle(c)
}
return
default:
http.Error(w, http.StatusText(code), code)
2021-05-29 21:14:15 +00:00
}
}
}
2021-01-19 03:00:46 +00:00
func (s *Server) authenticated(auth auth.Authenticator, redirect string, handle func(c *Context)) http.HandlerFunc {
2018-02-07 22:25:00 +00:00
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:
2021-01-19 03:00:46 +00:00
if redirect != "" {
http.Redirect(w, r, redirect, http.StatusSeeOther)
} else {
2021-01-19 03:23:52 +00:00
http.Error(w, "Not authorized", http.StatusUnauthorized)
2021-01-19 03:00:46 +00:00
}
return
default:
http.Error(w, http.StatusText(code), code)
2018-02-07 22:25:00 +00:00
}
}
}
func (s *Server) wrapper(handle func(c *Context)) http.HandlerFunc {
2018-02-07 22:25:00 +00:00
return func(w http.ResponseWriter, r *http.Request) {
2018-03-24 17:31:10 +00:00
c := s.makeContext(nil, w, r)
2021-01-19 05:33:12 +00:00
if s.auth != nil {
if rec, code := s.auth.DoAuth(w, r); code == http.StatusOK {
2021-01-19 05:33:12 +00:00
c.auth = rec
}
}
2018-02-07 22:25:00 +00:00
handle(c)
// discard the rest of the body content
io.Copy(ioutil.Discard, r.Body)
defer r.Body.Close()
2018-02-07 22:25:00 +00:00
}
2018-01-24 02:26:26 +00:00
}
// This is a bit different then the standard template.parseFiles code in that it gives us hiarchial templates
2023-05-23 14:09:57 +00:00
//
// 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 {
2021-01-30 05:45:18 +00:00
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 {
2021-05-29 21:14:15 +00:00
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
}
2021-01-30 05:45:18 +00:00
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) {
2021-05-29 21:14:15 +00:00
tmpl := template.New("").Funcs(s.templateFuncs)
2018-01-24 02:26:26 +00:00
2021-01-30 05:45:18 +00:00
err := Walk(fs, base, func(path string, info os.FileInfo, err error) error {
2018-01-24 02:26:26 +00:00
if strings.Contains(path, ".html") {
_, err := s.parseTemplates(tmpl, path)
if err != nil {
2023-05-23 14:09:57 +00:00
log.Printf("%+v\n", err)
}
2018-01-24 02:26:26 +00:00
}
return err
})
2021-01-30 05:45:18 +00:00
if err != nil {
return nil, err
}
return tmpl, nil
}
func (s *Server) loadTemplates() *template.Template {
tmpl, err := s.LoadTemplatesFS(s.fs, s.templates)
2018-01-24 02:26:26 +00:00
if err != nil {
2023-05-23 14:09:57 +00:00
log.Fatalf("loadTemplates %v %v\n", err, s.templates)
2018-01-24 02:26:26 +00:00
}
return tmpl
}
func (s *Server) getTemplates() *template.Template {
2018-01-24 02:26:26 +00:00
if s.debug {
return s.loadTemplates()
}
if s.cachedTmpl == nil {
s.cachedTmpl = s.loadTemplates()
}
return s.cachedTmpl
}
2018-03-24 17:31:10 +00:00
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 {
2023-05-23 14:09:57 +00:00
log.Warnf("%s\n", err)
}
2018-01-24 02:26:26 +00:00
}
func (s *Server) reply(w http.ResponseWriter, msg string) {
w.Write([]byte(msg))
}
func (s *Server) renderError(w http.ResponseWriter, code int, msg string) {
2018-02-11 00:17:14 +00:00
w.WriteHeader(code)
w.Write([]byte(msg))
}
2021-01-19 03:23:52 +00:00
func (s *Server) HandleFuncAuthenticated(path, redirect string, f func(c *Context)) *mux.Route {
2018-01-24 02:26:26 +00:00
if s.auth == nil {
2018-02-11 00:17:14 +00:00
return nil
2018-01-24 02:26:26 +00:00
}
2021-01-19 03:23:52 +00:00
return s.router.HandleFunc(path, s.authenticated(s.auth, redirect, f))
2018-02-07 22:25:00 +00:00
}
2021-05-29 21:14:15 +00:00
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))
}
2021-01-19 03:00:46 +00:00
func (s *Server) HandleFuncCustomAuth(auth auth.Authenticator, path, redirect string, f func(c *Context)) *mux.Route {
if auth == nil {
2023-05-23 14:09:57 +00:00
log.Warnf("Nil auth on %s\n", path)
return nil
}
2021-01-19 03:23:52 +00:00
return s.router.HandleFunc(path, s.authenticated(auth, redirect, f))
}
2018-02-11 00:17:14 +00:00
func (s *Server) HandleFunc(path string, f func(c *Context)) *mux.Route {
return s.router.HandleFunc(path, s.wrapper(f))
2018-01-24 02:26:26 +00:00
}
2021-06-18 19:43:52 +00:00
func (s *Server) AddRoute(path string, r *RouteBuilder) *mux.Route {
return s.router.HandleFunc(path, r.BuildRoute(s))
}
func (s *Server) SetDebug(enable bool) {
2018-02-07 22:25:00 +00:00
s.debug = enable
}
func (s *Server) EnableStatus(path string) {
}
2021-01-19 03:00:46 +00:00
func (s *Server) SetTheme(themePath string) {
s.theme = path.Clean(themePath)
}
2021-01-19 03:00:46 +00:00
func (s *Server) SetTemplatePath(tmplPath string) {
s.templates = path.Clean(tmplPath)
2018-03-24 17:31:10 +00:00
}
func (s *Server) Router() *mux.Router {
return s.router
}
func (s *Server) ServeTLS(keyPath string, certPath string) error {
2021-06-18 19:43:52 +00:00
kpr, err := autocert.NewManager(certPath, keyPath)
if err != nil {
2023-05-23 14:09:57 +00:00
log.Fatalf("%v\n", err)
2021-06-18 19:43:52 +00:00
}
2024-03-21 18:09:05 +00:00
s.http = &http.Server{
2018-01-24 02:26:26 +00:00
Handler: s.router,
Addr: s.address,
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 120 * time.Second,
ReadTimeout: 120 * time.Second,
2021-06-18 19:43:52 +00:00
TLSConfig: &tls.Config{},
2018-01-24 02:26:26 +00:00
}
2024-03-21 18:09:05 +00:00
s.http.TLSConfig.GetCertificate = kpr.GetCertificateFunc()
return s.http.ListenAndServeTLS("", "")
2021-06-18 19:43:52 +00:00
}
2018-01-24 02:26:26 +00:00
2021-06-18 19:43:52 +00:00
func (s *Server) ServeTLSRedirect(address string) error {
2024-03-21 18:09:05 +00:00
s.http = &http.Server{
2021-06-18 19:43:52 +00:00
Addr: address,
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 120 * time.Second,
ReadTimeout: 120 * time.Second,
}
2024-03-21 18:09:05 +00:00
return s.http.ListenAndServe()
2018-01-24 02:26:26 +00:00
}
2024-03-21 18:09:05 +00:00
// Serve content forever
func (s *Server) Serve() error {
2024-03-21 18:09:05 +00:00
s.http = &http.Server{
2018-01-24 02:26:26 +00:00
Handler: s.router,
Addr: s.address,
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 120 * time.Second,
ReadTimeout: 120 * time.Second,
2018-01-24 02:26:26 +00:00
}
2024-03-21 18:09:05 +00:00
return s.http.ListenAndServe()
2018-01-24 02:26:26 +00:00
}
2021-01-30 05:45:18 +00:00
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 {
2021-01-19 03:00:46 +00:00
s.theme = path.Clean(themeURL)
return s
}
func (s *Server) EnableTestMode(enable bool) *Server {
s.testModeEnabled = enable
return s
}
2021-01-31 00:34:04 +00:00
func (s *Server) WithRootFileSystem(fs http.FileSystem) *Server {
s.fs = fs
return s
}
func (s *Server) WithDebug(debugURL string) *Server {
2021-01-19 03:00:46 +00:00
sub := s.router.PathPrefix(debugURL).Subrouter()
setupDebugHandler(sub)
return s
}
2021-05-29 21:14:15 +00:00
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)
})
}
2021-01-19 03:00:46 +00:00
func (s *Server) Dump() {
2021-01-19 03:23:52 +00:00
fmt.Printf(" Theme: %s\n", s.theme)
fmt.Printf(" Templates: %s\n", s.templates)
2021-01-19 03:00:46 +00:00
}
2024-03-21 18:09:05 +00:00
func (s *Server) Shutdown(ctx context.Context) {
if s.http != nil {
s.http.Shutdown(ctx)
}
}
2021-01-19 03:00:46 +00:00
func New(address string, path string, auth auth.Authenticator) *Server {
s := Server{
2021-05-29 21:14:15 +00:00
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),
2018-01-24 02:26:26 +00:00
}
return &s
}