From efa84fee8b076c87235b017862f73bea6e31d6d8 Mon Sep 17 00:00:00 2001 From: Ludvig Lundgren Date: Sun, 9 Jan 2022 14:41:48 +0100 Subject: [PATCH] Feature: Improve config for http server (#67) * feat: improve config for http server * Feature: Support multiple action status per release (#69) * feat: move release actions to separate table * chore: update sqlite driver * fix(indexers): btn api client (#71) What: * Api key and torrentId in wrong order * Set hardcoded ID in jsonrpc request object * ParsetorrentId from url Fixes #68 * feat: show irc network status in settings list * feat: show irc channel status * chore: go mod tidy * feat: improve config for http server * feat: add context to user repo * feat: only set secure cookie if https --- cmd/autobrr/main.go | 6 +---- cmd/autobrrctl/main.go | 7 +++--- internal/auth/service.go | 7 +++--- internal/database/user.go | 11 +++++---- internal/domain/user.go | 6 +++-- internal/http/auth.go | 46 ++++++++++++++++++++++--------------- internal/http/middleware.go | 4 ++-- internal/http/server.go | 32 +++++++++++++++----------- internal/user/service.go | 11 +++++---- 9 files changed, 74 insertions(+), 56 deletions(-) diff --git a/cmd/autobrr/main.go b/cmd/autobrr/main.go index 61ce6d3..5a1c3c6 100644 --- a/cmd/autobrr/main.go +++ b/cmd/autobrr/main.go @@ -2,7 +2,6 @@ package main import ( "database/sql" - "fmt" "os" "os/signal" "syscall" @@ -97,18 +96,15 @@ func main() { ircService = irc.NewService(ircRepo, filterService, indexerService, releaseService) userService = user.NewService(userRepo) authService = auth.NewService(userService) - //announceService = announce.NewService(filterService, indexerService, releaseService) ) // register event subscribers events.NewSubscribers(bus, releaseService) - addr := fmt.Sprintf("%v:%v", cfg.Host, cfg.Port) - errorChannel := make(chan error) go func() { - httpServer := http.NewServer(serverEvents, addr, cfg.BaseURL, version, commit, date, actionService, authService, downloadClientService, filterService, indexerService, ircService, releaseService) + httpServer := http.NewServer(cfg, serverEvents, version, commit, date, actionService, authService, downloadClientService, filterService, indexerService, ircService, releaseService) errorChannel <- httpServer.Open() }() diff --git a/cmd/autobrrctl/main.go b/cmd/autobrrctl/main.go index ebcddec..6f35e6e 100644 --- a/cmd/autobrrctl/main.go +++ b/cmd/autobrrctl/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "database/sql" "flag" "fmt" @@ -75,7 +76,7 @@ func main() { Username: username, Password: hashed, } - if err := userRepo.Store(user); err != nil { + if err := userRepo.Store(context.Background(), user); err != nil { log.Fatalf("failed to create user: %v", err) } case "change-password": @@ -85,7 +86,7 @@ func main() { os.Exit(1) } - user, err := userRepo.FindByUsername(username) + user, err := userRepo.FindByUsername(context.Background(), username) if err != nil { log.Fatalf("failed to get user: %v", err) } @@ -104,7 +105,7 @@ func main() { } user.Password = hashed - if err := userRepo.Store(*user); err != nil { + if err := userRepo.Store(context.Background(), *user); err != nil { log.Fatalf("failed to create user: %v", err) } default: diff --git a/internal/auth/service.go b/internal/auth/service.go index 27bb563..0411325 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -1,6 +1,7 @@ package auth import ( + "context" "errors" "github.com/autobrr/autobrr/internal/domain" @@ -9,7 +10,7 @@ import ( ) type Service interface { - Login(username, password string) (*domain.User, error) + Login(ctx context.Context, username, password string) (*domain.User, error) } type service struct { @@ -22,13 +23,13 @@ func NewService(userSvc user.Service) Service { } } -func (s *service) Login(username, password string) (*domain.User, error) { +func (s *service) Login(ctx context.Context, username, password string) (*domain.User, error) { if username == "" || password == "" { return nil, errors.New("bad credentials") } // find user - u, err := s.userSvc.FindByUsername(username) + u, err := s.userSvc.FindByUsername(ctx, username) if err != nil { return nil, err } diff --git a/internal/database/user.go b/internal/database/user.go index eece54d..b76bc94 100644 --- a/internal/database/user.go +++ b/internal/database/user.go @@ -1,6 +1,7 @@ package database import ( + "context" "database/sql" "github.com/rs/zerolog/log" @@ -16,10 +17,10 @@ func NewUserRepo(db *sql.DB) domain.UserRepo { return &UserRepo{db: db} } -func (r *UserRepo) FindByUsername(username string) (*domain.User, error) { +func (r *UserRepo) FindByUsername(ctx context.Context, username string) (*domain.User, error) { query := `SELECT id, username, password FROM users WHERE username = ?` - row := r.db.QueryRow(query, username) + row := r.db.QueryRowContext(ctx, query, username) if err := row.Err(); err != nil { return nil, err } @@ -34,12 +35,12 @@ func (r *UserRepo) FindByUsername(username string) (*domain.User, error) { return &user, nil } -func (r *UserRepo) Store(user domain.User) error { +func (r *UserRepo) Store(ctx context.Context, user domain.User) error { var err error if user.ID != 0 { update := `UPDATE users SET password = ? WHERE username = ?` - _, err = r.db.Exec(update, user.Password, user.Username) + _, err = r.db.ExecContext(ctx, update, user.Password, user.Username) if err != nil { log.Error().Stack().Err(err).Msg("error executing query") return err @@ -47,7 +48,7 @@ func (r *UserRepo) Store(user domain.User) error { } else { query := `INSERT INTO users (username, password) VALUES (?, ?)` - _, err = r.db.Exec(query, user.Username, user.Password) + _, err = r.db.ExecContext(ctx, query, user.Username, user.Password) if err != nil { log.Error().Stack().Err(err).Msg("error executing query") return err diff --git a/internal/domain/user.go b/internal/domain/user.go index 8f278b0..016b376 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -1,8 +1,10 @@ package domain +import "context" + type UserRepo interface { - FindByUsername(username string) (*User, error) - Store(user User) error + FindByUsername(ctx context.Context, username string) (*User, error) + Store(ctx context.Context, user User) error } type User struct { diff --git a/internal/http/auth.go b/internal/http/auth.go index ba1d4af..5b24cab 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -1,38 +1,37 @@ package http import ( + "context" "encoding/json" "net/http" "github.com/go-chi/chi" "github.com/gorilla/sessions" - "github.com/autobrr/autobrr/internal/config" "github.com/autobrr/autobrr/internal/domain" ) type authService interface { - Login(username, password string) (*domain.User, error) + Login(ctx context.Context, username, password string) (*domain.User, error) } type authHandler struct { encoder encoder + config domain.Config service authService + + cookieStore *sessions.CookieStore } -func newAuthHandler(encoder encoder, service authService) *authHandler { +func newAuthHandler(encoder encoder, config domain.Config, cookieStore *sessions.CookieStore, service authService) *authHandler { return &authHandler{ - encoder: encoder, - service: service, + encoder: encoder, + config: config, + service: service, + cookieStore: cookieStore, } } -var ( - // key will only be valid as long as it's running. - key = []byte(config.Config.SessionSecret) - store = sessions.NewCookieStore(key) -) - func (h authHandler) Routes(r chi.Router) { r.Post("/login", h.login) r.Post("/logout", h.logout) @@ -51,12 +50,23 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) { return } - store.Options.Secure = true - store.Options.HttpOnly = true - store.Options.SameSite = http.SameSiteStrictMode - session, _ := store.Get(r, "user_session") + h.cookieStore.Options.HttpOnly = true + h.cookieStore.Options.SameSite = http.SameSiteLaxMode + h.cookieStore.Options.Path = h.config.BaseURL - _, err := h.service.Login(data.Username, data.Password) + // autobrr does not support serving on TLS / https, so this is only available behind reverse proxy + // if forwarded protocol is https then set cookie secure + // SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + fwdProto := r.Header.Get("X-Forwarded-Proto") + if fwdProto == "https" { + h.cookieStore.Options.Secure = true + h.cookieStore.Options.SameSite = http.SameSiteStrictMode + } + + session, _ := h.cookieStore.Get(r, "user_session") + + _, err := h.service.Login(ctx, data.Username, data.Password) if err != nil { h.encoder.StatusResponse(ctx, w, nil, http.StatusUnauthorized) return @@ -72,7 +82,7 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) { func (h authHandler) logout(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - session, _ := store.Get(r, "user_session") + session, _ := h.cookieStore.Get(r, "user_session") // Revoke users authentication session.Values["authenticated"] = false @@ -83,7 +93,7 @@ func (h authHandler) logout(w http.ResponseWriter, r *http.Request) { func (h authHandler) test(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - session, _ := store.Get(r, "user_session") + session, _ := h.cookieStore.Get(r, "user_session") // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { diff --git a/internal/http/middleware.go b/internal/http/middleware.go index 93970a7..ae4519b 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -2,10 +2,10 @@ package http import "net/http" -func IsAuthenticated(next http.Handler) http.Handler { +func (s Server) IsAuthenticated(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // check session - session, _ := store.Get(r, "user_session") + session, _ := s.cookieStore.Get(r, "user_session") // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { diff --git a/internal/http/server.go b/internal/http/server.go index 4126bcf..8efc691 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -1,14 +1,16 @@ package http import ( + "fmt" "io/fs" "net" "net/http" - "github.com/autobrr/autobrr/internal/config" + "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/web" "github.com/go-chi/chi" + "github.com/gorilla/sessions" "github.com/r3labs/sse/v2" "github.com/rs/cors" ) @@ -16,8 +18,8 @@ import ( type Server struct { sse *sse.Server - address string - baseUrl string + config domain.Config + cookieStore *sessions.CookieStore version string commit string @@ -32,15 +34,16 @@ type Server struct { releaseService releaseService } -func NewServer(sse *sse.Server, address string, baseUrl string, version string, commit string, date string, actionService actionService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, indexerSvc indexerService, ircSvc ircService, releaseSvc releaseService) Server { +func NewServer(config domain.Config, sse *sse.Server, version string, commit string, date string, actionService actionService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, indexerSvc indexerService, ircSvc ircService, releaseSvc releaseService) Server { return Server{ + config: config, sse: sse, - address: address, - baseUrl: baseUrl, version: version, commit: commit, date: date, + cookieStore: sessions.NewCookieStore([]byte(config.SessionSecret)), + actionService: actionService, authService: authService, downloadClientService: downloadClientSvc, @@ -52,7 +55,8 @@ func NewServer(sse *sse.Server, address string, baseUrl string, version string, } func (s Server) Open() error { - listener, err := net.Listen("tcp", s.address) + addr := fmt.Sprintf("%v:%v", s.config.Host, s.config.Port) + listener, err := net.Listen("tcp", addr) if err != nil { return err } @@ -91,10 +95,10 @@ func (s Server) Handler() http.Handler { fileSystem.ServeHTTP(w, r) }) - r.Route("/api/auth", newAuthHandler(encoder, s.authService).Routes) + r.Route("/api/auth", newAuthHandler(encoder, s.config, s.cookieStore, s.authService).Routes) r.Group(func(r chi.Router) { - r.Use(IsAuthenticated) + r.Use(s.IsAuthenticated) r.Route("/api", func(r chi.Router) { r.Route("/actions", newActionHandler(encoder, s.actionService).Routes) @@ -121,17 +125,17 @@ func (s Server) Handler() http.Handler { }) //r.HandleFunc("/*", handler.ServeHTTP) - r.Get("/", index) - r.Get("/*", index) + r.Get("/", s.index) + r.Get("/*", s.index) return r } -func index(w http.ResponseWriter, r *http.Request) { +func (s Server) index(w http.ResponseWriter, r *http.Request) { p := web.IndexParams{ Title: "Dashboard", - Version: "thisistheversion", - BaseUrl: config.Config.BaseURL, + Version: s.version, + BaseUrl: s.config.BaseURL, } web.Index(w, p) } diff --git a/internal/user/service.go b/internal/user/service.go index bc37cd9..ba9ed85 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -1,9 +1,12 @@ package user -import "github.com/autobrr/autobrr/internal/domain" +import ( + "context" + "github.com/autobrr/autobrr/internal/domain" +) type Service interface { - FindByUsername(username string) (*domain.User, error) + FindByUsername(ctx context.Context, username string) (*domain.User, error) } type service struct { @@ -16,8 +19,8 @@ func NewService(repo domain.UserRepo) Service { } } -func (s *service) FindByUsername(username string) (*domain.User, error) { - user, err := s.repo.FindByUsername(username) +func (s *service) FindByUsername(ctx context.Context, username string) (*domain.User, error) { + user, err := s.repo.FindByUsername(ctx, username) if err != nil { return nil, err }