base.py 9.45 KB
Newer Older
echel0n's avatar
echel0n committed
1
# ##############################################################################
2
3
4
#  Author: echel0n <[email protected]>
#  URL: https://sickrage.ca/
#  Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
echel0n's avatar
echel0n committed
5
#  -
6
#  This file is part of SiCKRAGE.
echel0n's avatar
echel0n committed
7
#  -
8
9
10
11
#  SiCKRAGE is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
echel0n's avatar
echel0n committed
12
#  -
13
14
15
16
#  SiCKRAGE is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
echel0n's avatar
echel0n committed
17
#  -
18
19
#  You should have received a copy of the GNU General Public License
#  along with SiCKRAGE.  If not, see <http://www.gnu.org/licenses/>.
echel0n's avatar
echel0n committed
20
# ##############################################################################
echel0n's avatar
echel0n committed
21
import functools
22
23
import time
import traceback
24
25
import types
from concurrent.futures.thread import ThreadPoolExecutor
26
from typing import Optional, Awaitable
27
28
from urllib.parse import urlparse, urljoin

29
import bleach
30
from jose import ExpiredSignatureError
31
32
from keycloak.exceptions import KeycloakClientError
from mako.exceptions import RichTraceback
33
34
from tornado import locale
from tornado.escape import utf8
echel0n's avatar
echel0n committed
35
from tornado.ioloop import IOLoop
36
37
38
from tornado.web import RequestHandler

import sickrage
39
from sickrage.core.helpers import is_ip_whitelisted, torrent_webui_url
40

41

42
class BaseHandler(RequestHandler):
43
44
    def __init__(self, application, request, **kwargs):
        super(BaseHandler, self).__init__(application, request, **kwargs)
45

46
        self.executor = ThreadPoolExecutor(thread_name_prefix='WEB-Thread')
47

48
49
        self.startTime = time.time()

50
51
52
    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
        pass

53
    def get_user_locale(self):
54
        return locale.get(sickrage.app.config.gui.gui_lang)
55
56

    def write_error(self, status_code, **kwargs):
57
        if status_code not in [401, 404] and "exc_info" in kwargs:
58
            exc_info = kwargs["exc_info"]
59
            error = repr(exc_info[1])
60

61
            sickrage.app.log.error(error)
62

63
64
65
66
67
            if self.settings.get("debug"):
                trace_info = ''.join([f"{line}<br>" for line in traceback.format_exception(*exc_info)])
                request_info = ''.join([f"<strong>{k}</strong>: {v}<br>" for k, v in self.request.__dict__.items()])

                self.set_header('Content-Type', 'text/html')
68
                return self.finish(f"""<html>
69
70
71
72
73
74
75
76
77
78
79
80
81
                                 <title>{error}</title>
                                 <body>
                                    <button onclick="window.location='{sickrage.app.config.general.web_root}/logs/';">View Log(Errors)</button>
                                    <button onclick="window.location='{sickrage.app.config.general.web_root}/home/restart?pid={sickrage.app.pid}&force=1';">Restart SiCKRAGE</button>
                                    <button onclick="window.location='{sickrage.app.config.general.web_root}/logout';">Logout</button>
                                    <h2>Error</h2>
                                    <p>{error}</p>
                                    <h2>Traceback</h2>
                                    <p>{trace_info}</p>
                                    <h2>Request Info</h2>
                                    <p>{request_info}</p>
                                 </body>
                               </html>""")
82
83

    def get_current_user(self):
84
        if is_ip_whitelisted(self.request.remote_ip):
85
            return True
86
        elif sickrage.app.config.general.sso_auth_enabled and sickrage.app.auth_server.health:
87
            try:
88
89
90
91
92
                access_token = self.get_secure_cookie('_sr_access_token')
                refresh_token = self.get_secure_cookie('_sr_refresh_token')
                if not all([access_token, refresh_token]):
                    return

93
94
95
96
                certs = sickrage.app.auth_server.certs()
                if not certs:
                    return

97
                try:
98
                    return sickrage.app.auth_server.decode_token(access_token.decode("utf-8"), certs)
99
100
                except (KeycloakClientError, ExpiredSignatureError):
                    token = sickrage.app.auth_server.refresh_token(refresh_token.decode("utf-8"))
101
102
103
                    if not token:
                        return

104
105
                    self.set_secure_cookie('_sr_access_token', token['access_token'])
                    self.set_secure_cookie('_sr_refresh_token', token['refresh_token'])
106
                    return sickrage.app.auth_server.decode_token(token['access_token'], certs)
107
            except Exception as e:
108
                return
