1
|
|
|
from plugin.api.core.exceptions import ApiError |
2
|
|
|
from plugin.preferences import Preferences |
3
|
|
|
|
4
|
|
|
from threading import Lock |
5
|
|
|
import logging |
6
|
|
|
import six |
7
|
|
|
|
8
|
|
|
log = logging.getLogger(__name__) |
9
|
|
|
|
10
|
|
|
|
11
|
|
|
class ApiContext(object): |
12
|
|
|
def __init__(self, method, headers, body): |
13
|
|
|
self.method = method |
14
|
|
|
self.headers = headers |
15
|
|
|
self.body = body |
16
|
|
|
|
17
|
|
|
# Validate token |
18
|
|
|
self.token = self._validate_token() |
19
|
|
|
|
20
|
|
|
def _validate_token(self): |
21
|
|
|
token = self.headers.get('X-Channel-Token') if self.headers else None |
22
|
|
|
|
23
|
|
|
if not token: |
24
|
|
|
return None |
25
|
|
|
|
26
|
|
|
# Validate `token` |
27
|
|
|
system = ApiManager.get_service('system') |
28
|
|
|
|
29
|
|
|
try: |
30
|
|
|
return system.validate(token) |
31
|
|
|
except: |
|
|
|
|
32
|
|
|
return None |
33
|
|
|
|
34
|
|
|
|
35
|
|
|
class ApiManager(object): |
36
|
|
|
service_classes = {} |
37
|
|
|
services = {} |
38
|
|
|
|
39
|
|
|
# Call attributes |
40
|
|
|
lock = Lock() |
41
|
|
|
context = None |
42
|
|
|
|
43
|
|
|
@classmethod |
44
|
|
|
def process(cls, method, headers, body, key, *args, **kwargs): |
45
|
|
|
log.debug('Handling API %s request %r - args: %r, kwargs: %r', method, key, len(args), len(kwargs.keys())) |
46
|
|
|
|
47
|
|
|
if not Preferences.get('api.enabled'): |
48
|
|
|
log.debug('Unable to process request, API is currently disabled') |
49
|
|
|
return cls.build_error('disabled', 'Unable to process request, API is currently disabled') |
50
|
|
|
|
51
|
|
|
k_service, k_method = key.rsplit('.', 1) |
52
|
|
|
|
53
|
|
|
# Try find matching service |
54
|
|
|
service = cls.get_service(k_service) |
55
|
|
|
|
56
|
|
|
if service is None: |
57
|
|
|
log.warn('Unable to find service: %r', k_service) |
58
|
|
|
return cls.build_error('unknown.service', 'Unable to find service: %r' % k_service) |
59
|
|
|
|
60
|
|
|
func = getattr(service, k_method, None) |
61
|
|
|
|
62
|
|
|
if func is None: |
63
|
|
|
log.warn('Unable to find method: %r', k_method) |
64
|
|
|
return cls.build_error('unknown.method', 'Unable to find method: %r' % k_method) |
65
|
|
|
|
66
|
|
|
# Validate |
67
|
|
|
meta = getattr(func, '__meta__', {}) |
68
|
|
|
|
69
|
|
|
if not meta.get('exposed', False): |
70
|
|
|
log.warn('Method is not exposed: %r', k_method) |
71
|
|
|
return cls.build_error('restricted.method', 'Method is not exposed: %r' % k_method) |
72
|
|
|
|
73
|
|
|
# Decode strings in the `args` parameter |
74
|
|
|
try: |
75
|
|
|
args = cls.decode(args) |
76
|
|
|
except Exception as ex: |
|
|
|
|
77
|
|
|
return cls.build_error('args.decode_error', 'Unable to decode provided args') |
78
|
|
|
|
79
|
|
|
# Decode strings in the `kwargs` parameter |
80
|
|
|
try: |
81
|
|
|
kwargs = cls.decode(kwargs) |
82
|
|
|
except Exception as ex: |
|
|
|
|
83
|
|
|
return cls.build_error('kwargs.decode_error', 'Unable to decode provided kwargs') |
84
|
|
|
|
85
|
|
|
# Execute request handler |
86
|
|
|
try: |
87
|
|
|
result = cls.call(method, headers, body, func, args, kwargs) |
88
|
|
|
except ApiError as ex: |
89
|
|
|
log.warn('Error returned while handling request %r: %r', key, ex, exc_info=True) |
90
|
|
|
return cls.build_error('error.%s' % ex.code, ex.message) |
91
|
|
|
except Exception as ex: |
|
|
|
|
92
|
|
|
log.error('Exception raised while handling request %r: %s', key, ex, exc_info=True) |
93
|
|
|
return cls.build_error('exception', 'Exception raised while handling the request') |
94
|
|
|
|
95
|
|
|
# Build response |
96
|
|
|
return cls.build_response(result) |
97
|
|
|
|
98
|
|
|
@classmethod |
99
|
|
|
def call(cls, method, headers, body, func, args, kwargs): |
100
|
|
|
with cls.lock: |
101
|
|
|
# Construct context |
102
|
|
|
cls.context = ApiContext(method, headers, body) |
103
|
|
|
|
104
|
|
|
# Call function |
105
|
|
|
return func(*args, **kwargs) |
106
|
|
|
|
107
|
|
|
@classmethod |
108
|
|
|
def register(cls, service): |
109
|
|
|
key = service.__key__ |
110
|
|
|
|
111
|
|
|
if not key: |
112
|
|
|
log.warn('Service %r has an invalid "__key__" attribute', service) |
113
|
|
|
return |
114
|
|
|
|
115
|
|
|
cls.service_classes[key] = service |
116
|
|
|
|
117
|
|
|
log.debug('Registered service: %r (%r)', key, service) |
118
|
|
|
|
119
|
|
|
@classmethod |
120
|
|
|
def get_service(cls, key): |
121
|
|
|
if key in cls.services: |
122
|
|
|
# Service already constructed |
123
|
|
|
return cls.services[key] |
124
|
|
|
|
125
|
|
|
if key not in cls.service_classes: |
126
|
|
|
# Service doesn't exist |
127
|
|
|
return None |
128
|
|
|
|
129
|
|
|
# Construct service |
130
|
|
|
cls.services[key] = cls.service_classes[key](cls) |
131
|
|
|
|
132
|
|
|
return cls.services[key] |
133
|
|
|
|
134
|
|
|
@classmethod |
135
|
|
|
def decode(cls, data): |
136
|
|
|
if not data: |
137
|
|
|
return data |
138
|
|
|
|
139
|
|
|
# Strings |
140
|
|
|
if isinstance(data, six.string_types): |
141
|
|
|
try: |
142
|
|
|
return data.decode('unicode-escape') |
143
|
|
|
except Exception as ex: |
|
|
|
|
144
|
|
|
log.warn('Unable to decode string: %s', ex, exc_info=True) |
145
|
|
|
return data |
146
|
|
|
|
147
|
|
|
# Collections |
148
|
|
|
if type(data) is dict: |
149
|
|
|
return dict([ |
150
|
|
|
(cls.decode(key), cls.decode(value)) |
151
|
|
|
for key, value in data.items() |
152
|
|
|
]) |
153
|
|
|
|
154
|
|
|
if type(data) is list: |
155
|
|
|
return [ |
156
|
|
|
cls.decode(value) |
157
|
|
|
for value in data |
158
|
|
|
] |
159
|
|
|
|
160
|
|
|
if type(data) is tuple: |
161
|
|
|
return tuple([ |
162
|
|
|
cls.decode(value) |
163
|
|
|
for value in list(data) |
164
|
|
|
]) |
165
|
|
|
|
166
|
|
|
return data |
167
|
|
|
|
168
|
|
|
@classmethod |
169
|
|
|
def build_error(cls, code, message=None): |
170
|
|
|
result = { |
171
|
|
|
'error': { |
172
|
|
|
'code': code |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
if message: |
177
|
|
|
result['error']['message'] = message |
178
|
|
|
|
179
|
|
|
return result |
180
|
|
|
|
181
|
|
|
@classmethod |
182
|
|
|
def build_response(cls, result): |
183
|
|
|
return { |
184
|
|
|
'result': result |
185
|
|
|
} |
186
|
|
|
|
Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.