diff options
| -rw-r--r-- | .golangci.yml | 2 | ||||
| -rw-r--r-- | cmd/server/app.go | 18 | ||||
| -rw-r--r-- | cmd/server/auth.go | 108 | ||||
| -rw-r--r-- | cmd/server/dashboard.go | 5 | ||||
| -rw-r--r-- | cmd/server/edit.go | 90 | ||||
| -rw-r--r-- | cmd/server/feed.go | 2 | ||||
| -rw-r--r-- | cmd/server/handlers.go | 7 | ||||
| -rw-r--r-- | cmd/server/main.go | 7 | ||||
| -rw-r--r-- | cmd/server/middleware.go | 142 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | page/page.go | 58 | ||||
| -rw-r--r-- | page/render.go | 137 | ||||
| -rw-r--r-- | users/main.go | 43 |
14 files changed, 589 insertions, 42 deletions
diff --git a/.golangci.yml b/.golangci.yml index 319e4af..2201c93 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,7 +27,7 @@ linters: - godot - godox - gofmt - - gofumpt + # - gofumpt - goheader # - goimports - golint diff --git a/cmd/server/app.go b/cmd/server/app.go index 4f62a09..d4977ab 100644 --- a/cmd/server/app.go +++ b/cmd/server/app.go @@ -16,11 +16,15 @@ type App struct { redisPool *redis.Pool RedisKey string - ReIndexPath string - StaticDirectory string - BaseTemplate string - DocumentSplit string - Suffix string + ReIndexPath string + StaticDirectory string + BaseTemplate string + TemplateDirectory string + DocumentSplit string + Suffix string + + // Related to user authentication + auth *Auth // Related to the Atom feed Title string @@ -59,6 +63,10 @@ func loadConf(fn string) (*App, error) { page.BaseTemplate = app.BaseTemplate } + if app.TemplateDirectory != "" { + page.TemplateDirectory = app.TemplateDirectory + } + if app.DocumentSplit != "" { page.DocumentSplit = app.DocumentSplit } diff --git a/cmd/server/auth.go b/cmd/server/auth.go new file mode 100644 index 0000000..caade97 --- /dev/null +++ b/cmd/server/auth.go @@ -0,0 +1,108 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "riedstra.dev/mitch/go-website/users" +) + +type Auth struct { + Users []*users.SiteUser `json:"Users"` + // How long are JWTs valid? + LoginHours int `json:"LoginHours"` + // JWT secret + TokenKey string `json:"TokenKey"` + // Are cookies HTTP only? + HTTPOnly bool `json:"HTTPOnly"` + // Do they require HTTPs? + Secure bool `json:"SecureCookie"` + // See https://pkg.go.dev/net/http#SameSite + // You probably want this set to 3 + SameSiteStrict http.SameSite `json:"SameSiteStrict"` +} + +func GenTokenKey() string { + r := make([]byte, 16) // 128 bits + _, err := rand.Read(r) + + if err != nil { + // Not my favorite thing, but I consider this to be a + // critical issue + panic(fmt.Errorf("reading random bytes: %w", err)) + } + + return base64.RawURLEncoding.EncodeToString(r) +} + +func (a *App) ReadAuth(fn string) error { //nolint + auth := &Auth{ + Users: []*users.SiteUser{ + { + Username: "admin", + Password: "admin", + }, + }, + LoginHours: 1, + TokenKey: GenTokenKey(), + HTTPOnly: true, + Secure: true, + SameSiteStrict: http.SameSiteStrictMode, + } + + var dec *json.Decoder + + fh, err := os.Open(fn) + if err != nil { + if strings.Contains(err.Error(), "no such file") { + goto write + } + + return fmt.Errorf("opening %s: %w", fn, err) + } + + dec = json.NewDecoder(fh) + dec.DisallowUnknownFields() + + err = dec.Decode(auth) + if err != nil { + return fmt.Errorf("decoding file %s: %w", fn, err) + } + + err = fh.Close() + if err != nil { + return fmt.Errorf("closing file %s: %w", fn, err) + } + + for _, u := range auth.Users { + err = u.SetPasswordHashIfNecessary() + if err != nil { + return fmt.Errorf("setting password hash for: %s: %w", + u.Username, err) + } + } + +write: + fh, err = os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + + if err != nil { + return fmt.Errorf("opening file %s: %w", fn, err) + } + + enc := json.NewEncoder(fh) + enc.SetIndent("", " ") + + err = enc.Encode(auth) + if err != nil { + return fmt.Errorf("encoding to file %s: %w", fn, err) + } + + a.auth = auth + + return nil +} diff --git a/cmd/server/dashboard.go b/cmd/server/dashboard.go new file mode 100644 index 0000000..a07e722 --- /dev/null +++ b/cmd/server/dashboard.go @@ -0,0 +1,5 @@ +package main + +// func (a *App) DashboardHandler(w http.ResponseWriter, r *http.Request) { +// page.Render(" +// } diff --git a/cmd/server/edit.go b/cmd/server/edit.go new file mode 100644 index 0000000..730e6e9 --- /dev/null +++ b/cmd/server/edit.go @@ -0,0 +1,90 @@ +package main + +import ( + "bytes" + "html" + "io" + "log" + "net/http" + "os" + "path/filepath" + + "riedstra.dev/mitch/go-website/page" +) + +func (a *App) EditPage(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + page.Render(w, r, page.TemplateDirectory+"/4xx", map[string]interface{}{ + "Title": "Method Not allowed", + "Description": "Method not allowed", + }, http.StatusMethodNotAllowed) + + return + } + + p := r.URL.Path + + p = filepath.Clean(p) + + fh, err := os.Open("./" + p + page.Suffix) + if err != nil { + log.Printf("opening page: %s", err) + a.Err500Default(w, r) + + return + } + + b, err := io.ReadAll(fh) + if err != nil { + log.Printf("opening page: %s", err) + a.Err500Default(w, r) + + return + } + + page.Render(w, r, page.TemplateDirectory+"/edit", map[string]interface{}{ + "Page": p, + "Content": html.EscapeString(string(b)), + }, http.StatusOK) +} + +func (a *App) SaveEditPage(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + a.Err500Default(w, r) + + return + } + + p := r.URL.Path + content := r.FormValue("content") + + content = html.UnescapeString(content) + p = filepath.Clean(p) + + fn := "./" + p + page.Suffix + + fh, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Printf("opening file %s for writing: %s", fn, err) + a.Err500Default(w, r) + + return + } + + c := bytes.ReplaceAll([]byte(content), []byte{'\r'}, []byte{}) + + _, err = fh.Write(c) + if err != nil { + log.Printf("opening file %s for writing: %s", fn, err) + a.Err500Default(w, r) + + return + } + + http.Redirect(w, r, "/"+r.URL.Path, http.StatusFound) + + err = a.ClearRedis() // Clear out the cache if any + if err != nil { + log.Printf("after editing %s: %s", fn, err) + } +} diff --git a/cmd/server/feed.go b/cmd/server/feed.go index 880eeb4..212e5da 100644 --- a/cmd/server/feed.go +++ b/cmd/server/feed.go @@ -203,7 +203,7 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { //nolint:fun } entry := Entry{ - Title: p.Title, + Title: p.Title(), Updated: &p.Date.Time, Links: []Link{{Href: strings.Join([]string{a.SiteURL, p.Path()}, "/")}}, } diff --git a/cmd/server/handlers.go b/cmd/server/handlers.go index 205d462..1711ae3 100644 --- a/cmd/server/handlers.go +++ b/cmd/server/handlers.go @@ -15,6 +15,13 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { rtr.HandleFunc(a.ReIndexPath, a.RebuildIndexHandler) rtr.PathPrefix("/static/").Handler(a.StaticHandler()) + rtr.HandleFunc("/login", a.LoginHandler) + rtr.Handle("/logout", a.RequiresLogin(http.HandlerFunc(a.LogoutHandler))) + rtr.PathPrefix("/edit/").Handler( + a.RequiresLogin(http.StripPrefix("/edit/", http.HandlerFunc(a.EditPage)))).Methods("GET") + rtr.PathPrefix("/edit/").Handler( + a.RequiresLogin(http.StripPrefix("/edit/", http.HandlerFunc(a.SaveEditPage)))).Methods("POST") + if a.redisPool != nil { rtr.PathPrefix(fmt.Sprintf("/%s/{tag}", a.FeedPrefix)).Handler( rediscache.HandleWithParams(a.redisPool, a.RedisKey, diff --git a/cmd/server/main.go b/cmd/server/main.go index 912cba0..ee8cf6f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -28,6 +28,8 @@ func main() { //nolint:funlen directory := fl.String("d", ".", "Directory to serve.") version := fl.Bool("v", false, "Print the version then exit") confFn := fl.String("c", "conf.yml", "Location for the config file") + authConfFn := fl.String("ac", "auth.json", + "Location for authorization configuration file") verbose := fl.Bool("V", false, "Be more verbose ( dump config, etc ) ") fl.StringVar(&page.TimeFormat, "T", page.TimeFormat, "Set the page time format, be careful with this") @@ -61,6 +63,11 @@ func main() { //nolint:funlen app = &App{} } + err = app.ReadAuth(*authConfFn) + if err != nil { + log.Println(err) + } + if app.ReIndexPath == "" || *indexPath != defaultIndexPath { app.ReIndexPath = *indexPath } diff --git a/cmd/server/middleware.go b/cmd/server/middleware.go new file mode 100644 index 0000000..5e4bf26 --- /dev/null +++ b/cmd/server/middleware.go @@ -0,0 +1,142 @@ +package main + +import ( + "log" + "net/http" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "riedstra.dev/mitch/go-website/page" + "riedstra.dev/mitch/go-website/users" +) + +func (a *App) Err5xx(w http.ResponseWriter, r *http.Request, + statusCode int, title, desc string) { + page.Render5xx(w, r, map[string]interface{}{ + "Error": title, + "Description": desc, + }, statusCode) +} + +func (a *App) Err500Default(w http.ResponseWriter, r *http.Request) { + a.Err5xx(w, r, http.StatusInternalServerError, "Internal server error", + "Internal server error.") +} + +func (a *App) LogoutHandler(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: "Auth", + HttpOnly: a.auth.HTTPOnly, + SameSite: a.auth.SameSiteStrict, + Secure: a.auth.Secure, + Value: "logout", + Expires: time.Now().Add(time.Second * 15), //nolint + }) + + http.Redirect(w, r, "/", http.StatusFound) +} + +func (a *App) LoginHandler(w http.ResponseWriter, r *http.Request) { //nolint + if r.Method == "GET" { + page.RenderForPath(w, r, "login") + + return + } + + if r.Method != "POST" { + a.Err500Default(w, r) + + return + } + + log.Printf("made it") + + username := r.FormValue("username") + password := r.FormValue("password") + + var ( + err error = nil + u *users.SiteUser + found = false + ) + + for _, u = range a.auth.Users { + if u.Username == username { + err = u.CheckPassword(password) + found = true + } + } + + if err != nil || !found { + page.Render(w, r, "login", map[string]interface{}{ + "Error": "Invalid username or password", + "Username": username, + }, http.StatusUnauthorized) + + return + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS512, &jwt.StandardClaims{ + ExpiresAt: time.Now().Add( + time.Hour * time.Duration(a.auth.LoginHours)).Unix(), + Id: u.Username, + }) + + ss, err := token.SignedString([]byte(a.auth.TokenKey)) + if err != nil { + log.Println("login: encountered while setting up JWT: ", err) + a.Err500Default(w, r) + + return + } + + log.Println(ss) + + http.SetCookie(w, &http.Cookie{ + Name: "Auth", + HttpOnly: a.auth.HTTPOnly, + SameSite: a.auth.SameSiteStrict, + Secure: a.auth.Secure, + Value: ss, + }) + + http.Redirect(w, r, "/login", http.StatusFound) +} + +func (a *App) RequiresLogin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mustLoginResp := func() { + page.Render(w, r, "login", map[string]interface{}{ + "Error": "You must login to view this page", + }, http.StatusUnauthorized) + } + + c, err := r.Cookie("Auth") + if err != nil { + mustLoginResp() + + return + } + + token, err := jwt.Parse(c.Value, + func(token *jwt.Token) (interface{}, error) { + return []byte(a.auth.TokenKey), nil + }, + ) + + if err != nil { + log.Printf("Unauthorized request %s %s", r.Method, r.URL.Path) + mustLoginResp() + + return + } + + if !token.Valid { + mustLoginResp() + + return + } + + next.ServeHTTP(w, r) + }) +} @@ -3,12 +3,14 @@ module riedstra.dev/mitch/go-website go 1.13 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gomodule/redigo v1.8.5 // indirect github.com/gorilla/mux v1.8.0 github.com/kr/pretty v0.1.0 // indirect github.com/russross/blackfriday v2.0.0+incompatible github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc= @@ -25,11 +27,19 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/page/page.go b/page/page.go index b2cc3e1..adb2362 100644 --- a/page/page.go +++ b/page/page.go @@ -46,11 +46,11 @@ import ( // The exported fields can be filled in the yaml at the top of a page and // utilized within. type Page struct { - path string - Title string - Description string - AuthorName string - AuthorEmail string + path string + DefaultTitle string `yaml:"title"` + DefaultDescription string `yaml:"description"` + AuthorName string + AuthorEmail string // Tags to apply to the page in question. Useful for Index() Tags map[string]interface{} Date *PageTime @@ -76,8 +76,14 @@ var CacheIndex = true // backend. var FileSystem = os.DirFS(".") +// TemplateDirectory is the parent directory which templates are stored, +// usually the BaseTemplate is stored here as well, though it does not +// have to be. Currently this is used for the 4xx and 5xx pages. ( 4xx.md +// and 5xx.md respectively. +var TemplateDirectory = "tpl" + // BaseTemplate can be adjusted to change the base template used in rendering. -var BaseTemplate = "inc/base.html" +var BaseTemplate = "tpl/base.md" // Suffix is applied to all pages for reading off of the disk. var Suffix = ".md" @@ -107,6 +113,46 @@ func (p Page) Path() string { return filepath.ToSlash(p.path) } +// Title returns `DefaultTitle` unless `.Vars.Title` is +// set, then it returns that instead. +func (p Page) Title() string { + if p.Vars == nil { + return p.DefaultTitle + } + + t, ok := p.Vars["Title"] + if !ok { + return p.DefaultTitle + } + + nt, ok := t.(string) + if !ok { + return p.DefaultTitle + } + + return nt +} + +// Description returns `DefaultDescription` unless `.Vars.Description` is +// set, then it returns that instead. +func (p Page) Description() string { + if p.Vars == nil { + return p.DefaultDescription + } + + t, ok := p.Vars["Description"] + if !ok { + return p.DefaultDescription + } + + nt, ok := t.(string) + if !ok { + return p.DefaultDescription + } + + return nt +} + // Global is specifically for use inside of a page markdown file or // in a base template. This simply returns the package Global variable. func (p *Page) Global() interface{} { diff --git a/page/render.go b/page/render.go index 6cbabf9..d35ba38 100644 --- a/page/render.go +++ b/page/render.go @@ -1,20 +1,104 @@ package page import ( + "bytes" "net/http" "path/filepath" "strings" ) -// Render is a lower level option, allowing you to specify local -// variables and the status code in which to return. -func Render(w http.ResponseWriter, r *http.Request, - path string, vars map[string]interface{}, statusCode int) { +func getURLPath(r *http.Request) string { u := r.URL.Path if u == "/" { u = "/index" } + return u +} + +// Render5xx is automatically called if any render fails, +// additionally you can call it for your own uses. It will +// try to use the 5xx template but will fall back to a plain +// page if that fails. +func Render5xx(w http.ResponseWriter, r *http.Request, + vars map[string]interface{}, statusCode int) { + u := getURLPath(r) + + Logger.Printf("%s %s %d %s", + r.RemoteAddr, + r.Method, + statusCode, + u) + + p := NewPage(TemplateDirectory + "/5xx") + + buf := &bytes.Buffer{} + + err := p.Render(buf) + if err != nil { + Logger.Printf("%s %s path: %s while trying 5xx: %s", + r.RemoteAddr, + r.Method, + u, + err) + http.Error(w, "Internal server error", statusCode) + + return + } + + w.WriteHeader(statusCode) + + _, _ = w.Write(buf.Bytes()) + + Logger.Printf("%s %s %d %s", r.RemoteAddr, r.Method, statusCode, u) +} + +// Render4xx is mostly used to render a 404 page, pulls from 404 template. +// automatically called by Render if a page is missing. +func Render4xx(w http.ResponseWriter, r *http.Request, + vars map[string]interface{}, statusCode int) { + u := getURLPath(r) + + Logger.Printf("%s %s %d %s", + r.RemoteAddr, + r.Method, + statusCode, + u) + + p := NewPage(TemplateDirectory + "/4xx") + + buf := &bytes.Buffer{} + + err := p.Render(buf) + if err != nil { + Logger.Printf("%s %s path: %s while trying 404: %s", + r.RemoteAddr, + r.Method, + u, + err) + Render5xx(w, r, vars, http.StatusInternalServerError) + + return + } + + w.WriteHeader(statusCode) + + _, err = w.Write(buf.Bytes()) + if err != nil { + Render5xx(w, r, nil, http.StatusInternalServerError) + + return + } + + Logger.Printf("%s %s %d %s", r.RemoteAddr, r.Method, statusCode, u) +} + +// Render is a lower level option, allowing you to specify local +// variables and the status code in which to return. +func Render(w http.ResponseWriter, r *http.Request, + path string, vars map[string]interface{}, statusCode int) { + u := getURLPath(r) + u = filepath.Join(".", u) // Sepcifically use the specified path for the page @@ -24,42 +108,37 @@ func Render(w http.ResponseWriter, r *http.Request, p.Vars = vars } - err := p.Render(w) + buf := &bytes.Buffer{} + + err := p.Render(buf) if err != nil { if strings.HasSuffix(err.Error(), "no such file or directory") { - Logger.Printf("%s %s %d %s", - r.RemoteAddr, - r.Method, - http.StatusNotFound, - u) + Render4xx(w, r, vars, http.StatusNotFound) - p = NewPage("404") - - w.WriteHeader(http.StatusNotFound) + return + } - err := p.Render(w) - if err != nil { - Logger.Printf("%s %s path: %s while trying 404: %s", - r.RemoteAddr, - r.Method, - u, - err) - http.Error(w, "Internal server error", - http.StatusInternalServerError) + Logger.Printf("%s %s path: %s rendering encountered: %s", + r.RemoteAddr, + r.Method, + u, + err) + Render5xx(w, r, vars, http.StatusInternalServerError) - return - } + return + } - return - } + w.WriteHeader(statusCode) - Logger.Printf("%s %s path: %s encountered: %s", + _, err = w.Write(buf.Bytes()) + if err != nil { + Logger.Printf("%s %s %d %s: while writing buf: %s", r.RemoteAddr, r.Method, + statusCode, u, err) - http.Error(w, "Internal server error", - http.StatusInternalServerError) + Render5xx(w, r, nil, http.StatusInternalServerError) return } diff --git a/users/main.go b/users/main.go new file mode 100644 index 0000000..edb306f --- /dev/null +++ b/users/main.go @@ -0,0 +1,43 @@ +package users + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +type SiteUser struct { + Username string `yaml:"username"` + PasswordHash string `yaml:"password-hash"` + Password string `yaml:"password"` +} + +func (su *SiteUser) SetPasswordHashIfNecessary() error { + if su.Password == "" { + return nil + } + + res, err := bcrypt.GenerateFromPassword([]byte(su.Password), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("SetPasswordHashIfNecessary: %w", err) + } + + su.Password = "" + su.PasswordHash = string(res) + + return nil +} + +func (su *SiteUser) CheckPassword(pass string) error { + err := su.SetPasswordHashIfNecessary() + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword([]byte(su.PasswordHash), []byte(pass)) + if err != nil { + return fmt.Errorf("CheckPassword: %w", err) + } + + return nil +} |
