quasarr 1.32.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of quasarr might be problematic. Click here for more details.

@@ -0,0 +1,250 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import os
9
+ import time
10
+
11
+ from bottle import request, response, redirect
12
+
13
+ import quasarr.providers.html_images as images
14
+ from quasarr.providers.version import get_version
15
+
16
+ # Auth configuration from environment
17
+ AUTH_USER = os.environ.get('USER', '')
18
+ AUTH_PASS = os.environ.get('PASS', '')
19
+ AUTH_TYPE = os.environ.get('AUTH', '').lower()
20
+
21
+ # Cookie settings
22
+ COOKIE_NAME = 'quasarr_session'
23
+ COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days
24
+
25
+
26
+ def is_auth_enabled():
27
+ """Check if authentication is enabled (both USER and PASS set)."""
28
+ return bool(AUTH_USER and AUTH_PASS)
29
+
30
+
31
+ def is_form_auth():
32
+ """Check if form-based auth is enabled."""
33
+ return AUTH_TYPE == 'form'
34
+
35
+
36
+ def _sign_cookie(user, expiry):
37
+ """Create HMAC signature for cookie."""
38
+ msg = f"{user}:{expiry}".encode()
39
+ return hmac.new(AUTH_PASS.encode(), msg, hashlib.sha256).hexdigest()
40
+
41
+
42
+ def _create_session_cookie(user):
43
+ """Create a signed session cookie value."""
44
+ expiry = int(time.time()) + COOKIE_MAX_AGE
45
+ signature = _sign_cookie(user, expiry)
46
+ return f"{user}:{expiry}:{signature}"
47
+
48
+
49
+ def _verify_session_cookie(cookie_value):
50
+ """Verify a session cookie. Returns True if valid."""
51
+ try:
52
+ parts = cookie_value.split(':')
53
+ if len(parts) != 3:
54
+ return False
55
+ user, expiry, signature = parts
56
+ expiry = int(expiry)
57
+ if time.time() > expiry:
58
+ return False
59
+ expected_sig = _sign_cookie(user, expiry)
60
+ return hmac.compare_digest(signature, expected_sig)
61
+ except:
62
+ return False
63
+
64
+
65
+ def check_basic_auth():
66
+ """Check HTTP Basic Auth header. Returns True if valid."""
67
+ auth = request.headers.get('Authorization', '')
68
+ if not auth.startswith('Basic '):
69
+ return False
70
+ try:
71
+ decoded = base64.b64decode(auth[6:]).decode('utf-8')
72
+ user, passwd = decoded.split(':', 1)
73
+ return user == AUTH_USER and passwd == AUTH_PASS
74
+ except:
75
+ return False
76
+
77
+
78
+ def check_form_auth():
79
+ """Check session cookie. Returns True if valid."""
80
+ cookie = request.get_cookie(COOKIE_NAME)
81
+ return cookie and _verify_session_cookie(cookie)
82
+
83
+
84
+ def require_basic_auth():
85
+ """Send 401 response for Basic Auth."""
86
+ response.status = 401
87
+ response.set_header('WWW-Authenticate', 'Basic realm="Quasarr"')
88
+ return "Authentication required"
89
+
90
+
91
+ def _render_login_page(error=None):
92
+ """Render login form page using Quasarr styling."""
93
+ error_html = f'<p style="color: #dc3545; margin-bottom: 1rem;"><b>{error}</b></p>' if error else ''
94
+ next_url = request.query.get('next', '/')
95
+
96
+ # Inline the centered HTML to avoid circular import
97
+ return f'''<html>
98
+ <head>
99
+ <meta charset="utf-8">
100
+ <meta name="viewport" content="width=device-width, initial-scale=1">
101
+ <title>Quasarr - Login</title>
102
+ <link rel="icon" href="{images.logo}" type="image/png">
103
+ <style>
104
+ :root {{
105
+ --bg-color: #ffffff;
106
+ --fg-color: #212529;
107
+ --card-bg: #ffffff;
108
+ --card-shadow: rgba(0, 0, 0, 0.1);
109
+ --primary: #0d6efd;
110
+ --secondary: #6c757d;
111
+ --spacing: 1rem;
112
+ }}
113
+ @media (prefers-color-scheme: dark) {{
114
+ :root {{
115
+ --bg-color: #181a1b;
116
+ --fg-color: #f1f1f1;
117
+ --card-bg: #242526;
118
+ --card-shadow: rgba(0, 0, 0, 0.5);
119
+ }}
120
+ }}
121
+ *, *::before, *::after {{ box-sizing: border-box; }}
122
+ html, body {{
123
+ margin: 0; padding: 0; width: 100%; height: 100%;
124
+ background-color: var(--bg-color); color: var(--fg-color);
125
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
126
+ line-height: 1.6; display: flex; flex-direction: column; min-height: 100vh;
127
+ }}
128
+ .outer {{ flex: 1; display: flex; justify-content: center; align-items: center; padding: var(--spacing); }}
129
+ .inner {{
130
+ background-color: var(--card-bg); border-radius: 1rem;
131
+ box-shadow: 0 0.5rem 1.5rem var(--card-shadow);
132
+ padding: calc(var(--spacing) * 2); text-align: center; width: 100%; max-width: 400px;
133
+ }}
134
+ .logo {{ height: 64px; width: 64px; vertical-align: middle; margin-right: 0.5rem; }}
135
+ h1 {{ margin: 0 0 1rem 0; font-size: 1.75rem; }}
136
+ h2 {{ margin: 0 0 1.5rem 0; font-size: 1.25rem; font-weight: 500; }}
137
+ label {{ display: block; text-align: left; margin-bottom: 0.25rem; font-weight: 500; }}
138
+ input[type="text"], input[type="password"] {{
139
+ width: 100%; padding: 0.5rem; margin-bottom: 1rem;
140
+ border: 1px solid var(--secondary); border-radius: 0.5rem;
141
+ font-size: 1rem; background: var(--card-bg); color: var(--fg-color);
142
+ }}
143
+ .btn-primary {{
144
+ background-color: var(--primary); color: #fff; border: 1.5px solid #0856c7;
145
+ padding: 0.5rem 1.5rem; font-size: 1rem; border-radius: 0.5rem;
146
+ font-weight: 500; cursor: pointer; width: 100%;
147
+ }}
148
+ .btn-primary:hover {{ background-color: #0b5ed7; }}
149
+ footer {{ text-align: center; font-size: 0.75rem; color: var(--secondary); padding: 0.5rem 0; }}
150
+ </style>
151
+ </head>
152
+ <body>
153
+ <div class="outer">
154
+ <div class="inner">
155
+ <h1><img src="{images.logo}" class="logo" alt="Quasarr"/>Quasarr</h1>
156
+ {error_html}
157
+ <form method="post" action="/login">
158
+ <input type="hidden" name="next" value="{next_url}">
159
+ <label for="username">Username</label>
160
+ <input type="text" id="username" name="username" autocomplete="username" required>
161
+ <label for="password">Password</label>
162
+ <input type="password" id="password" name="password" autocomplete="current-password" required>
163
+ <button type="submit" class="btn-primary">Login</button>
164
+ </form>
165
+ </div>
166
+ </div>
167
+ <footer>Quasarr v.{get_version()}</footer>
168
+ </body>
169
+ </html>'''
170
+
171
+
172
+ def _handle_login_post():
173
+ """Handle login form submission."""
174
+ username = request.forms.get('username', '')
175
+ password = request.forms.get('password', '')
176
+ next_url = request.forms.get('next', '/')
177
+
178
+ if username == AUTH_USER and password == AUTH_PASS:
179
+ cookie_value = _create_session_cookie(username)
180
+ response.set_cookie(COOKIE_NAME, cookie_value, max_age=COOKIE_MAX_AGE, path='/', httponly=True)
181
+ redirect(next_url)
182
+ else:
183
+ return _render_login_page(error="Invalid username or password")
184
+
185
+
186
+ def _handle_logout():
187
+ """Handle logout - clear cookie and redirect to login."""
188
+ response.delete_cookie(COOKIE_NAME, path='/')
189
+ redirect('/login')
190
+
191
+
192
+ def show_logout_link():
193
+ """Returns True if logout link should be shown (form auth enabled and authenticated)."""
194
+ return is_auth_enabled() and is_form_auth() and check_form_auth()
195
+
196
+
197
+ def add_auth_routes(app):
198
+ """Add login/logout routes to a Bottle app (for form auth only)."""
199
+ if not is_auth_enabled():
200
+ return
201
+
202
+ if is_form_auth():
203
+ @app.get('/login')
204
+ def login_get():
205
+ if check_form_auth():
206
+ redirect('/')
207
+ return _render_login_page()
208
+
209
+ @app.post('/login')
210
+ def login_post():
211
+ return _handle_login_post()
212
+
213
+ @app.get('/logout')
214
+ def logout():
215
+ return _handle_logout()
216
+
217
+
218
+ def add_auth_hook(app, whitelist_prefixes=None):
219
+ """Add authentication hook to a Bottle app.
220
+
221
+ Args:
222
+ app: Bottle application
223
+ whitelist_prefixes: List of path prefixes to skip auth (e.g., ['/api/', '/sponsors_helper/'])
224
+ """
225
+ if whitelist_prefixes is None:
226
+ whitelist_prefixes = []
227
+
228
+ @app.hook('before_request')
229
+ def auth_hook():
230
+ if not is_auth_enabled():
231
+ return
232
+
233
+ path = request.path
234
+
235
+ # Always allow login/logout
236
+ if path in ['/login', '/logout']:
237
+ return
238
+
239
+ # Check whitelist prefixes
240
+ for prefix in whitelist_prefixes:
241
+ if path.startswith(prefix):
242
+ return
243
+
244
+ # Check authentication
245
+ if is_form_auth():
246
+ if not check_form_auth():
247
+ redirect(f'/login?next={path}')
248
+ else:
249
+ if not check_basic_auth():
250
+ return require_basic_auth()