Commit | Line | Data |
---|---|---|
c840cb66 | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU Affero General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
405aa45a | 17 | import datetime |
060a7a7b | 18 | import urllib |
4990b47c | 19 | |
20238f54 BP |
20 | import six |
21 | ||
32ff6f4d | 22 | from oauthlib.oauth1.rfc5849.utils import UNICODE_ASCII_CHARACTER_SET |
617bff18 JT |
23 | from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint, |
24 | AccessTokenEndpoint) | |
42dbb26a | 25 | |
617bff18 | 26 | from mediagoblin.decorators import require_active_login |
d41c6a53 | 27 | from mediagoblin.tools.translate import pass_to_ugettext |
4990b47c | 28 | from mediagoblin.meddleware.csrf import csrf_exempt |
786bbd79 | 29 | from mediagoblin.tools.request import decode_request |
42dbb26a | 30 | from mediagoblin.tools.response import (render_to_response, redirect, |
2b60a56c | 31 | json_response, render_400, |
32 | form_response) | |
4990b47c | 33 | from mediagoblin.tools.crypto import random_string |
c33a34d4 | 34 | from mediagoblin.tools.validator import validate_email, validate_url |
005181b1 | 35 | from mediagoblin.oauth.forms import AuthorizeForm |
36 | from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest | |
37 | from mediagoblin.oauth.tools.request import decode_authorization_header | |
38 | from mediagoblin.oauth.tools.forms import WTFormData | |
617bff18 | 39 | from mediagoblin.db.models import NonceTimestamp, Client, RequestToken |
c840cb66 | 40 | |
41 | # possible client types | |
c5eb24b8 | 42 | CLIENT_TYPES = ["web", "native"] # currently what pump supports |
c840cb66 | 43 | |
4990b47c | 44 | @csrf_exempt |
c840cb66 | 45 | def client_register(request): |
46 | """ Endpoint for client registration """ | |
d41c6a53 | 47 | try: |
42dbb26a | 48 | data = decode_request(request) |
d41c6a53 | 49 | except ValueError: |
50 | error = "Could not decode data." | |
51 | return json_response({"error": error}, status=400) | |
52 | ||
53 | if data is "": | |
54 | error = "Unknown Content-Type" | |
55 | return json_response({"error": error}, status=400) | |
c840cb66 | 56 | |
4990b47c | 57 | if "type" not in data: |
d41c6a53 | 58 | error = "No registration type provided." |
59 | return json_response({"error": error}, status=400) | |
c5eb24b8 | 60 | if data.get("application_type", None) not in CLIENT_TYPES: |
d41c6a53 | 61 | error = "Unknown application_type." |
62 | return json_response({"error": error}, status=400) | |
42dbb26a | 63 | |
54fbbf09 | 64 | client_type = data["type"] |
65 | ||
66 | if client_type == "client_update": | |
67 | # updating a client | |
68 | if "client_id" not in data: | |
d41c6a53 | 69 | error = "client_id is requried to update." |
70 | return json_response({"error": error}, status=400) | |
54fbbf09 | 71 | elif "client_secret" not in data: |
d41c6a53 | 72 | error = "client_secret is required to update." |
73 | return json_response({"error": error}, status=400) | |
54fbbf09 | 74 | |
d41c6a53 | 75 | client = Client.query.filter_by( |
42dbb26a | 76 | id=data["client_id"], |
d41c6a53 | 77 | secret=data["client_secret"] |
78 | ).first() | |
54fbbf09 | 79 | |
c33a34d4 | 80 | if client is None: |
d41c6a53 | 81 | error = "Unauthorized." |
82 | return json_response({"error": error}, status=403) | |
83 | ||
84 | client.application_name = data.get( | |
42dbb26a | 85 | "application_name", |
d41c6a53 | 86 | client.application_name |
87 | ) | |
88 | ||
89 | client.application_type = data.get( | |
90 | "application_type", | |
91 | client.application_type | |
92 | ) | |
54fbbf09 | 93 | |
763e300d | 94 | app_name = ("application_type", client.application_name) |
c5eb24b8 | 95 | if app_name in CLIENT_TYPES: |
763e300d | 96 | client.application_name = app_name |
763e300d | 97 | |
54fbbf09 | 98 | elif client_type == "client_associate": |
99 | # registering | |
100 | if "client_id" in data: | |
d41c6a53 | 101 | error = "Only set client_id for update." |
102 | return json_response({"error": error}, status=400) | |
54fbbf09 | 103 | elif "access_token" in data: |
d41c6a53 | 104 | error = "access_token not needed for registration." |
105 | return json_response({"error": error}, status=400) | |
54fbbf09 | 106 | elif "client_secret" in data: |
d41c6a53 | 107 | error = "Only set client_secret for update." |
108 | return json_response({"error": error}, status=400) | |
54fbbf09 | 109 | |
c33a34d4 | 110 | # generate the client_id and client_secret |
32ff6f4d JT |
111 | client_id = random_string(22, UNICODE_ASCII_CHARACTER_SET) |
112 | client_secret = random_string(43, UNICODE_ASCII_CHARACTER_SET) | |
c33a34d4 | 113 | expirey = 0 # for now, lets not have it expire |
114 | expirey_db = None if expirey == 0 else expirey | |
42dbb26a RE |
115 | application_type = data["application_type"] |
116 | ||
c33a34d4 | 117 | # save it |
118 | client = Client( | |
42dbb26a RE |
119 | id=client_id, |
120 | secret=client_secret, | |
c33a34d4 | 121 | expirey=expirey_db, |
86ba4168 | 122 | application_type=application_type, |
d41c6a53 | 123 | ) |
c33a34d4 | 124 | |
125 | else: | |
d41c6a53 | 126 | error = "Invalid registration type" |
127 | return json_response({"error": error}, status=400) | |
c33a34d4 | 128 | |
670cdef7 JT |
129 | logo_uri = data.get("logo_uri", client.logo_url) |
130 | if logo_uri is not None and not validate_url(logo_uri): | |
131 | error = "Logo URI {0} is not a valid URI.".format(logo_uri) | |
d41c6a53 | 132 | return json_response( |
42dbb26a | 133 | {"error": error}, |
d41c6a53 | 134 | status=400 |
135 | ) | |
c33a34d4 | 136 | else: |
670cdef7 | 137 | client.logo_url = logo_uri |
42dbb26a | 138 | |
405aa45a | 139 | client.application_name = data.get("application_name", None) |
c33a34d4 | 140 | |
86ba4168 | 141 | contacts = data.get("contacts", None) |
c33a34d4 | 142 | if contacts is not None: |
20238f54 | 143 | if not isinstance(contacts, six.text_type): |
d41c6a53 | 144 | error = "Contacts must be a string of space-seporated email addresses." |
145 | return json_response({"error": error}, status=400) | |
c33a34d4 | 146 | |
147 | contacts = contacts.split() | |
148 | for contact in contacts: | |
149 | if not validate_email(contact): | |
150 | # not a valid email | |
d41c6a53 | 151 | error = "Email {0} is not a valid email.".format(contact) |
152 | return json_response({"error": error}, status=400) | |
42dbb26a RE |
153 | |
154 | ||
c33a34d4 | 155 | client.contacts = contacts |
156 | ||
86ba4168 | 157 | redirect_uris = data.get("redirect_uris", None) |
158 | if redirect_uris is not None: | |
20238f54 | 159 | if not isinstance(redirect_uris, six.text_type): |
d41c6a53 | 160 | error = "redirect_uris must be space-seporated URLs." |
617bff18 | 161 | return json_response({"error": error}, status=400) |
c33a34d4 | 162 | |
86ba4168 | 163 | redirect_uris = redirect_uris.split() |
c33a34d4 | 164 | |
86ba4168 | 165 | for uri in redirect_uris: |
c33a34d4 | 166 | if not validate_url(uri): |
167 | # not a valid uri | |
d41c6a53 | 168 | error = "URI {0} is not a valid URI".format(uri) |
169 | return json_response({"error": error}, status=400) | |
c33a34d4 | 170 | |
86ba4168 | 171 | client.redirect_uri = redirect_uris |
c33a34d4 | 172 | |
42dbb26a | 173 | |
4990b47c | 174 | client.save() |
c840cb66 | 175 | |
c33a34d4 | 176 | expirey = 0 if client.expirey is None else client.expirey |
177 | ||
4990b47c | 178 | return json_response( |
179 | { | |
d41c6a53 | 180 | "client_id": client.id, |
181 | "client_secret": client.secret, | |
182 | "expires_at": expirey, | |
4990b47c | 183 | }) |
d41c6a53 | 184 | |
d41c6a53 | 185 | @csrf_exempt |
186 | def request_token(request): | |
187 | """ Returns request token """ | |
188 | try: | |
42dbb26a | 189 | data = decode_request(request) |
d41c6a53 | 190 | except ValueError: |
191 | error = "Could not decode data." | |
192 | return json_response({"error": error}, status=400) | |
193 | ||
2b60a56c | 194 | if not data and request.headers: |
195 | data = request.headers | |
42dbb26a | 196 | |
2b60a56c | 197 | data = dict(data) # mutableifying |
405aa45a | 198 | |
2b60a56c | 199 | authorization = decode_authorization_header(data) |
d41c6a53 | 200 | |
2b60a56c | 201 | if authorization == dict() or u"oauth_consumer_key" not in authorization: |
405aa45a | 202 | error = "Missing required parameter." |
2b60a56c | 203 | return json_response({"error": error}, status=400) |
405aa45a | 204 | |
d41c6a53 | 205 | # check the client_id |
2b60a56c | 206 | client_id = authorization[u"oauth_consumer_key"] |
d41c6a53 | 207 | client = Client.query.filter_by(id=client_id).first() |
89d5b44e | 208 | |
209 | if client == None: | |
d41c6a53 | 210 | # client_id is invalid |
211 | error = "Invalid client_id" | |
212 | return json_response({"error": error}, status=400) | |
213 | ||
e9bb5879 | 214 | # make request token and return to client |
2b60a56c | 215 | request_validator = GMGRequestValidator(authorization) |
d41c6a53 | 216 | rv = RequestTokenEndpoint(request_validator) |
405aa45a | 217 | tokens = rv.create_request_token(request, authorization) |
d41c6a53 | 218 | |
cfe7054c | 219 | # store the nonce & timestamp before we return back |
220 | nonce = authorization[u"oauth_nonce"] | |
221 | timestamp = authorization[u"oauth_timestamp"] | |
89d5b44e | 222 | timestamp = datetime.datetime.fromtimestamp(float(timestamp)) |
cfe7054c | 223 | |
224 | nc = NonceTimestamp(nonce=nonce, timestamp=timestamp) | |
225 | nc.save() | |
226 | ||
2b60a56c | 227 | return form_response(tokens) |
405aa45a | 228 | |
42dbb26a | 229 | @require_active_login |
d41c6a53 | 230 | def authorize(request): |
231 | """ Displays a page for user to authorize """ | |
405aa45a | 232 | if request.method == "POST": |
233 | return authorize_finish(request) | |
42dbb26a | 234 | |
d41c6a53 | 235 | _ = pass_to_ugettext |
236 | token = request.args.get("oauth_token", None) | |
237 | if token is None: | |
238 | # no token supplied, display a html 400 this time | |
405aa45a | 239 | err_msg = _("Must provide an oauth_token.") |
d41c6a53 | 240 | return render_400(request, err_msg=err_msg) |
241 | ||
405aa45a | 242 | oauth_request = RequestToken.query.filter_by(token=token).first() |
243 | if oauth_request is None: | |
244 | err_msg = _("No request token found.") | |
245 | return render_400(request, err_msg) | |
42dbb26a | 246 | |
405aa45a | 247 | if oauth_request.used: |
248 | return authorize_finish(request) | |
42dbb26a | 249 | |
405aa45a | 250 | if oauth_request.verifier is None: |
786bbd79 | 251 | orequest = GMGRequest(request) |
24e12cb1 | 252 | orequest.resource_owner_key = token |
405aa45a | 253 | request_validator = GMGRequestValidator() |
254 | auth_endpoint = AuthorizationEndpoint(request_validator) | |
255 | verifier = auth_endpoint.create_verifier(orequest, {}) | |
256 | oauth_request.verifier = verifier["oauth_verifier"] | |
257 | ||
fd703bb4 | 258 | oauth_request.actor = request.user.id |
405aa45a | 259 | oauth_request.save() |
260 | ||
261 | # find client & build context | |
262 | client = Client.query.filter_by(id=oauth_request.client).first() | |
263 | ||
264 | authorize_form = AuthorizeForm(WTFormData({ | |
265 | "oauth_token": oauth_request.token, | |
266 | "oauth_verifier": oauth_request.verifier | |
267 | })) | |
268 | ||
269 | context = { | |
270 | "user": request.user, | |
271 | "oauth_request": oauth_request, | |
272 | "client": client, | |
273 | "authorize_form": authorize_form, | |
274 | } | |
275 | ||
276 | ||
d41c6a53 | 277 | # AuthorizationEndpoint |
405aa45a | 278 | return render_to_response( |
279 | request, | |
280 | "mediagoblin/api/authorize.html", | |
281 | context | |
282 | ) | |
42dbb26a | 283 | |
405aa45a | 284 | |
285 | def authorize_finish(request): | |
286 | """ Finishes the authorize """ | |
287 | _ = pass_to_ugettext | |
288 | token = request.form["oauth_token"] | |
289 | verifier = request.form["oauth_verifier"] | |
290 | oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier) | |
291 | oauth_request = oauth_request.first() | |
42dbb26a | 292 | |
405aa45a | 293 | if oauth_request is None: |
294 | # invalid token or verifier | |
295 | err_msg = _("No request token found.") | |
296 | return render_400(request, err_msg) | |
297 | ||
298 | oauth_request.used = True | |
299 | oauth_request.updated = datetime.datetime.now() | |
300 | oauth_request.save() | |
301 | ||
302 | if oauth_request.callback == "oob": | |
303 | # out of bounds | |
304 | context = {"oauth_request": oauth_request} | |
305 | return render_to_response( | |
306 | request, | |
307 | "mediagoblin/api/oob.html", | |
308 | context | |
309 | ) | |
310 | ||
311 | # okay we need to redirect them then! | |
312 | querystring = "?oauth_token={0}&oauth_verifier={1}".format( | |
313 | oauth_request.token, | |
314 | oauth_request.verifier | |
315 | ) | |
316 | ||
060a7a7b JT |
317 | # It's come from the OAuth headers so it'll be encoded. |
318 | redirect_url = urllib.unquote(oauth_request.callback) | |
319 | ||
405aa45a | 320 | return redirect( |
321 | request, | |
322 | querystring=querystring, | |
060a7a7b | 323 | location=redirect_url |
405aa45a | 324 | ) |
d41c6a53 | 325 | |
326 | @csrf_exempt | |
327 | def access_token(request): | |
42dbb26a | 328 | """ Provides an access token based on a valid verifier and request token """ |
2b60a56c | 329 | data = request.headers |
405aa45a | 330 | |
42dbb26a | 331 | parsed_tokens = decode_authorization_header(data) |
2b60a56c | 332 | |
333 | if parsed_tokens == dict() or "oauth_token" not in parsed_tokens: | |
334 | error = "Missing required parameter." | |
405aa45a | 335 | return json_response({"error": error}, status=400) |
336 | ||
24e12cb1 | 337 | request.resource_owner_key = parsed_tokens["oauth_consumer_key"] |
2b60a56c | 338 | request.oauth_token = parsed_tokens["oauth_token"] |
339 | request_validator = GMGRequestValidator(data) | |
86ee2d1a JT |
340 | |
341 | # Check that the verifier is valid | |
342 | verifier_valid = request_validator.validate_verifier( | |
343 | token=request.oauth_token, | |
344 | verifier=parsed_tokens["oauth_verifier"] | |
345 | ) | |
346 | if not verifier_valid: | |
347 | error = "Verifier code or token incorrect" | |
348 | return json_response({"error": error}, status=401) | |
349 | ||
2b60a56c | 350 | av = AccessTokenEndpoint(request_validator) |
351 | tokens = av.create_access_token(request, {}) | |
352 | return form_response(tokens) |