__init__.py 10 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ##############################################################################
#  Author: echel0n <[email protected]>
#  URL: https://sickrage.ca/
#  Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
#  -
#  This file is part of SiCKRAGE.
#  -
#  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.
#  -
#  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.
#  -
#  You should have received a copy of the GNU General Public License
#  along with SiCKRAGE.  If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
21
import functools
echel0n's avatar
echel0n committed
22
23
import json
import traceback
24
import types
25
from concurrent.futures.thread import ThreadPoolExecutor
echel0n's avatar
echel0n committed
26

27
import bleach
28
import sentry_sdk
echel0n's avatar
echel0n committed
29
30
31
32
33
from apispec import APISpec
from apispec.exceptions import APISpecError
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.tornado import TornadoPlugin
from tornado.escape import to_basestring
34
from tornado.ioloop import IOLoop
echel0n's avatar
echel0n committed
35
from tornado.web import HTTPError
36
from tornado.web import RequestHandler
echel0n's avatar
echel0n committed
37
38

import sickrage
39
from sickrage.core.enums import UserPermission
40
from sickrage.core.helpers import get_internal_ip
echel0n's avatar
echel0n committed
41
42


43
44
45
46
47
class APIBaseHandler(RequestHandler):
    def __init__(self, application, request, api_version='', **kwargs):
        super(APIBaseHandler, self).__init__(application, request, **kwargs)
        self.executor = ThreadPoolExecutor(thread_name_prefix=f'API{api_version}-Thread')

echel0n's avatar
echel0n committed
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    def prepare(self):
        super(APIBaseHandler, self).prepare()

        method_name = self.request.method.lower()
        if method_name == 'options':
            return

        certs = sickrage.app.auth_server.certs()
        auth_header = self.request.headers.get('Authorization')

        if auth_header:
            if 'bearer' in auth_header.lower():
                try:
                    token = auth_header.strip('Bearer').strip()
62
                    decoded_token = sickrage.app.auth_server.decode_token(token, certs)
echel0n's avatar
echel0n committed
63
64

                    if not sickrage.app.config.user.sub_id:
65
                        sickrage.app.config.user.sub_id = decoded_token.get('sub')
66
                        sickrage.app.config.save(mark_dirty=True)
echel0n's avatar
echel0n committed
67

68
                    if sickrage.app.config.user.sub_id == decoded_token.get('sub'):
69
                        save_config = False
70
71
                        if not sickrage.app.config.user.username:
                            sickrage.app.config.user.username = decoded_token.get('preferred_username')
72
73
74
75
76
77
78
79
80
81
82
83
                            save_config = True

                        if not sickrage.app.config.user.email:
                            sickrage.app.config.user.email = decoded_token.get('email')
                            save_config = True

                        if not sickrage.app.config.user.permissions == UserPermission.SUPERUSER:
                            sickrage.app.config.user.permissions = UserPermission.SUPERUSER
                            save_config = True

                        if save_config:
                            sickrage.app.config.save()
84
85
86
87
88
89
90
91
92

                    if sickrage.app.config.user.sub_id == decoded_token.get('sub'):
                        sentry_sdk.set_user({
                            'id': sickrage.app.config.user.sub_id,
                            'username': sickrage.app.config.user.username,
                            'email': sickrage.app.config.user.email
                        })

                    if sickrage.app.config.user.sub_id != decoded_token.get('sub'):
93
                        return self._unauthorized(error='user is not authorized')
echel0n's avatar
echel0n committed
94

95
                    if not sickrage.app.api.token:
echel0n's avatar
echel0n committed
96
97
98
99
100
                        exchanged_token = sickrage.app.auth_server.token_exchange(token)
                        if exchanged_token:
                            sickrage.app.api.token = exchanged_token

                    if not sickrage.app.config.general.server_id:
echel0n's avatar
echel0n committed
101
102
103
104
105
106
107
                        server_id = sickrage.app.api.server.register_server(
                            ip_addresses=','.join([get_internal_ip()]),
                            web_protocol=self.request.protocol,
                            web_port=sickrage.app.config.general.web_port,
                            web_root=sickrage.app.config.general.web_root,
                            server_version=sickrage.version()
                        )
echel0n's avatar
echel0n committed
108

echel0n's avatar
echel0n committed
109
110
111
                        if server_id:
                            sickrage.app.config.general.server_id = server_id
                            sickrage.app.config.save()
echel0n's avatar
echel0n committed
112
113
114

                    if sickrage.app.config.general.server_id:
                        sentry_sdk.set_tag('server_id', sickrage.app.config.general.server_id)
115
116
117

                    method = self.run_async(getattr(self, method_name))
                    setattr(self, method_name, method)
echel0n's avatar
echel0n committed
118
                except Exception:
119
                    return self._unauthorized(error='failed to decode token')
echel0n's avatar
echel0n committed
120
            else:
121
                return self._unauthorized(error='invalid authorization request')
echel0n's avatar
echel0n committed
122
        else:
