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