Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata, HTML on disk. Features: password-protected artifacts, per-artifact expiration, admin dashboard with view metrics (privacy-preserving daily-salt visitor hash, sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint. Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
1.8 KiB
Go
81 lines
1.8 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const unlockLifetime = 24 * time.Hour
|
|
|
|
type Artifact struct {
|
|
secret []byte
|
|
secure bool
|
|
}
|
|
|
|
func NewArtifact(secretHex string, secure bool) *Artifact {
|
|
return &Artifact{secret: []byte(secretHex), secure: secure}
|
|
}
|
|
|
|
func HashPassword(p string) (string, error) {
|
|
b, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
|
return string(b), err
|
|
}
|
|
|
|
func VerifyPassword(hash, p string) bool {
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(p)) == nil
|
|
}
|
|
|
|
func unlockCookieName(slug string) string {
|
|
return "artifacto_unlock_" + slug
|
|
}
|
|
|
|
func (a *Artifact) Issue(slug string) string {
|
|
issued := strconv.FormatInt(time.Now().Unix(), 10)
|
|
return issued + "." + a.sign(slug+"|"+issued)
|
|
}
|
|
|
|
func (a *Artifact) SetCookie(w http.ResponseWriter, slug string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: unlockCookieName(slug),
|
|
Value: a.Issue(slug),
|
|
Path: "/p/" + slug,
|
|
HttpOnly: true,
|
|
Secure: a.secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
Expires: time.Now().Add(unlockLifetime),
|
|
})
|
|
}
|
|
|
|
func (a *Artifact) Verify(r *http.Request, slug string) bool {
|
|
c, err := r.Cookie(unlockCookieName(slug))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
parts := strings.SplitN(c.Value, ".", 2)
|
|
if len(parts) != 2 {
|
|
return false
|
|
}
|
|
issued, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if time.Since(time.Unix(issued, 0)) > unlockLifetime {
|
|
return false
|
|
}
|
|
expected := a.sign(slug + "|" + parts[0])
|
|
return hmac.Equal([]byte(expected), []byte(parts[1]))
|
|
}
|
|
|
|
func (a *Artifact) sign(s string) string {
|
|
m := hmac.New(sha256.New, a.secret)
|
|
m.Write([]byte(s))
|
|
return base64.RawURLEncoding.EncodeToString(m.Sum(nil))
|
|
}
|