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 |
c5eb24b8 | 18 | import string |
4990b47c | 19 | |
617bff18 JT |
20 | from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint, |
21 | AccessTokenEndpoint) | |
42dbb26a | 22 | |
617bff18 | 23 | from mediagoblin.decorators import require_active_login |
d41c6a53 | 24 | from mediagoblin.tools.translate import pass_to_ugettext |
4990b47c | 25 | from mediagoblin.meddleware.csrf import csrf_exempt |
786bbd79 | 26 | from mediagoblin.tools.request import decode_request |
42dbb26a | 27 | from mediagoblin.tools.response import (render_to_response, redirect, |
2b60a56c | 28 | json_response, render_400, |
29 | form_response) | |
4990b47c | 30 | from mediagoblin.tools.crypto import random_string |
c33a34d4 | 31 | from mediagoblin.tools.validator import validate_email, validate_url |
005181b1 | 32 | from mediagoblin.oauth.forms import AuthorizeForm |
33 | from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest | |
34 | from mediagoblin.oauth.tools.request import decode_authorization_header | |
35 | from mediagoblin.oauth.tools.forms import WTFormData | |
617bff18 | 36 | from mediagoblin.db.models import NonceTimestamp, Client, RequestToken |
c840cb66 | 37 | |
38 | # possible client types | |
c5eb24b8 JT |
39 | CLIENT_TYPES = ["web", "native"] # currently what pump supports |
40 | OAUTH_ALPHABET = (string.ascii_letters.decode('ascii') + | |
41 | string.digits.decode('ascii')) | |
c840cb66 | 42 | |
4990b47c | 43 | @csrf_exempt |
c840cb66 | 44 | def client_register(request): |
45 | """ Endpoint for client registration """ | |
d41c6a53 | 46 | try: |
42dbb26a | 47 | data = decode_request(request) |
d41c6a53 | 48 | except ValueError: |
49 | error = "Could not decode data." | |
50 | return json_response({"error": error}, status=400) | |
51 | ||
52 | if data is "": | |
53 | error = "Unknown Content-Type" | |
54 | return json_response({"error": error}, status=400) | |
c840cb66 | 55 | |
4990b47c | 56 | if "type" not in data: |
d41c6a53 | 57 | error = "No registration type provided." |
58 | return json_response({"error": error}, status=400) | |
c5eb24b8 | 59 | if data.get("application_type", None) not in CLIENT_TYPES: |
d41c6a53 | 60 | error = "Unknown application_type." |
61 | return json_response({"error": error}, status=400) | |
42dbb26a | 62 | |
54fbbf09 | 63 | client_type = data["type"] |
64 | ||
65 | if client_type == "client_update": | |
66 | # updating a client | |
67 | if "client_id" not in data: | |
d41c6a53 | 68 | error = "client_id is requried to update." |
69 | return json_response({"error": error}, status=400) | |
54fbbf09 | 70 | elif "client_secret" not in data: |
d41c6a53 | 71 | error = "client_secret is required to update." |
72 | return json_response({"error": error}, status=400) | |
54fbbf09 | 73 | |
d41c6a53 | 74 | client = Client.query.filter_by( |
42dbb26a | 75 | id=data["client_id"], |
d41c6a53 | 76 | secret=data["client_secret"] |
77 | ).first() | |
54fbbf09 | 78 | |
c33a34d4 | 79 | if client is None: |
d41c6a53 | 80 | error = "Unauthorized." |
81 | return json_response({"error": error}, status=403) | |
82 | ||
83 | client.application_name = data.get( | |
42dbb26a | 84 | "application_name", |
d41c6a53 | 85 | client.application_name |
86 | ) | |
87 | ||
88 | client.application_type = data.get( | |
89 | "application_type", | |
90 | client.application_type | |
91 | ) | |
54fbbf09 | 92 | |
763e300d | 93 | app_name = ("application_type", client.application_name) |
c5eb24b8 | 94 | if app_name in CLIENT_TYPES: |
763e300d | 95 | client.application_name = app_name |
763e300d | 96 | |
54fbbf09 | 97 | elif client_type == "client_associate": |
98 | # registering | |
99 | if "client_id" in data: | |
d41c6a53 | 100 | error = "Only set client_id for update." |
101 | return json_response({"error": error}, status=400) | |
54fbbf09 | 102 | elif "access_token" in data: |
d41c6a53 | 103 | error = "access_token not needed for registration." |
104 | return json_response({"error": error}, status=400) | |
54fbbf09 | 105 | elif "client_secret" in data: |
d41c6a53 | 106 | error = "Only set client_secret for update." |
107 | return json_response({"error": error}, status=400) | |
54fbbf09 | 108 | |
c33a34d4 | 109 | # generate the client_id and client_secret |
c5eb24b8 JT |
110 | client_id = random_string(22, OAUTH_ALPHABET) |
111 | client_secret = random_string(43, OAUTH_ALPHABET) | |
c33a34d4 | 112 | expirey = 0 # for now, lets not have it expire |
113 | expirey_db = None if expirey == 0 else expirey | |
42dbb26a RE |
114 | application_type = data["application_type"] |
115 | ||
c33a34d4 | 116 | # save it |
117 | client = Client( | |
42dbb26a RE |
118 | id=client_id, |
119 | secret=client_secret, | |
c33a34d4 | 120 | expirey=expirey_db, |
86ba4168 | 121 | application_type=application_type, |
d41c6a53 | 122 | ) |
c33a34d4 | 123 | |
124 | else: | |
d41c6a53 | 125 | error = "Invalid registration type" |
126 | return json_response({"error": error}, status=400) | |
c33a34d4 | 127 | |
128 | logo_url = data.get("logo_url", client.logo_url) | |
129 | if logo_url is not None and not validate_url(logo_url): | |
d41c6a53 | 130 | error = "Logo URL {0} is not a valid URL.".format(logo_url) |
131 | return json_response( | |
42dbb26a | 132 | {"error": error}, |
d41c6a53 | 133 | status=400 |
134 | ) | |
c33a34d4 | 135 | else: |
136 | client.logo_url = logo_url | |
42dbb26a | 137 | |
405aa45a | 138 | client.application_name = data.get("application_name", None) |
c33a34d4 | 139 | |
86ba4168 | 140 | contacts = data.get("contacts", None) |
c33a34d4 | 141 | if contacts is not None: |
142 | if type(contacts) is not unicode: | |
d41c6a53 | 143 | error = "Contacts must be a string of space-seporated email addresses." |
144 | return json_response({"error": error}, status=400) | |
c33a34d4 | 145 | |
146 | contacts = contacts.split() | |
147 | for contact in contacts: | |
148 | if not validate_email(contact): | |
149 | # not a valid email | |
d41c6a53 | 150 | error = "Email {0} is not a valid email.".format(contact) |
151 | return json_response({"error": error}, status=400) | |
42dbb26a RE |
152 | |
153 | ||
c33a34d4 | 154 | client.contacts = contacts |
155 | ||
86ba4168 | 156 | redirect_uris = data.get("redirect_uris", None) |
157 | if redirect_uris is not None: | |
158 | if type(redirect_uris) is not unicode: | |
d41c6a53 | 159 | error = "redirect_uris must be space-seporated URLs." |
617bff18 | 160 | return json_response({"error": error}, status=400) |
c33a34d4 | 161 | |
86ba4168 | 162 | redirect_uris = redirect_uris.split() |
c33a34d4 | 163 | |
86ba4168 | 164 | for uri in redirect_uris: |
c33a34d4 | 165 | if not validate_url(uri): |
166 | # not a valid uri | |
d41c6a53 | 167 | error = "URI {0} is not a valid URI".format(uri) |
168 | return json_response({"error": error}, status=400) | |
c33a34d4 | 169 | |
86ba4168 | 170 | client.redirect_uri = redirect_uris |
c33a34d4 | 171 | |
42dbb26a | 172 | |
4990b47c | 173 | client.save() |
c840cb66 | 174 | |
c33a34d4 | 175 | expirey = 0 if client.expirey is None else client.expirey |
176 | ||
4990b47c | 177 | return json_response( |
178 | { | |
d41c6a53 | 179 | "client_id": client.id, |
180 | "client_secret": client.secret, | |
181 | "expires_at": expirey, | |
4990b47c | 182 | }) |
d41c6a53 | 183 | |
d41c6a53 | 184 | @csrf_exempt |
185 | def request_token(request): | |
186 | """ Returns request token """ | |
187 | try: | |
42dbb26a | 188 | data = decode_request(request) |
d41c6a53 | 189 | except ValueError: |
190 | error = "Could not decode data." | |
191 | return json_response({"error": error}, status=400) | |
192 | ||
405aa45a | 193 | if data == "": |
d41c6a53 | 194 | error = "Unknown Content-Type" |
195 | return json_response({"error": error}, status=400) | |
196 | ||
2b60a56c | 197 | if not data and request.headers: |
198 | data = request.headers | |
42dbb26a | 199 | |
2b60a56c | 200 | data = dict(data) # mutableifying |
405aa45a | 201 | |
2b60a56c | 202 | authorization = decode_authorization_header(data) |
d41c6a53 | 203 | |
2b60a56c | 204 | if authorization == dict() or u"oauth_consumer_key" not in authorization: |
405aa45a | 205 | error = "Missing required parameter." |
2b60a56c | 206 | return json_response({"error": error}, status=400) |
405aa45a | 207 | |
d41c6a53 | 208 | # check the client_id |
2b60a56c | 209 | client_id = authorization[u"oauth_consumer_key"] |
d41c6a53 | 210 | client = Client.query.filter_by(id=client_id).first() |
89d5b44e | 211 | |
212 | if client == None: | |
d41c6a53 | 213 | # client_id is invalid |
214 | error = "Invalid client_id" | |
215 | return json_response({"error": error}, status=400) | |
216 | ||
89d5b44e | 217 | # make request token and return to client |
2b60a56c | 218 | request_validator = GMGRequestValidator(authorization) |
d41c6a53 | 219 | rv = RequestTokenEndpoint(request_validator) |
405aa45a | 220 | tokens = rv.create_request_token(request, authorization) |
d41c6a53 | 221 | |
cfe7054c | 222 | # store the nonce & timestamp before we return back |
223 | nonce = authorization[u"oauth_nonce"] | |
224 | timestamp = authorization[u"oauth_timestamp"] | |
89d5b44e | 225 | timestamp = datetime.datetime.fromtimestamp(float(timestamp)) |
cfe7054c | 226 | |
227 | nc = NonceTimestamp(nonce=nonce, timestamp=timestamp) | |
228 | nc.save() | |
229 | ||
2b60a56c | 230 | return form_response(tokens) |
405aa45a | 231 | |
42dbb26a | 232 | @require_active_login |
d41c6a53 | 233 | def authorize(request): |
234 | """ Displays a page for user to authorize """ | |
405aa45a | 235 | if request.method == "POST": |
236 | return authorize_finish(request) | |
42dbb26a | 237 | |
d41c6a53 | 238 | _ = pass_to_ugettext |
239 | token = request.args.get("oauth_token", None) | |
240 | if token is None: | |
241 | # no token supplied, display a html 400 this time | |
405aa45a | 242 | err_msg = _("Must provide an oauth_token.") |
d41c6a53 | 243 | return render_400(request, err_msg=err_msg) |
244 | ||
405aa45a | 245 | oauth_request = RequestToken.query.filter_by(token=token).first() |
246 | if oauth_request is None: | |
247 | err_msg = _("No request token found.") | |
248 | return render_400(request, err_msg) | |
42dbb26a | 249 | |
405aa45a | 250 | if oauth_request.used: |
251 | return authorize_finish(request) | |
42dbb26a | 252 | |
405aa45a | 253 | if oauth_request.verifier is None: |
786bbd79 | 254 | orequest = GMGRequest(request) |
405aa45a | 255 | request_validator = GMGRequestValidator() |
256 | auth_endpoint = AuthorizationEndpoint(request_validator) | |
257 | verifier = auth_endpoint.create_verifier(orequest, {}) | |
258 | oauth_request.verifier = verifier["oauth_verifier"] | |
259 | ||
260 | oauth_request.user = request.user.id | |
261 | oauth_request.save() | |
262 | ||
263 | # find client & build context | |
264 | client = Client.query.filter_by(id=oauth_request.client).first() | |
265 | ||
266 | authorize_form = AuthorizeForm(WTFormData({ | |
267 | "oauth_token": oauth_request.token, | |
268 | "oauth_verifier": oauth_request.verifier | |
269 | })) | |
270 | ||
271 | context = { | |
272 | "user": request.user, | |
273 | "oauth_request": oauth_request, | |
274 | "client": client, | |
275 | "authorize_form": authorize_form, | |
276 | } | |
277 | ||
278 | ||
d41c6a53 | 279 | # AuthorizationEndpoint |
405aa45a | 280 | return render_to_response( |
281 | request, | |
282 | "mediagoblin/api/authorize.html", | |
283 | context | |
284 | ) | |
42dbb26a | 285 | |
405aa45a | 286 | |
287 | def authorize_finish(request): | |
288 | """ Finishes the authorize """ | |
289 | _ = pass_to_ugettext | |
290 | token = request.form["oauth_token"] | |
291 | verifier = request.form["oauth_verifier"] | |
292 | oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier) | |
293 | oauth_request = oauth_request.first() | |
42dbb26a | 294 | |
405aa45a | 295 | if oauth_request is None: |
296 | # invalid token or verifier | |
297 | err_msg = _("No request token found.") | |
298 | return render_400(request, err_msg) | |
299 | ||
300 | oauth_request.used = True | |
301 | oauth_request.updated = datetime.datetime.now() | |
302 | oauth_request.save() | |
303 | ||
304 | if oauth_request.callback == "oob": | |
305 | # out of bounds | |
306 | context = {"oauth_request": oauth_request} | |
307 | return render_to_response( | |
308 | request, | |
309 | "mediagoblin/api/oob.html", | |
310 | context | |
311 | ) | |
312 | ||
313 | # okay we need to redirect them then! | |
314 | querystring = "?oauth_token={0}&oauth_verifier={1}".format( | |
315 | oauth_request.token, | |
316 | oauth_request.verifier | |
317 | ) | |
318 | ||
319 | return redirect( | |
320 | request, | |
321 | querystring=querystring, | |
322 | location=oauth_request.callback | |
323 | ) | |
d41c6a53 | 324 | |
325 | @csrf_exempt | |
326 | def access_token(request): | |
42dbb26a | 327 | """ Provides an access token based on a valid verifier and request token """ |
2b60a56c | 328 | data = request.headers |
405aa45a | 329 | |
42dbb26a | 330 | parsed_tokens = decode_authorization_header(data) |
2b60a56c | 331 | |
332 | if parsed_tokens == dict() or "oauth_token" not in parsed_tokens: | |
333 | error = "Missing required parameter." | |
405aa45a | 334 | return json_response({"error": error}, status=400) |
335 | ||
2b60a56c | 336 | |
337 | request.oauth_token = parsed_tokens["oauth_token"] | |
338 | request_validator = GMGRequestValidator(data) | |
339 | av = AccessTokenEndpoint(request_validator) | |
340 | tokens = av.create_access_token(request, {}) | |
341 | return form_response(tokens) | |
1e2675b0 | 342 |