Rewrote account switching, fix some minor bugs here and there

This commit is contained in:
Mukhtar Akere
2025-09-16 21:15:24 +01:00
parent 76f5b85313
commit 30b2db06e7
34 changed files with 945 additions and 866 deletions

View File

@@ -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")