Remove a bunch of unused cruft

This commit is contained in:
James Mills
2022-10-05 11:11:40 +10:00
parent 06c8ce3410
commit a07f747eb1
25 changed files with 31 additions and 1542 deletions

View File

@@ -180,8 +180,6 @@ func main() {
internal.WithBaseURL(baseURL),
// Administration
internal.WithAdminUser(adminUser),
internal.WithAdminPass(adminPass),
internal.WithAdminName(adminName),
internal.WithAdminEmail(adminEmail),
@@ -189,9 +187,7 @@ func main() {
internal.WithResultsPerPage(resultsPerPage),
// Secrets
internal.WithAPISigningKey(apiSigningKey),
internal.WithCookieSecret(cookieSecret),
internal.WithMagicLinkSecret(magiclinkSecret),
// Email Setitngs
internal.WithSMTPHost(smtpHost),
@@ -202,8 +198,6 @@ func main() {
// Timeouts
internal.WithSessionExpiry(sessionExpiry),
internal.WithSessionCacheTTL(sessionCacheTTL),
internal.WithAPISessionTime(apiSessionTime),
)
if err != nil {
log.WithError(err).Fatal("error creating server")

4
go.mod
View File

@@ -17,14 +17,11 @@ require (
github.com/apex/log v1.9.0
github.com/blevesearch/bleve/v2 v2.0.1
github.com/creasty/defaults v1.5.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/elithrar/simple-scrypt v1.3.0
github.com/gabstv/merger v1.0.1
github.com/glycerine/go-unsnap-stream v0.0.0-20210130063903-47dfef350d96 // indirect
github.com/go-mail/mail v2.3.1+incompatible
github.com/go-shiori/go-readability v0.0.0-20201011032228-bdc871772408
github.com/goccy/go-yaml v1.8.8
github.com/gocolly/colly/v2 v2.1.0
github.com/golang/snappy v0.0.2 // indirect
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e
@@ -57,6 +54,7 @@ require (
golang.org/x/exp v0.0.0-20210201131500-d352d2db2ceb // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
golang.org/x/text v0.3.5
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
)

22
go.sum
View File

@@ -181,7 +181,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -191,8 +190,6 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elithrar/simple-scrypt v1.3.0 h1:KIlOlxdoQf9JWKl5lMAJ28SY2URB0XTRDn2TckyzAZg=
github.com/elithrar/simple-scrypt v1.3.0/go.mod h1:U2XQRI95XHY0St410VE3UjT7vuKb1qPwrl/EJwEqnZo=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -203,8 +200,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -228,14 +223,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-shiori/dom v0.0.0-20201011032054-d6b74a54fe52 h1:wEe9mu6BOmGYT5yQ9ag5E38LHUMUv7/AFx0J8YNR8HI=
github.com/go-shiori/dom v0.0.0-20201011032054-d6b74a54fe52/go.mod h1:aLEd5DGjh1qYKnJJ/tC5OL0f3CV4CMcreDOn4RpCmUc=
github.com/go-shiori/go-readability v0.0.0-20201011032228-bdc871772408 h1:xq7Sck0bwvgp/WWw6tHFDn3dUTCQwWRWLudr+inH/gs=
@@ -245,8 +232,6 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.8.8 h1:MGfRB1GeSn/hWXYWS2Pt67iC2GJNnebdIro01ddyucA=
github.com/goccy/go-yaml v1.8.8/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
@@ -430,8 +415,6 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
@@ -441,14 +424,10 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
@@ -876,7 +855,6 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -1,158 +0,0 @@
package internal
import (
"context"
"errors"
"fmt"
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"git.mills.io/prologic/spyda/internal/passwords"
)
// ContextKey ...
type ContextKey int
const (
TokenContextKey ContextKey = iota
UserContextKey
)
var (
// ErrInvalidCredentials is returned for invalid credentials against /auth
ErrInvalidCredentials = errors.New("error: invalid credentials")
// ErrInvalidToken is returned for expired or invalid tokens used in Authorizeation headers
ErrInvalidToken = errors.New("error: invalid token")
)
// API ...
type API struct {
router *Router
config *Config
db Store
pm passwords.Passwords
}
// NewAPI ...
func NewAPI(router *Router, config *Config, db Store, pm passwords.Passwords) *API {
api := &API{router, config, db, pm}
api.initRoutes()
return api
}
func (a *API) initRoutes() {
router := a.router.Group("/api/v1")
router.GET("/ping", a.PingEndpoint())
}
// CreateToken ...
func (a *API) CreateToken(user *User, r *http.Request) (*Token, error) {
claims := jwt.MapClaims{}
claims["username"] = user.Username
createdAt := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(a.config.APISigningKey))
if err != nil {
log.WithError(err).Error("error creating signed token")
return nil, err
}
signedToken, err := jwt.Parse(tokenString, a.jwtKeyFunc)
if err != nil {
log.WithError(err).Error("error creating signed token")
return nil, err
}
tkn := &Token{
Signature: signedToken.Signature,
Value: tokenString,
UserAgent: r.UserAgent(),
CreatedAt: createdAt,
}
return tkn, nil
}
func (a *API) jwtKeyFunc(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("There was an error")
}
return []byte(a.config.APISigningKey), nil
}
func (a *API) getLoggedInUser(r *http.Request) *User {
token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc)
if err != nil {
return nil
}
if !token.Valid {
return nil
}
claims := token.Claims.(jwt.MapClaims)
username := claims["username"].(string)
user, err := a.db.GetUser(username)
if err != nil {
log.WithError(err).Error("error loading user object")
return nil
}
return user
}
func (a *API) isAuthorized(endpoint httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if r.Header.Get("Token") == "" {
http.Error(w, "No Token Provided", http.StatusUnauthorized)
return
}
token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc)
if err != nil {
log.WithError(err).Error("error parsing token")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if token.Valid {
claims := token.Claims.(jwt.MapClaims)
username := claims["username"].(string)
user, err := a.db.GetUser(username)
if err != nil {
log.WithError(err).Error("error loading user object")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
ctx := context.WithValue(r.Context(), TokenContextKey, token)
ctx = context.WithValue(ctx, UserContextKey, user)
endpoint(w, r.WithContext(ctx), p)
} else {
http.Error(w, "Invalid Token", http.StatusUnauthorized)
return
}
}
}
// PingEndpoint ...
func (a *API) PingEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{}`))
}
}

View File

@@ -1,70 +0,0 @@
package auth
import (
"net/http"
"github.com/julienschmidt/httprouter"
"git.mills.io/prologic/spyda/internal/session"
)
// Options ...
type Options struct {
login string
register string
}
// NewOptions ...
func NewOptions(login, register string) *Options {
return &Options{login, register}
}
// Manager ...
type Manager struct {
options *Options
}
// NewManager ...
func NewManager(options *Options) *Manager {
return &Manager{options}
}
// MustAuth ...
func (m *Manager) MustAuth(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if sess := r.Context().Value(session.SessionKey); sess != nil {
if _, ok := sess.(*session.Session).Get("username"); ok {
next(w, r, p)
return
}
}
http.Redirect(w, r, m.options.login, http.StatusFound)
}
}
// ShouldAuth ...
func (m *Manager) ShouldAuth(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if sess := r.Context().Value(session.SessionKey); sess != nil {
if _, ok := sess.(*session.Session).Get("username"); ok {
next(w, r, p)
return
}
}
http.Redirect(w, r, m.options.login, http.StatusFound)
}
}
func (m *Manager) HasAuth(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if sess := r.Context().Value(session.SessionKey); sess != nil {
if _, ok := sess.(*session.Session).Get("username"); ok {
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
next(w, r, p)
}
}

View File

@@ -1,99 +0,0 @@
package internal
import (
"net/http"
"time"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"git.mills.io/prologic/spyda/internal/session"
)
// LoginHandler ...
func (s *Server) LoginHandler() httprouter.Handle {
// #239: Throttle failed login attempts and lock user account.
failures := NewTTLCache(5 * time.Minute)
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
s.render("login", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
password := r.FormValue("password")
rememberme := r.FormValue("rememberme") == "on"
// Error: no username or password provided
if username == "" || password == "" {
log.Warn("no username or password provided")
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Lookup user
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Invalid username! Hint: Register an account?"
s.render("error", w, ctx)
return
}
// #239: Throttle failed login attempts and lock user account.
if failures.Get(user.Username) > MaxFailedLogins {
ctx.Error = true
ctx.Message = "Too many failed login attempts. Account temporarily locked! Please try again later."
s.render("error", w, ctx)
return
}
// Validate cleartext password against KDF hash
err = s.pm.CheckPassword(user.Password, password)
if err != nil {
// #239: Throttle failed login attempts and lock user account.
failed := failures.Inc(user.Username)
time.Sleep(time.Duration(IntPow(2, failed)) * time.Second)
ctx.Error = true
ctx.Message = "Invalid password! Hint: Reset your password?"
s.render("error", w, ctx)
return
}
// #239: Throttle failed login attempts and lock user account.
failures.Reset(user.Username)
// Login successful
log.Infof("login successful: %s", username)
// Lookup session
sess := r.Context().Value(session.SessionKey)
if sess == nil {
log.Warn("no session found")
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Authorize session
_ = sess.(*session.Session).Set("username", username)
// Persist session?
if rememberme {
_ = sess.(*session.Session).Set("persist", "1")
}
http.Redirect(w, r, RedirectRefererURL(r, s.config, "/"), http.StatusFound)
}
}
// LogoutHandler ...
func (s *Server) LogoutHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
s.sm.Delete(w, r)
http.Redirect(w, r, "/", http.StatusFound)
}
}

View File

@@ -2,20 +2,13 @@ package internal
import (
"fmt"
"strings"
"git.mills.io/prologic/bitcask"
log "github.com/sirupsen/logrus"
"git.mills.io/prologic/spyda/internal/session"
)
const (
feedsKeyPrefix = "/feeds"
sessionsKeyPrefix = "/sessions"
usersKeyPrefix = "/users"
urlsKeyPrefix = "/urls"
tokensKeyPrefix = "/tokens"
)
// BitcaskStore ...
@@ -68,89 +61,6 @@ func (bs *BitcaskStore) Merge() error {
return nil
}
func (bs *BitcaskStore) HasUser(username string) bool {
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
return bs.db.Has(key)
}
func (bs *BitcaskStore) DelUser(username string) error {
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
return bs.db.Delete(key)
}
func (bs *BitcaskStore) GetUser(username string) (*User, error) {
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
data, err := bs.db.Get(key)
if err == bitcask.ErrKeyNotFound {
return nil, ErrUserNotFound
}
return LoadUser(data)
}
func (bs *BitcaskStore) SetUser(username string, user *User) error {
data, err := user.Bytes()
if err != nil {
return err
}
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
if err := bs.db.Put(key, data); err != nil {
return err
}
return nil
}
func (bs *BitcaskStore) LenUsers() int64 {
var count int64
if err := bs.db.Scan([]byte(usersKeyPrefix), func(_ []byte) error {
count++
return nil
}); err != nil {
log.WithError(err).Error("error scanning")
}
return count
}
func (bs *BitcaskStore) SearchUsers(prefix string) []string {
var keys []string
if err := bs.db.Scan([]byte(usersKeyPrefix), func(key []byte) error {
if strings.HasPrefix(strings.ToLower(string(key)), prefix) {
keys = append(keys, strings.TrimPrefix(string(key), "/users/"))
}
return nil
}); err != nil {
log.WithError(err).Error("error scanning")
}
return keys
}
func (bs *BitcaskStore) GetAllUsers() ([]*User, error) {
var users []*User
err := bs.db.Scan([]byte(usersKeyPrefix), func(key []byte) error {
data, err := bs.db.Get(key)
if err != nil {
return err
}
user, err := LoadUser(data)
if err != nil {
return err
}
users = append(users, user)
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
func (bs *BitcaskStore) HasURL(hash string) bool {
key := []byte(fmt.Sprintf("%s/%s", urlsKeyPrefix, hash))
return bs.db.Has(key)
@@ -216,145 +126,3 @@ func (bs *BitcaskStore) ForEachURL(f func(url *URL) error) error {
return nil
}
func (bs *BitcaskStore) GetSession(sid string) (*session.Session, error) {
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
data, err := bs.db.Get(key)
if err != nil {
if err == bitcask.ErrKeyNotFound {
return nil, session.ErrSessionNotFound
}
return nil, err
}
sess := session.NewSession(bs)
if err := session.LoadSession(data, sess); err != nil {
return nil, err
}
return sess, nil
}
func (bs *BitcaskStore) SetSession(sid string, sess *session.Session) error {
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
data, err := sess.Bytes()
if err != nil {
return err
}
return bs.db.Put(key, data)
}
func (bs *BitcaskStore) HasSession(sid string) bool {
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
return bs.db.Has(key)
}
func (bs *BitcaskStore) DelSession(sid string) error {
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
return bs.db.Delete(key)
}
func (bs *BitcaskStore) SyncSession(sess *session.Session) error {
// Only persist sessions with a logged in user associated with an account
// This saves resources as we don't need to keep session keys around for
// sessions we may never load from the store again.
if sess.Has("username") {
return bs.SetSession(sess.ID, sess)
}
return nil
}
func (bs *BitcaskStore) LenSessions() int64 {
var count int64
if err := bs.db.Scan([]byte(sessionsKeyPrefix), func(_ []byte) error {
count++
return nil
}); err != nil {
log.WithError(err).Error("error scanning")
}
return count
}
func (bs *BitcaskStore) GetAllSessions() ([]*session.Session, error) {
var sessions []*session.Session
err := bs.db.Scan([]byte(sessionsKeyPrefix), func(key []byte) error {
data, err := bs.db.Get(key)
if err != nil {
return err
}
sess := session.NewSession(bs)
if err := session.LoadSession(data, sess); err != nil {
return err
}
sessions = append(sessions, sess)
return nil
})
if err != nil {
return nil, err
}
return sessions, nil
}
func (bs *BitcaskStore) GetUserTokens(user *User) ([]*Token, error) {
tokens := []*Token{}
for _, signature := range user.Tokens {
tkn, err := bs.GetToken(signature)
if err != nil {
return tokens, err
}
tokens = append(tokens, tkn)
}
return tokens, nil
}
func (bs *BitcaskStore) GetToken(signature string) (*Token, error) {
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
data, err := bs.db.Get(key)
if err == bitcask.ErrKeyNotFound {
return nil, ErrTokenNotFound
}
tkn, err := LoadToken(data)
if err != nil {
return nil, err
}
return tkn, nil
}
func (bs *BitcaskStore) SetToken(signature string, tkn *Token) error {
data, err := tkn.Bytes()
if err != nil {
return err
}
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
if err := bs.db.Put(key, data); err != nil {
return err
}
return nil
}
func (bs *BitcaskStore) DelToken(signature string) error {
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
return bs.db.Delete(key)
}
func (bs *BitcaskStore) LenTokens() int64 {
var count int64
if err := bs.db.Scan([]byte(tokensKeyPrefix), func(_ []byte) error {
count++
return nil
}); err != nil {
log.WithError(err).Error("error scanning")
}
return count
}

View File

@@ -3,15 +3,12 @@ package internal
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/url"
"os"
"strings"
"time"
"github.com/gabstv/merger"
"github.com/goccy/go-yaml"
log "github.com/sirupsen/logrus"
)
@@ -35,21 +32,15 @@ type Config struct {
Store string
Theme string
BaseURL string
AdminUser string
AdminPass string
AdminName string
AdminEmail string
SearchPrompts []string
ResultsPerPage int
APISessionTime time.Duration
SessionExpiry time.Duration
SessionCacheTTL time.Duration
APISigningKey string
CookieSecret string
MagicLinkSecret string
SMTPHost string
SMTPPort int
@@ -94,55 +85,8 @@ func (c *Config) Validate() error {
}
if c.CookieSecret == InvalidConfigValue {
return fmt.Errorf("error: COOKIE_SECRET is not configured!")
}
if c.MagicLinkSecret == InvalidConfigValue {
return fmt.Errorf("error: MAGICLINK_SECRET is not configured!")
}
if c.APISigningKey == InvalidConfigValue {
return fmt.Errorf("error: API_SIGNING_KEY is not configured!")
return fmt.Errorf("error: COOKIE_SECRET is not configured")
}
return nil
}
// LoadSettings loads pod settings from the given path
func LoadSettings(path string) (*Settings, error) {
var settings Settings
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &settings); err != nil {
return nil, err
}
return &settings, nil
}
// Save saves the pod settings to the given path
func (s *Settings) Save(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
data, err := yaml.MarshalWithOptions(s, yaml.Indent(4))
if err != nil {
return err
}
if _, err = f.Write(data); err != nil {
return err
}
if err = f.Sync(); err != nil {
return err
}
return f.Close()
}

View File

@@ -5,13 +5,11 @@ import (
"net/http"
"time"
log "github.com/sirupsen/logrus"
"github.com/vcraescu/go-paginator"
"github.com/justinas/nosurf"
"git.mills.io/prologic/spyda"
"git.mills.io/prologic/spyda/internal/session"
)
type Meta struct {
@@ -92,34 +90,5 @@ func NewContext(conf *Config, db Store, req *http.Request) *Context {
ctx.CSRFToken = nosurf.Token(req)
if sess := req.Context().Value(session.SessionKey); sess != nil {
if username, ok := sess.(*session.Session).Get("username"); ok {
ctx.Authenticated = true
ctx.Username = username
}
}
if ctx.Authenticated && ctx.Username != "" {
user, err := db.GetUser(ctx.Username)
if err != nil {
log.WithError(err).Warnf("error loading user object for %s", ctx.Username)
}
ctx.User = user
tokens, err := db.GetUserTokens(user)
if err != nil {
log.WithError(err).Warnf("error loading tokens for %s", ctx.Username)
}
ctx.Tokens = tokens
} else {
ctx.User = &User{}
}
if ctx.Username == conf.AdminUser {
ctx.IsAdmin = true
}
return ctx
}

View File

@@ -29,13 +29,7 @@ func NewCrawler(conf *Config, tasks *Dispatcher, db Store, indexer Indexer) (Cra
}
func (c *crawler) loop() {
for {
select {
case url, ok := <-c.queue:
if !ok {
log.Debugf("crawler shutting down...")
return
}
for url := range c.queue {
log.Debugf("crawling %s", url)
uuid, err := c.tasks.Dispatch(NewCrawlTask(c.conf, c.db, c.indexer, url))
if err != nil {
@@ -45,7 +39,6 @@ func (c *crawler) loop() {
log.WithField("uuid", uuid).Infof("successfully created crawl task for %s: %s", url, taskURL)
}
}
}
}
func (c *crawler) Crawl(url string) error {

View File

@@ -14,24 +14,9 @@ import (
var (
ErrSendingEmail = errors.New("error: unable to send email")
passwordResetEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .Username }},
supportRequestEmailTemplate = template.Must(template.New("email").Parse(`Hello,
You have requested to have your password on {{ .Pod }} reset for your account.
**IMPORTANT:** If this was __NOT__ initiated by you, please ignore this email and contract support!
To reset your password, please visit the following link:
{{ .BaseURL}}/newPassword?token={{ .Token }}
Kind regards,
{{ .Pod}} Support
`))
supportRequestEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
{{ .Name }} <{{ .Email }} from {{ .Pod }} has sent the following support request:
{{ .Name }} <{{ .Email }} from {{ .Instance }} has sent the following support request:
> Subject: {{ .Subject }}
>
@@ -39,21 +24,12 @@ Kind regards,
Kind regards,
{{ .Pod}} Support
{{ .Instance }} Support
`))
)
type PasswordResetEmailContext struct {
Pod string
BaseURL string
Token string
Username string
}
type SupportRequestEmailContext struct {
Pod string
AdminUser string
Instance string
Name string
Email string
@@ -96,34 +72,6 @@ func SendEmail(conf *Config, recipients []string, replyTo, subject string, body
return nil
}
func SendPasswordResetEmail(conf *Config, user *User, email, token string) error {
recipients := []string{email}
subject := fmt.Sprintf(
"[%s]: Password Reset Request for %s",
conf.Name, user.Username,
)
ctx := PasswordResetEmailContext{
Pod: conf.Name,
BaseURL: conf.BaseURL,
Token: token,
Username: user.Username,
}
buf := &bytes.Buffer{}
if err := passwordResetEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, conf.SMTPFrom, subject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending new token to %s", recipients[0])
return err
}
return nil
}
func SendSupportRequestEmail(conf *Config, name, email, subject, message string) error {
recipients := []string{conf.AdminEmail, email}
emailSubject := fmt.Sprintf(
@@ -131,8 +79,7 @@ func SendSupportRequestEmail(conf *Config, name, email, subject, message string)
conf.Name, subject,
)
ctx := SupportRequestEmailContext{
Pod: conf.Name,
AdminUser: conf.AdminUser,
Instance: conf.Name,
Name: name,
Email: email,

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
@@ -99,7 +99,7 @@ func (s *Server) CacheHandler() httprouter.Handle {
var entry Entry
data, err := ioutil.ReadFile(fn)
data, err := os.ReadFile(fn)
if err != nil {
log.WithError(err).Error("error reading cached entry")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)

View File

@@ -1,190 +0,0 @@
package internal
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/julienschmidt/httprouter"
"github.com/renstrom/shortuuid"
log "github.com/sirupsen/logrus"
)
// ManageHandler ...
func (s *Server) ManageHandler() httprouter.Handle {
isAdminUser := IsAdminUserFactory(s.config)
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if !isAdminUser(ctx.User) {
ctx.Error = true
ctx.Message = "You are not a Pod Owner!"
s.render("403", w, ctx)
return
}
if r.Method == "GET" {
s.render("managePod", w, ctx)
return
}
name := strings.TrimSpace(r.FormValue("podName"))
description := strings.TrimSpace(r.FormValue("podDescription"))
// Update name
if name != "" {
s.config.Name = name
} else {
ctx.Error = true
ctx.Message = ""
s.render("error", w, ctx)
return
}
// Update pod description
if description != "" {
s.config.Description = description
} else {
ctx.Error = true
ctx.Message = ""
s.render("error", w, ctx)
return
}
// Save config file
if err := s.config.Settings().Save(filepath.Join(s.config.Data, "settings.yaml")); err != nil {
log.WithError(err).Error("error saving config")
os.Exit(1)
}
ctx.Error = false
ctx.Message = "Pod updated successfully"
s.render("error", w, ctx)
}
}
// ManageUsersHandler ...
func (s *Server) ManageUsersHandler() httprouter.Handle {
isAdminUser := IsAdminUserFactory(s.config)
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if !isAdminUser(ctx.User) {
ctx.Error = true
ctx.Message = "You are not a Pod Owner!"
s.render("403", w, ctx)
return
}
s.render("manageUsers", w, ctx)
}
}
// AddUserHandler ...
func (s *Server) AddUserHandler() httprouter.Handle {
isAdminUser := IsAdminUserFactory(s.config)
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if !isAdminUser(ctx.User) {
ctx.Error = true
ctx.Message = "You are not a Pod Owner!"
s.render("403", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
// XXX: We DO NOT store this! (EVER)
email := strings.TrimSpace(r.FormValue("email"))
// Random password -- User is expected to user "Password Reset"
password := shortuuid.New()
if err := ValidateUsername(username); err != nil {
ctx.Error = true
ctx.Message = fmt.Sprintf("Username validation failed: %s", err.Error())
s.render("error", w, ctx)
return
}
if s.db.HasUser(username) {
ctx.Error = true
ctx.Message = "User or Feed with that name already exists! Please pick another!"
s.render("error", w, ctx)
return
}
hash, err := s.pm.CreatePassword(password)
if err != nil {
log.WithError(err).Error("error creating password hash")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
recoveryHash := fmt.Sprintf("email:%s", FastHash(email))
user := NewUser()
user.Username = username
user.Recovery = recoveryHash
user.Password = hash
user.CreatedAt = time.Now()
if err := s.db.SetUser(username, user); err != nil {
log.WithError(err).Error("error saving user object for new user")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx.Error = false
ctx.Message = "User successfully created"
s.render("error", w, ctx)
}
}
// DelUserHandler ...
func (s *Server) DelUserHandler() httprouter.Handle {
isAdminUser := IsAdminUserFactory(s.config)
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if !isAdminUser(ctx.User) {
ctx.Error = true
ctx.Message = "You are not a Pod Owner!"
s.render("403", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
user, err := s.db.GetUser(username)
if err != nil {
log.WithError(err).Errorf("error loading user object for %s", username)
ctx.Error = true
ctx.Message = "Error deleting account"
s.render("error", w, ctx)
return
}
// Delete user
if err := s.db.DelUser(user.Username); err != nil {
ctx.Error = true
ctx.Message = "An error occured whilst deleting your account"
s.render("error", w, ctx)
return
}
s.sm.Delete(w, r)
ctx.Error = false
ctx.Message = "Successfully deleted account"
s.render("error", w, ctx)
}
}

View File

@@ -91,11 +91,8 @@ func NewConfig() *Config {
Store: DefaultStore,
Theme: DefaultTheme,
BaseURL: DefaultBaseURL,
AdminUser: DefaultAdminUser,
AdminPass: DefaultAdminPass,
CookieSecret: DefaultCookieSecret,
MagicLinkSecret: DefaultMagicLinkSecret,
SearchPrompts: DefaultSearchPrompts,
ResultsPerPage: DefaultResultsPerPage,
@@ -149,22 +146,6 @@ func WithBaseURL(baseURL string) Option {
}
}
// WithAdminUser sets the Admin username
func WithAdminUser(adminUser string) Option {
return func(cfg *Config) error {
cfg.AdminUser = adminUser
return nil
}
}
// WithAdminPass sets the Admin password
func WithAdminPass(adminPass string) Option {
return func(cfg *Config) error {
cfg.AdminPass = adminPass
return nil
}
}
// WithAdminName sets the Admin name used to identify the pod operator
func WithAdminName(adminName string) Option {
return func(cfg *Config) error {
@@ -221,14 +202,6 @@ func WithResultsPerPage(resultsPerPage int) Option {
}
}
// WithSessionCacheTTL sets the server's session cache ttl
func WithSessionCacheTTL(cacheTTL time.Duration) Option {
return func(cfg *Config) error {
cfg.SessionCacheTTL = cacheTTL
return nil
}
}
// WithSessionExpiry sets the server's session expiry time
func WithSessionExpiry(expiry time.Duration) Option {
return func(cfg *Config) error {
@@ -237,14 +210,6 @@ func WithSessionExpiry(expiry time.Duration) Option {
}
}
// WithMagicLinkSecret sets the MagicLinkSecert used to create password reset tokens
func WithMagicLinkSecret(secret string) Option {
return func(cfg *Config) error {
cfg.MagicLinkSecret = secret
return nil
}
}
// WithSMTPHost sets the SMTPHost to use for sending email
func WithSMTPHost(host string) Option {
return func(cfg *Config) error {
@@ -284,19 +249,3 @@ func WithSMTPFrom(from string) Option {
return nil
}
}
// WithAPISessionTime sets the API session time for tokens
func WithAPISessionTime(duration time.Duration) Option {
return func(cfg *Config) error {
cfg.APISessionTime = duration
return nil
}
}
// WithAPISigningKey sets the API JWT signing key for tokens
func WithAPISigningKey(key string) Option {
return func(cfg *Config) error {
cfg.APISigningKey = key
return nil
}
}

View File

@@ -10,7 +10,6 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gomarkdown/markdown"
@@ -20,6 +19,8 @@ import (
"github.com/julienschmidt/httprouter"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const pagesDir = "pages"
@@ -104,6 +105,8 @@ func (s *Server) PageHandler(name string) httprouter.Handle {
return page, nil
}
caser := cases.Title(language.English)
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
@@ -157,7 +160,7 @@ func (s *Server) PageHandler(name string) httprouter.Handle {
if frontmatter.Title != "" {
title = frontmatter.Title
} else {
title = strings.Title(name)
title = caser.String(name)
}
ctx.Title = title
ctx.Meta.Description = frontmatter.Description

View File

@@ -1,194 +0,0 @@
package internal
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
)
// ResetPasswordHandler ...
func (s *Server) ResetPasswordHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
ctx.Title = "Reset password"
s.render("resetPassword", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
email := strings.TrimSpace(r.FormValue("email"))
recovery := fmt.Sprintf("email:%s", FastHash(email))
// Check if user exist
if !s.db.HasUser(username) {
ctx.Error = true
ctx.Message = "User not found!"
s.render("error", w, ctx)
return
}
// Get user object from DB
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
if recovery != user.Recovery {
ctx.Error = true
ctx.Message = "Error! The email address you supplied does not match what you registered with :/"
s.render("error", w, ctx)
return
}
// Create magic link expiry time
now := time.Now()
secs := now.Unix()
expiresAfterSeconds := int64(600) // Link expires after 10 minutes
expiryTime := secs + expiresAfterSeconds
// Create magic link
token := jwt.NewWithClaims(
jwt.SigningMethodHS256,
jwt.MapClaims{"username": username, "expiresAt": expiryTime},
)
tokenString, err := token.SignedString([]byte(s.config.MagicLinkSecret))
if err != nil {
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
if err := SendPasswordResetEmail(s.config, user, email, tokenString); err != nil {
log.WithError(err).Errorf("unable to send reset password email to %s", user.Username)
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
log.Infof("reset password email sent for %s", user.Username)
// Show success msg
ctx.Error = false
ctx.Message = "Password request request sent! Please check your email and follow the instructions"
s.render("error", w, ctx)
}
}
// ResetPasswordMagicLinkHandler ...
func (s *Server) ResetPasswordMagicLinkHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
// Get token from query string
tokens, ok := r.URL.Query()["token"]
// Check if valid token
if !ok || len(tokens[0]) < 1 {
ctx.Error = true
ctx.Message = "Invalid token"
s.render("error", w, ctx)
return
}
tokenEmail := tokens[0]
ctx.PasswordResetToken = tokenEmail
// Show newPassword page
s.render("newPassword", w, ctx)
}
}
// NewPasswordHandler ...
func (s *Server) NewPasswordHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
return
}
password := r.FormValue("password")
tokenEmail := r.FormValue("token")
// Check if token is valid
token, err := jwt.Parse(tokenEmail, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.config.MagicLinkSecret), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var username = fmt.Sprintf("%v", claims["username"])
var expiresAt int = int(claims["expiresAt"].(float64))
now := time.Now()
secs := now.Unix()
// Check token expiry
if secs > int64(expiresAt) {
ctx.Error = true
ctx.Message = "Token expires"
s.render("error", w, ctx)
return
}
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
// Reset password
if password != "" {
hash, err := s.pm.CreatePassword(password)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
user.Password = hash
// Save user
if err := s.db.SetUser(username, user); err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
}
log.Infof("password changed: %v", user)
// Show success msg
ctx.Error = false
ctx.Message = "Password reset successfully."
s.render("error", w, ctx)
} else {
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
}
}

View File

@@ -1,9 +0,0 @@
package passwords
// Passwords is an interface for creating and verifying secure passwords
// An implementation must implement all methods and it is up to the impl
// which underlying crypto to use for hasing cleartext passwrods.
type Passwords interface {
CreatePassword(password string) (string, error)
CheckPassword(hash, password string) error
}

View File

@@ -1,72 +0,0 @@
package passwords
import (
"time"
scrypt "github.com/elithrar/simple-scrypt"
log "github.com/sirupsen/logrus"
)
const (
// DefaultMaxTimeout default max timeout in ms
DefaultMaxTimeout = 500 * time.Millisecond
// DefaultMaxMemory default max memory in MB
DefaultMaxMemory = 64
)
// Options ...
type Options struct {
maxTimeout time.Duration
maxMemory int
}
// NewOptions ...
func NewOptions(maxTimeout time.Duration, maxMemory int) *Options {
return &Options{maxTimeout, maxMemory}
}
// ScryptPasswords ...
type ScryptPasswords struct {
options *Options
params scrypt.Params
}
// NewScryptPasswords ...
func NewScryptPasswords(options *Options) Passwords {
if options == nil {
options = &Options{}
}
if options.maxTimeout == 0 {
options.maxTimeout = DefaultMaxTimeout
}
if options.maxMemory == 0 {
options.maxMemory = DefaultMaxMemory
}
log.Info("Calibrating scrypt parameters ...")
params, err := scrypt.Calibrate(
options.maxTimeout,
options.maxMemory,
scrypt.DefaultParams,
)
if err != nil {
log.Fatalf("error calibrating scrypt params: %s", err)
}
log.WithField("params", params).Info("scrypt params")
return &ScryptPasswords{options, params}
}
// CreatePassword ...
func (sp *ScryptPasswords) CreatePassword(password string) (string, error) {
hash, err := scrypt.GenerateFromPassword([]byte(password), sp.params)
return string(hash), err
}
// CheckPassword ...
func (sp *ScryptPasswords) CheckPassword(hash, password string) error {
return scrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}

View File

@@ -2,7 +2,6 @@ package internal
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@@ -67,7 +66,7 @@ func Scrape(conf *Config, url string) (*Entry, error) {
return nil, fmt.Errorf("error serializing entry: %s", err)
}
if err := ioutil.WriteFile(fn, data, 0644); err != nil {
if err := os.WriteFile(fn, data, 0644); err != nil {
log.WithError(err).Error("error persisting entry")
return nil, fmt.Errorf("error persisting entry: %w", err)
}

View File

@@ -6,21 +6,17 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"git.mills.io/prologic/observe"
"github.com/NYTimes/gziphandler"
"github.com/gabstv/merger"
"github.com/justinas/nosurf"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/unrolled/logger"
"git.mills.io/prologic/spyda"
"git.mills.io/prologic/spyda/internal/auth"
"git.mills.io/prologic/spyda/internal/passwords"
"git.mills.io/prologic/spyda/internal/session"
"git.mills.io/prologic/spyda/internal/static"
)
@@ -56,18 +52,9 @@ type Server struct {
// Dispatcher
tasks *Dispatcher
// Auth
am *auth.Manager
// Sessions
sc *SessionStore
sc session.Store
sm *session.Manager
// API
api *API
// Passwords
pm passwords.Passwords
}
func (s *Server) render(name string, w http.ResponseWriter, ctx *Context) {
@@ -164,37 +151,6 @@ func (s *Server) setupMetrics() {
},
)
// sessions
metrics.NewGaugeFunc(
"server", "sessions",
"Number of active in-memory sessions (non-persistent)",
func() float64 {
return float64(s.sc.Count())
},
)
// database keys
metrics.NewGaugeFunc(
"db", "sessions",
"Number of database /sessions keys",
func() float64 {
return float64(s.db.LenSessions())
},
)
metrics.NewGaugeFunc(
"db", "users",
"Number of database /users keys",
func() float64 {
return float64(s.db.LenUsers())
},
)
metrics.NewGaugeFunc(
"db", "tokens",
"Number of database /tokens keys",
func() float64 {
return float64(s.db.LenTokens())
},
)
metrics.NewGaugeFunc(
"db", "urls",
"Number of database /urls keys",
@@ -311,18 +267,6 @@ func (s *Server) initRoutes() {
s.router.GET("/cache/:hash", s.CacheHandler())
s.router.HEAD("/cache/:hash", s.CacheHandler())
s.router.GET("/login", s.am.HasAuth(s.LoginHandler()))
s.router.POST("/login", s.LoginHandler())
s.router.GET("/logout", s.LogoutHandler())
s.router.POST("/logout", s.LogoutHandler())
// Reset Password
s.router.GET("/pwreset", s.ResetPasswordHandler())
s.router.POST("/pwreset", s.ResetPasswordHandler())
s.router.GET("/chpasswd", s.ResetPasswordMagicLinkHandler())
s.router.POST("/chpasswd", s.NewPasswordHandler())
// Task State
s.router.GET("/tasks", s.TasksHandler())
s.router.GET("/task/:uuid", s.TaskHandler())
@@ -330,13 +274,6 @@ func (s *Server) initRoutes() {
s.router.GET("/add", s.AddHandler())
s.router.POST("/add", s.AddHandler())
s.router.GET("/manage", s.ManageHandler())
s.router.POST("/manage", s.ManageHandler())
s.router.GET("/manage/users", s.ManageUsersHandler())
s.router.POST("/manage/adduser", s.AddUserHandler())
s.router.POST("/manage/deluser", s.DelUserHandler())
// Support
s.router.GET("/support", s.SupportHandler())
s.router.POST("/support", s.SupportHandler())
@@ -353,16 +290,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
}
}
settings, err := LoadSettings(filepath.Join(config.Data, "settings.yaml"))
if err != nil {
log.Warnf("error loading pod settings: %s", err)
} else {
if err := merger.MergeOverwrite(config, settings); err != nil {
log.WithError(err).Error("error merging pod settings")
return nil, err
}
}
if err := config.Validate(); err != nil {
log.WithError(err).Error("error validating config")
return nil, fmt.Errorf("error validating config: %w", err)
@@ -387,14 +314,9 @@ func NewServer(bind string, options ...Option) (*Server, error) {
router := NewRouter()
am := auth.NewManager(auth.NewOptions("/login", "/register"))
tasks := NewDispatcher(2, 10) // TODO: Make this configurable?
pm := passwords.NewScryptPasswords(nil)
sc := NewSessionStore(db, config.SessionCacheTTL)
sc := session.NewMemoryStore(config.SessionExpiry)
sm := session.NewManager(
session.NewOptions(
config.Name,
@@ -417,8 +339,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
return nil, err
}
api := NewAPI(router, config, db, pm)
csrfHandler := nosurf.New(router)
csrfHandler.ExemptGlob("/api/v1/*")
@@ -440,9 +360,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
),
},
// API
api: api,
// Indexer
indexer: indexer,
@@ -458,15 +375,9 @@ func NewServer(bind string, options ...Option) (*Server, error) {
// Dispatcher
tasks: tasks,
// Auth Manager
am: am,
// Session Manager
sc: sc,
sm: sm,
// Password Manager
pm: pm,
}
if err := server.setupCronJobs(); err != nil {
@@ -488,17 +399,14 @@ func NewServer(bind string, options ...Option) (*Server, error) {
// Log interesting configuration options
log.Infof("Instance Name: %s", server.config.Name)
log.Infof("Base URL: %s", server.config.BaseURL)
log.Infof("Admin User: %s", server.config.AdminUser)
log.Infof("Admin Name: %s", server.config.AdminName)
log.Infof("Admin Email: %s", server.config.AdminEmail)
log.Infof("SMTP Host: %s", server.config.SMTPHost)
log.Infof("SMTP Port: %d", server.config.SMTPPort)
log.Infof("SMTP User: %s", server.config.SMTPUser)
log.Infof("SMTP From: %s", server.config.SMTPFrom)
log.Infof("API Session Time: %s", server.config.APISessionTime)
server.initRoutes()
api.initRoutes()
go server.runStartupJobs()

View File

@@ -1,90 +0,0 @@
package internal
import (
"time"
"github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
"git.mills.io/prologic/spyda/internal/session"
)
// SessionStore ...
type SessionStore struct {
store Store
cached *cache.Cache
}
func NewSessionStore(store Store, sessionCacheTTL time.Duration) *SessionStore {
return &SessionStore{
store: store,
cached: cache.New(sessionCacheTTL, time.Minute*5),
}
}
func (s *SessionStore) Count() int {
return s.cached.ItemCount()
}
func (s *SessionStore) GetSession(sid string) (*session.Session, error) {
val, found := s.cached.Get(sid)
if found {
return val.(*session.Session), nil
}
return s.store.GetSession(sid)
}
func (s *SessionStore) SetSession(sid string, sess *session.Session) error {
s.cached.Set(sid, sess, cache.DefaultExpiration)
if persist, ok := sess.Get("persist"); !ok || persist != "1" {
return nil
}
return s.store.SetSession(sid, sess)
}
func (s *SessionStore) HasSession(sid string) bool {
_, ok := s.cached.Get(sid)
if ok {
return true
}
return s.store.HasSession(sid)
}
func (s *SessionStore) DelSession(sid string) error {
if s.store.HasSession(sid) {
if err := s.store.DelSession(sid); err != nil {
log.WithError(err).Errorf("error deleting persistent session %s", sid)
return err
}
}
s.cached.Delete(sid)
return nil
}
func (s *SessionStore) SyncSession(sess *session.Session) error {
if persist, ok := sess.Get("persist"); ok && persist == "1" {
if err := s.store.SetSession(sess.ID, sess); err != nil {
log.WithError(err).Errorf("error persisting session %s", sess.ID)
return err
}
}
return s.SetSession(sess.ID, sess)
}
func (s *SessionStore) GetAllSessions() ([]*session.Session, error) {
var sessions []*session.Session
for _, item := range s.cached.Items() {
sess := item.Object.(*session.Session)
sessions = append(sessions, sess)
}
persistedSessions, err := s.store.GetAllSessions()
if err != nil {
log.WithError(err).Error("error getting all persisted sessions")
return sessions, err
}
return append(sessions, persistedSessions...), nil
}

View File

@@ -3,16 +3,11 @@ package internal
import (
"errors"
"fmt"
"git.mills.io/prologic/spyda/internal/session"
)
var (
ErrInvalidStore = errors.New("error: invalid store")
ErrUserNotFound = errors.New("error: user not found")
ErrTokenNotFound = errors.New("error: token not found")
ErrURLNotFound = errors.New("error: url not found")
ErrInvalidSession = errors.New("error: invalid session")
)
type Store interface {
@@ -20,33 +15,12 @@ type Store interface {
Close() error
Sync() error
DelUser(username string) error
HasUser(username string) bool
GetUser(username string) (*User, error)
SetUser(username string, user *User) error
LenUsers() int64
SearchUsers(prefix string) []string
GetAllUsers() ([]*User, error)
DelURL(hash string) error
HasURL(hash string) bool
GetURL(hash string) (*URL, error)
SetURL(hash string, url *URL) error
URLCount() int64
ForEachURL(f func(*URL) error) error
GetSession(sid string) (*session.Session, error)
SetSession(sid string, sess *session.Session) error
HasSession(sid string) bool
DelSession(sid string) error
SyncSession(sess *session.Session) error
LenSessions() int64
GetAllSessions() ([]*session.Session, error)
GetUserTokens(user *User) ([]*Token, error)
SetToken(signature string, token *Token) error
DelToken(signature string) error
LenTokens() int64
}
func NewStore(store string) (Store, error) {

View File

@@ -42,7 +42,6 @@ func NewTemplateManager(conf *Config) (*TemplateManager, error) {
funcMap["prettyURL"] = PrettyURL
funcMap["isLocalURL"] = IsLocalURLFactory(conf)
funcMap["formatForDateTime"] = FormatForDateTime
funcMap["isAdminUser"] = IsAdminUserFactory(conf)
m := &TemplateManager{debug: conf.Debug, templates: templates, funcMap: funcMap}

View File

@@ -1,44 +0,0 @@
{{define "content"}}
<article>
<div>
<hgroup>
<h2>Sign in</h2>
<p>Login to your Spyda account on {{ .InstanceName }}</p>
</hgroup>
<form action="/login" method="POST">
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<input type="text" name="username" placeholder="Username" aria-label="Username" autocomplete="nickname" required="required" autofocus />
<input type="password" name="password" placeholder="Password" aria-label="Password" autocomplete="current-password" required="required" />
<fieldset>
<label for="rememberme">
<input type="checkbox" id="rememberme" name="rememberme">
Remember me?
</label>
</fieldset>
<input type="submit" class="contrast" value="Login" />
<p>
<a href="/resetPassword">Forgotten your password?</a>
</p>
</form>
</div>
<div>
<hgroup>
<h2>How to login to your account</h2>
</hgroup>
<p>
Login to your Spyda account on {{ .InstanceName }} by filling in
the Username and Password you used when you created your account.
</p>
<p>
Check the "Remember Me" box if you don't want to have to keep logging in
every few hours.
</p>
<p>
If you have forgotten your password you can request a
<a href="/resetPassword">Password Reset</a> as long as you remember
your username and email address you signed up with and retain access to
your email (<i>We <b>NEVER</b> store your email address!</i>).
</p>
</div>
</article>
{{end}}

View File

@@ -357,14 +357,6 @@ func PrettyURL(uri string) string {
return fmt.Sprintf("%s/%s", u.Hostname(), strings.TrimPrefix(u.EscapedPath(), "/"))
}
// IsAdminUserFactory returns a function that returns true if the user provided
// is the configured pod administrator, false otherwise.
func IsAdminUserFactory(conf *Config) func(user *User) bool {
return func(user *User) bool {
return NormalizeUsername(conf.AdminUser) == NormalizeUsername(user.Username)
}
}
func URLForPage(baseURL, page string) string {
return fmt.Sprintf(
"%s/%s",