diff options
| author | Leonardo Bishop <me@leonardobishop.net> | 2026-03-19 17:14:20 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.net> | 2026-03-19 17:14:20 +0000 |
| commit | 66bd5d2f7fd84eec39d69f4a8f5c435fc978804f (patch) | |
| tree | 20bfd8637cce28750b4a66bdd9aa47cb9831308b /pkg | |
| parent | 60cd7875c2c9ee595012078a3ba8f13b71c73dc9 (diff) | |
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/auth/provider.go | 6 | ||||
| -rw-r--r-- | pkg/config/site.go | 5 | ||||
| -rw-r--r-- | pkg/html/password.go | 47 | ||||
| -rw-r--r-- | pkg/server/serve.go | 79 | ||||
| -rw-r--r-- | pkg/site/fs.go | 6 | ||||
| -rw-r--r-- | pkg/site/site.go | 11 |
6 files changed, 141 insertions, 13 deletions
diff --git a/pkg/auth/provider.go b/pkg/auth/provider.go index 0d515ab..e521c8c 100644 --- a/pkg/auth/provider.go +++ b/pkg/auth/provider.go @@ -1,6 +1,7 @@ package auth import ( + "crypto/rand" "time" "github.com/golang-jwt/jwt/v5" @@ -11,9 +12,12 @@ type Authenticator struct { parser *jwt.Parser } -func NewAuthenticator(secretKey []byte) *Authenticator { +func NewAuthenticator() *Authenticator { parser := jwt.NewParser(jwt.WithIssuer("scrapbook"), jwt.WithExpirationRequired()) + secretKey := make([]byte, 32) + rand.Read(secretKey) + a := &Authenticator{ secretKey: secretKey, parser: parser, diff --git a/pkg/config/site.go b/pkg/config/site.go index 4b36f1a..bcdbe86 100644 --- a/pkg/config/site.go +++ b/pkg/config/site.go @@ -17,9 +17,10 @@ const ( ) type SiteConfig struct { - Host string + Flags SiteFlag `scfg:"flags"` - Flags SiteFlag + Host string `scfg:"host"` + Password string `scfg:"password"` } func ReadSiteConfig(filePath string, dst *SiteConfig) error { diff --git a/pkg/html/password.go b/pkg/html/password.go new file mode 100644 index 0000000..ba23a06 --- /dev/null +++ b/pkg/html/password.go @@ -0,0 +1,47 @@ +package html + +import ( + "net/url" + + . "github.com/LMBishop/scrapbook/web/skeleton" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func AuthenticateSitePage(err, redirect, siteName string) Node { + return Page("Authenticate", + H1(Text("A password is required to visit this site")), + + If(err != "", AlertError(err)), + + Form( + Action("/authenticate?redirect="+url.QueryEscape(redirect)), + Method("post"), + + FieldSet( + Legend(Text("Authentication")), + Label( + For("password"), + Text("Password"), + ), + Input( + ID("password"), + Name("password"), + Type("password"), + ), + Span( + Class("form-help"), + Text("Enter the password to continue."), + ), + ), + + Div( + Class("control-group group-right"), + Input( + Type("submit"), + Value("Submit"), + ), + ), + ), + ) +} diff --git a/pkg/server/serve.go b/pkg/server/serve.go index c938870..84d0a21 100644 --- a/pkg/server/serve.go +++ b/pkg/server/serve.go @@ -1,10 +1,16 @@ package server import ( + "crypto/subtle" + "fmt" "net/http" + "net/url" + "strings" + "github.com/LMBishop/scrapbook/pkg/config" "github.com/LMBishop/scrapbook/pkg/html" "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/pkg/site" ) func ServeSite(siteIndex *index.SiteIndex) func(w http.ResponseWriter, r *http.Request) { @@ -16,6 +22,79 @@ func ServeSite(siteIndex *index.SiteIndex) func(w http.ResponseWriter, r *http.R return } + if site.SiteConfig.Flags&config.FlagDisable != 0 { + w.WriteHeader(http.StatusForbidden) + html.ForbiddenDisabledPage(site.SiteConfig.Host).Render(w) + return + } + + if site.SiteConfig.Flags&config.FlagPassword != 0 { + jwt, err := r.Cookie("session") + if err != nil { + goto deny + } + + err = site.Authenticator.VerifyJwt(jwt.Value) + if err != nil { + goto deny + } + + goto permit + + deny: + if strings.HasPrefix(r.URL.Path, "/authenticate") { + goto ask + } + http.Redirect(w, r, "/authenticate?redirect="+url.QueryEscape(r.URL.Path), 302) + return + + ask: + handleAsk(w, r, site) + return + + permit: + } + site.Handler.ServeHTTP(w, r) } } + +func handleAsk(w http.ResponseWriter, r *http.Request, site *site.Site) { + redirect := r.URL.Query().Get("redirect") + + switch r.Method { + case "GET": + html.AuthenticateSitePage("", redirect, site.Name).Render(w) + case "POST": + err := r.ParseForm() + if err != nil { + html.AuthenticateSitePage(err.Error(), redirect, site.Name).Render(w) + return + } + + password := r.Form.Get("password") + + if len(site.SiteConfig.Password) == 0 || subtle.ConstantTimeCompare([]byte(password), []byte(site.SiteConfig.Password)) != 1 { + html.AuthenticateSitePage("The password is incorrect", redirect, site.Name).Render(w) + return + } + + jwt, err := site.Authenticator.NewJwt() + if err != nil { + html.AuthenticateSitePage(fmt.Errorf("Failed to create jwt: %w", err).Error(), redirect, site.Name).Render(w) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: jwt, + + Secure: true, + SameSite: http.SameSiteStrictMode, + HttpOnly: true, + }) + http.Redirect(w, r, redirect, 302) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} diff --git a/pkg/site/fs.go b/pkg/site/fs.go index ec57318..4542344 100644 --- a/pkg/site/fs.go +++ b/pkg/site/fs.go @@ -21,12 +21,6 @@ func NewSiteFileServer(root http.FileSystem, siteConfig *config.SiteConfig) *Sit } func (fs *SiteFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if fs.siteConfig.Flags&config.FlagDisable != 0 { - w.WriteHeader(http.StatusForbidden) - html.ForbiddenDisabledPage(fs.siteConfig.Host).Render(w) - return - } - path := filepath.Clean(r.URL.Path) var info os.FileInfo diff --git a/pkg/site/site.go b/pkg/site/site.go index 8b7af41..e15480f 100644 --- a/pkg/site/site.go +++ b/pkg/site/site.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/LMBishop/scrapbook/pkg/auth" "github.com/LMBishop/scrapbook/pkg/config" ) @@ -19,10 +20,11 @@ const versionRegex = "[0-9]{4}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}" const timeFormat = "2006_01_02_15_04_05" type Site struct { - Name string - Path string - Handler http.Handler - SiteConfig *config.SiteConfig + Name string + Path string + Handler http.Handler + Authenticator *auth.Authenticator + SiteConfig *config.SiteConfig } func NewSite(name string, dir string, config *config.SiteConfig) *Site { @@ -30,6 +32,7 @@ func NewSite(name string, dir string, config *config.SiteConfig) *Site { site.Name = name site.Path = dir site.SiteConfig = config + site.Authenticator = auth.NewAuthenticator() site.Handler = NewSiteFileServer(http.Dir(path.Join(dir, "default")), config) return &site } |
