Rewrote account switching, fix some minor bugs here and there
This commit is contained in:
@@ -2,16 +2,18 @@ package qbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
@@ -97,14 +99,22 @@ func decodeAuthHeader(header string) (string, string, error) {
|
||||
bearer := string(bytes)
|
||||
|
||||
colonIndex := strings.LastIndex(bearer, ":")
|
||||
host := bearer[:colonIndex]
|
||||
token := bearer[colonIndex+1:]
|
||||
username := bearer[:colonIndex]
|
||||
password := bearer[colonIndex+1:]
|
||||
|
||||
return host, token, nil
|
||||
if username == "" || password == "" {
|
||||
return username, password, fmt.Errorf("empty username or password")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(username), strings.TrimSpace(password), nil
|
||||
}
|
||||
|
||||
func (q *QBit) categoryContext(next http.Handler) http.Handler {
|
||||
// Print full URL for debugging
|
||||
|
||||
// Try to get category from URL query first
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Print request method and URL
|
||||
category := strings.Trim(r.URL.Query().Get("category"), "")
|
||||
if category == "" {
|
||||
// Get from form
|
||||
@@ -127,49 +137,114 @@ func (q *QBit) categoryContext(next http.Handler) http.Handler {
|
||||
// Only a valid host and token will be added to the context/config. The rest are manual
|
||||
func (q *QBit) authContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
host, token, err := decodeAuthHeader(r.Header.Get("Authorization"))
|
||||
category := getCategory(r.Context())
|
||||
arrs := wire.Get().Arr()
|
||||
// Check if arr exists
|
||||
a := arrs.Get(category)
|
||||
if a == nil {
|
||||
// Arr is not configured, create a new one
|
||||
downloadUncached := false
|
||||
a = arr.New(category, "", "", false, false, &downloadUncached, "", "auto")
|
||||
}
|
||||
if err == nil {
|
||||
host = strings.TrimSpace(host)
|
||||
if host != "" {
|
||||
a.Host = host
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
if token != "" {
|
||||
a.Token = token
|
||||
}
|
||||
}
|
||||
if cfg.NeedsAuth() {
|
||||
if a.Host == "" || a.Token == "" {
|
||||
http.Error(w, "Unauthorized: Host and token are required for authentication", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// try to use either Arr validate, or user auth validation
|
||||
if err := a.Validate(); err != nil {
|
||||
// If this failed, try to use user auth validation
|
||||
if !verifyAuth(host, token) {
|
||||
http.Error(w, "Unauthorized: Invalid host or token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.Source = "auto"
|
||||
arrs.AddOrUpdate(a)
|
||||
username, password, err := getUsernameAndPassword(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
category := getCategory(r.Context())
|
||||
a, err := q.authenticate(category, username, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), arrKey, a)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func getUsernameAndPassword(r *http.Request) (string, string, error) {
|
||||
// Try to get from authorization header
|
||||
username, password, err := decodeAuthHeader(r.Header.Get("Authorization"))
|
||||
if err == nil && username != "" {
|
||||
return username, password, err
|
||||
}
|
||||
// Try to get from cookie
|
||||
sid, err := r.Cookie("sid")
|
||||
if err != nil {
|
||||
// try SID
|
||||
sid, err = r.Cookie("SID")
|
||||
}
|
||||
if err == nil {
|
||||
username, password, err = extractFromSID(sid.Value)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
func (q *QBit) authenticate(category, username, password string) (*arr.Arr, error) {
|
||||
cfg := config.Get()
|
||||
arrs := wire.Get().Arr()
|
||||
// Check if arr exists
|
||||
a := arrs.Get(category)
|
||||
if a == nil {
|
||||
// Arr is not configured, create a new one
|
||||
downloadUncached := false
|
||||
a = arr.New(category, "", "", false, false, &downloadUncached, "", "auto")
|
||||
}
|
||||
a.Host = username
|
||||
a.Token = password
|
||||
if cfg.UseAuth {
|
||||
if a.Host == "" || a.Token == "" {
|
||||
return nil, fmt.Errorf("unauthorized: Host and token are required for authentication(you've enabled authentication)")
|
||||
}
|
||||
// try to use either Arr validate, or user auth validation
|
||||
if err := a.Validate(); err != nil {
|
||||
// If this failed, try to use user auth validation
|
||||
if !verifyAuth(username, password) {
|
||||
return nil, fmt.Errorf("unauthorized: invalid credentials")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.Source = "auto"
|
||||
arrs.AddOrUpdate(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func createSID(username, password string) string {
|
||||
// Create a verification hash
|
||||
cfg := config.Get()
|
||||
combined := fmt.Sprintf("%s|%s", username, password)
|
||||
hash := sha256.Sum256([]byte(combined + cfg.SecretKey()))
|
||||
hashStr := fmt.Sprintf("%x", hash)[:16] // First 16 chars
|
||||
// Base64 encode
|
||||
return base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s|%s", combined, hashStr)))
|
||||
}
|
||||
|
||||
func extractFromSID(sid string) (string, string, error) {
|
||||
// Decode base64
|
||||
decoded, err := base64.URLEncoding.DecodeString(sid)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid SID format")
|
||||
}
|
||||
|
||||
// Split into parts: username:password:hash
|
||||
parts := strings.Split(string(decoded), "|")
|
||||
if len(parts) != 3 {
|
||||
return "", "", fmt.Errorf("invalid SID structure")
|
||||
}
|
||||
|
||||
username := parts[0]
|
||||
password := parts[1]
|
||||
providedHash := parts[2]
|
||||
|
||||
// Verify hash
|
||||
cfg := config.Get()
|
||||
combined := fmt.Sprintf("%s|%s", username, password)
|
||||
expectedHash := sha256.Sum256([]byte(combined + cfg.SecretKey()))
|
||||
expectedHashStr := fmt.Sprintf("%x", expectedHash)[:16]
|
||||
|
||||
if providedHash != expectedHashStr {
|
||||
return "", "", fmt.Errorf("invalid SID signature")
|
||||
}
|
||||
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
func hashesContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_hashes := chi.URLParam(r, "hashes")
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/sirrobot01/decypharr/internal/request"
|
||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/internal/request"
|
||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||
)
|
||||
|
||||
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
_arr := getArrFromContext(ctx)
|
||||
if _arr == nil {
|
||||
// Arr not in context, return OK
|
||||
_, _ = w.Write([]byte("Ok."))
|
||||
cfg := config.Get()
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
a, err := q.authenticate(getCategory(ctx), username, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err := _arr.Validate(); err != nil {
|
||||
q.logger.Error().Err(err).Msgf("Error validating arr")
|
||||
http.Error(w, "Invalid arr configuration", http.StatusBadRequest)
|
||||
return
|
||||
if cfg.UseAuth {
|
||||
cookie := &http.Cookie{
|
||||
Name: "sid",
|
||||
Value: createSID(a.Host, a.Token),
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
_, _ = w.Write([]byte("Ok."))
|
||||
}
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (q *QBit) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(q.categoryContext)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(q.authContext)
|
||||
r.Post("/auth/login", q.handleLogin)
|
||||
r.Route("/torrents", func(r chi.Router) {
|
||||
r.Use(q.authContext)
|
||||
r.Use(hashesContext)
|
||||
|
||||
r.Get("/info", q.handleTorrentsInfo)
|
||||
r.Post("/info", q.handleTorrentsInfo)
|
||||
|
||||
r.Post("/add", q.handleTorrentsAdd)
|
||||
r.Post("/delete", q.handleTorrentsDelete)
|
||||
|
||||
r.Get("/categories", q.handleCategories)
|
||||
r.Post("/categories", q.handleCategories)
|
||||
|
||||
r.Post("/createCategory", q.handleCreateCategory)
|
||||
r.Post("/setCategory", q.handleSetCategory)
|
||||
r.Post("/addTags", q.handleAddTorrentTags)
|
||||
r.Post("/removeTags", q.handleRemoveTorrentTags)
|
||||
r.Post("/createTags", q.handleCreateTags)
|
||||
|
||||
r.Get("/tags", q.handleGetTags)
|
||||
r.Get("/pause", q.handleTorrentsPause)
|
||||
r.Get("/resume", q.handleTorrentsResume)
|
||||
r.Get("/recheck", q.handleTorrentRecheck)
|
||||
r.Get("/properties", q.handleTorrentProperties)
|
||||
r.Get("/files", q.handleTorrentFiles)
|
||||
|
||||
// Create POST equivalents for pause, resume, recheck
|
||||
r.Post("/tags", q.handleGetTags)
|
||||
r.Post("/pause", q.handleTorrentsPause)
|
||||
r.Post("/resume", q.handleTorrentsResume)
|
||||
r.Post("/recheck", q.handleTorrentRecheck)
|
||||
r.Post("/properties", q.handleTorrentProperties)
|
||||
r.Post("/files", q.handleTorrentFiles)
|
||||
|
||||
})
|
||||
|
||||
r.Route("/app", func(r chi.Router) {
|
||||
|
||||
Reference in New Issue
Block a user