109
        elif sickrage.app.config.general.local_auth_enabled:
110
            cookie = self.get_secure_cookie('_sr').decode() if self.get_secure_cookie('_sr') else None
111
            if cookie == sickrage.app.config.general.api_v1_key:
112
                return True
113

114
    def render(self, template_name, **kwargs):
115
116
117
118
119
120
121
122
        template_kwargs = {
            'title': "",
            'header': "",
            'topmenu': "",
            'submenu': "",
            'controller': "home",
            'action': "index",
            'srPID': sickrage.app.pid,
123
            'srHttpsEnabled': sickrage.app.config.general.enable_https or bool(self.request.headers.get('X-Forwarded-Proto') == 'https'),
124
            'srHost': self.request.headers.get('X-Forwarded-Host', self.request.host.split(':')[0]),
125
126
127
128
129
            'srHttpPort': self.request.headers.get('X-Forwarded-Port', sickrage.app.config.general.web_port),
            'srHttpsPort': sickrage.app.config.general.web_port,
            'srHandleReverseProxy': sickrage.app.config.general.handle_reverse_proxy,
            'srDefaultPage': sickrage.app.config.general.default_page.value,
            'srWebRoot': sickrage.app.config.general.web_root,
130
131
132
133
134
            'srLocale': self.get_user_locale().code,
            'srLocaleDir': sickrage.LOCALE_DIR,
            'srStartTime': self.startTime,
            'makoStartTime': time.time(),
            'overall_stats': None,
135
            'torrent_webui_url': torrent_webui_url(),
136
137
138
139
140
141
142
143
            'application': self.application,
            'request': self.request,
        }

        template_kwargs.update(self.get_template_namespace())
        template_kwargs.update(kwargs)

        try:
144
            return self.application.settings['templates'][template_name].render_unicode(**template_kwargs)
145
146
147
148
149
150
        except Exception:
            kwargs['title'] = _('HTTP Error 500')
            kwargs['header'] = _('HTTP Error 500')
            kwargs['backtrace'] = RichTraceback()
            template_kwargs.update(kwargs)

151
            sickrage.app.log.error("%s: %s" % (str(kwargs['backtrace'].error.__class__.__name__), kwargs['backtrace'].error))
152

153
            return self.application.settings['templates']['errors/500.mako'].render_unicode(**template_kwargs)
154
155
156

    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")
157
158
159
        self.set_header("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With")
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, PUT, PATCH, DELETE, OPTIONS')
        self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
160

161
162
    def redirect(self, url, permanent=True, status=None, add_web_root=True):
        if add_web_root and sickrage.app.config.general.web_root not in url:
163
            url = urljoin(sickrage.app.config.general.web_root + '/', url.lstrip('/'))
164
165
166
167
168
169
170
171
172

        if self._headers_written:
            raise Exception("Cannot redirect after headers have been written")
        if status is None:
            status = 301 if permanent else 302
        else:
            assert isinstance(status, int) and 300 <= status <= 399
        self.set_status(status)
        self.set_header("Location", utf8(url))
173
174

    def previous_url(self):
175
        url = urlparse(self.request.headers.get("referer", "/{}/".format(sickrage.app.config.general.default_page.value)))
176
177
        return url._replace(scheme="", netloc="").geturl()

178
    def _genericMessage(self, subject, message):
179
180
181
182
183
184
        return self.render('generic_message.mako',
                           message=message,
                           subject=subject,
                           title="",
                           controller='root',
                           action='genericmessage')
185
186

    def get_url(self, url):
187
188
        if sickrage.app.config.general.web_root not in url:
            url = urljoin(sickrage.app.config.general.web_root + '/', url.lstrip('/'))
189
190
191
        url = urljoin("{}://{}".format(self.request.protocol, self.request.host), url)
        return url

192
193
194
    def run_async(self, method):
        @functools.wraps(method)
        async def wrapper(self, *args, **kwargs):
195
196
            resp = await IOLoop.current().run_in_executor(self.executor, functools.partial(method, *args, **kwargs))
            self.finish(resp)
197
198
199
200
201
202
203
204
205
206

        return types.MethodType(wrapper, self)

    def prepare(self):
        method_name = self.request.method.lower()
        method = self.run_async(getattr(self, method_name))
        setattr(self, method_name, method)

    def options(self, *args, **kwargs):
        self.set_status(204)
207
208
209
210
211
212
213
214

    def get_argument(self, *args, **kwargs):
        value = super(BaseHandler, self).get_argument(*args, **kwargs)

        try:
            return bleach.clean(value)
        except TypeError:
            return value