123
            return self._unauthorized(error='authorization header missing')
echel0n's avatar
echel0n committed
124

125
126
127
128
129
130
131
132
    def run_async(self, method):
        @functools.wraps(method)
        async def wrapper(self, *args, **kwargs):
            resp = await IOLoop.current().run_in_executor(self.executor, functools.partial(method, *args, **kwargs))
            self.finish(resp)

        return types.MethodType(wrapper, self)

echel0n's avatar
echel0n committed
133
    def get_current_user(self):
134
135
        auth_header = self.request.headers.get('Authorization')
        if 'bearer' in auth_header.lower():
136
            certs = sickrage.app.auth_server.certs()
137
138
139
140
            token = auth_header.strip('Bearer').strip()
            decoded_token = sickrage.app.auth_server.decode_token(token, certs)
            if sickrage.app.config.user.sub_id == decoded_token.get('sub'):
                return decoded_token
echel0n's avatar
echel0n committed
141
142
143
144
145
146
147
148
149

    def write_error(self, status_code, **kwargs):
        if status_code == 500:
            excp = kwargs['exc_info'][1]
            tb = kwargs['exc_info'][2]
            stack = traceback.extract_tb(tb)
            clean_stack = [i for i in stack if i[0][-6:] != 'gen.py' and i[0][-13:] != 'concurrent.py']
            error_msg = '{}\n  Exception: {}'.format(''.join(traceback.format_list(clean_stack)), excp)
        else:
150
            error_msg = kwargs.get('reason', '') or kwargs.get('error', '') or kwargs.get('errors', '')
echel0n's avatar
echel0n committed
151

152
153
        sickrage.app.log.error(error_msg)

154
        return self.finish(self.json_response(error=error_msg, status=status_code))
echel0n's avatar
echel0n committed
155
156

    def set_default_headers(self):
157
158
159
160
161
162
163
        self.set_header('X-SiCKRAGE-Server', sickrage.version())
        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With, X-SiCKRAGE-Server")
        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')

    def options(self, *args, **kwargs):
164
        self._no_content()
165
166

    def json_response(self, data=None, error=None, status=200):
echel0n's avatar
echel0n committed
167
168
        self.set_header('Content-Type', 'application/json')

169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
        self.set_status(status)

        if error is not None:
            return json.dumps({'error': error})

        if data is not None:
            return json.dumps(data)

        return None

    def _no_content(self):
        return self.json_response(status=204)

    def _unauthorized(self, error):
        return self.json_response(error=error, status=401)

    def _bad_request(self, error):
        return self.json_response(error=error, status=400)

    def _not_found(self, error):
        return self.json_response(error=error, status=404)
echel0n's avatar
echel0n committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226

    def _validate_schema(self, schema, arguments):
        return schema().validate({k: to_basestring(v[0]) if len(v) <= 1 else to_basestring(v) for k, v in arguments.items()})

    def _parse_value(self, value, func):
        if value is not None:
            try:
                return func(value)
            except ValueError:
                raise HTTPError(400, f'Invalid value {value!r}')

    def _parse_boolean(self, value):
        if isinstance(value, str):
            return value.lower() == 'true'
        return self._parse_value(value, bool)

    def generate_swagger_json(self, handlers, api_version):
        """Automatically generates Swagger spec file based on RequestHandler
        docstrings and returns it.
        """

        spec = APISpec(
            title="SiCKRAGE App API",
            version=api_version,
            openapi_version="3.0.2",
            info={'description': "Documentation for SiCKRAGE App API"},
            plugins=[TornadoPlugin(), MarshmallowPlugin()],
        )

        for handler in handlers:
            try:
                spec.path(urlspec=handler)
            except APISpecError:
                pass

        return spec.to_dict()

227
228
229
230
231
232
233
234
    def get_argument(self, *args, **kwargs):
        value = super(APIBaseHandler, self).get_argument(*args, **kwargs)

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

echel0n's avatar
echel0n committed
235

echel0n's avatar
echel0n committed
236
237
class ApiProfileHandler(APIBaseHandler):
    def get(self):
238
        return self.json_response(self.current_user)
echel0n's avatar
echel0n committed
239
240


241
class ApiPingHandler(APIBaseHandler):
echel0n's avatar
echel0n committed
242
    def get(self):
243
        return self.json_response({'message': 'pong'})
echel0n's avatar
echel0n committed
244
245


246
class ApiSwaggerDotJsonHandler(APIBaseHandler):
echel0n's avatar
echel0n committed
247
    def initialize(self, api_handlers, api_version):
248
        super(ApiSwaggerDotJsonHandler, self).initialize()
echel0n's avatar
echel0n committed
249
250
251
252
253
        self.api_handlers = sickrage.app.wserver.handlers[api_handlers]
        self.api_version = api_version

    def get(self):
        """ Get swagger.json """
254
        return self.json_response(self.generate_swagger_json(self.api_handlers, self.api_version))