| 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 | |
| 17 | import datetime |
| 18 | import string |
| 19 | |
| 20 | from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint, |
| 21 | AccessTokenEndpoint) |
| 22 | |
| 23 | from mediagoblin.decorators import require_active_login |
| 24 | from mediagoblin.tools.translate import pass_to_ugettext |
| 25 | from mediagoblin.meddleware.csrf import csrf_exempt |
| 26 | from mediagoblin.tools.request import decode_request |
| 27 | from mediagoblin.tools.response import (render_to_response, redirect, |
| 28 | json_response, render_400, |
| 29 | form_response) |
| 30 | from mediagoblin.tools.crypto import random_string |
| 31 | from mediagoblin.tools.validator import validate_email, validate_url |
| 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 |
| 36 | from mediagoblin.db.models import NonceTimestamp, Client, RequestToken |
| 37 | |
| 38 | # possible client types |
| 39 | CLIENT_TYPES = ["web", "native"] # currently what pump supports |
| 40 | OAUTH_ALPHABET = (string.ascii_letters.decode('ascii') + |
| 41 | string.digits.decode('ascii')) |
| 42 | |
| 43 | @csrf_exempt |
| 44 | def client_register(request): |
| 45 | """ Endpoint for client registration """ |
| 46 | try: |
| 47 | data = decode_request(request) |
| 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) |
| 55 | |
| 56 | if "type" not in data: |
| 57 | error = "No registration type provided." |
| 58 | return json_response({"error": error}, status=400) |
| 59 | if data.get("application_type", None) not in CLIENT_TYPES: |
| 60 | error = "Unknown application_type." |
| 61 | return json_response({"error": error}, status=400) |
| 62 | |
| 63 | client_type = data["type"] |
| 64 | |
| 65 | if client_type == "client_update": |
| 66 | # updating a client |
| 67 | if "client_id" not in data: |
| 68 | error = "client_id is requried to update." |
| 69 | return json_response({"error": error}, status=400) |
| 70 | elif "client_secret" not in data: |
| 71 | error = "client_secret is required to update." |
| 72 | return json_response({"error": error}, status=400) |
| 73 | |
| 74 | client = Client.query.filter_by( |
| 75 | id=data["client_id"], |
| 76 | secret=data["client_secret"] |
| 77 | ).first() |
| 78 | |
| 79 | if client is None: |
| 80 | error = "Unauthorized." |
| 81 | return json_response({"error": error}, status=403) |
| 82 | |
| 83 | client.application_name = data.get( |
| 84 | "application_name", |
| 85 | client.application_name |
| 86 | ) |
| 87 | |
| 88 | client.application_type = data.get( |
| 89 | "application_type", |
| 90 | client.application_type |
| 91 | ) |
| 92 | |
| 93 | app_name = ("application_type", client.application_name) |
| 94 | if app_name in CLIENT_TYPES: |
| 95 | client.application_name = app_name |
| 96 | |
| 97 | elif client_type == "client_associate": |
| 98 | # registering |
| 99 | if "client_id" in data: |
| 100 | error = "Only set client_id for update." |
| 101 | return json_response({"error": error}, status=400) |
| 102 | elif "access_token" in data: |
| 103 | error = "access_token not needed for registration." |
| 104 | return json_response({"error": error}, status=400) |
| 105 | elif "client_secret" in data: |
| 106 | error = "Only set client_secret for update." |
| 107 | return json_response({"error": error}, status=400) |
| 108 | |
| 109 | # generate the client_id and client_secret |
| 110 | client_id = random_string(22, OAUTH_ALPHABET) |
| 111 | client_secret = random_string(43, OAUTH_ALPHABET) |
| 112 | expirey = 0 # for now, lets not have it expire |
| 113 | expirey_db = None if expirey == 0 else expirey |
| 114 | application_type = data["application_type"] |
| 115 | |
| 116 | # save it |
| 117 | client = Client( |
| 118 | id=client_id, |
| 119 | secret=client_secret, |
| 120 | expirey=expirey_db, |
| 121 | application_type=application_type, |
| 122 | ) |
| 123 | |
| 124 | else: |
| 125 | error = "Invalid registration type" |
| 126 | return json_response({"error": error}, status=400) |
| 127 | |
| 128 | logo_url = data.get("logo_url", client.logo_url) |
| 129 | if logo_url is not None and not validate_url(logo_url): |
| 130 | error = "Logo URL {0} is not a valid URL.".format(logo_url) |
| 131 | return json_response( |
| 132 | {"error": error}, |
| 133 | status=400 |
| 134 | ) |
| 135 | else: |
| 136 | client.logo_url = logo_url |
| 137 | |
| 138 | client.application_name = data.get("application_name", None) |
| 139 | |
| 140 | contacts = data.get("contacts", None) |
| 141 | if contacts is not None: |
| 142 | if type(contacts) is not unicode: |
| 143 | error = "Contacts must be a string of space-seporated email addresses." |
| 144 | return json_response({"error": error}, status=400) |
| 145 | |
| 146 | contacts = contacts.split() |
| 147 | for contact in contacts: |
| 148 | if not validate_email(contact): |
| 149 | # not a valid email |
| 150 | error = "Email {0} is not a valid email.".format(contact) |
| 151 | return json_response({"error": error}, status=400) |
| 152 | |
| 153 | |
| 154 | client.contacts = contacts |
| 155 | |
| 156 | redirect_uris = data.get("redirect_uris", None) |
| 157 | if redirect_uris is not None: |
| 158 | if type(redirect_uris) is not unicode: |
| 159 | error = "redirect_uris must be space-seporated URLs." |
| 160 | return json_response({"error": error}, status=400) |
| 161 | |
| 162 | redirect_uris = redirect_uris.split() |
| 163 | |
| 164 | for uri in redirect_uris: |
| 165 | if not validate_url(uri): |
| 166 | # not a valid uri |
| 167 | error = "URI {0} is not a valid URI".format(uri) |
| 168 | return json_response({"error": error}, status=400) |
| 169 | |
| 170 | client.redirect_uri = redirect_uris |
| 171 | |
| 172 | |
| 173 | client.save() |
| 174 | |
| 175 | expirey = 0 if client.expirey is None else client.expirey |
| 176 | |
| 177 | return json_response( |
| 178 | { |
| 179 | "client_id": client.id, |
| 180 | "client_secret": client.secret, |
| 181 | "expires_at": expirey, |
| 182 | }) |
| 183 | |
| 184 | @csrf_exempt |
| 185 | def request_token(request): |
| 186 | """ Returns request token """ |
| 187 | try: |
| 188 | data = decode_request(request) |
| 189 | except ValueError: |
| 190 | error = "Could not decode data." |
| 191 | return json_response({"error": error}, status=400) |
| 192 | |
| 193 | if data == "": |
| 194 | error = "Unknown Content-Type" |
| 195 | return json_response({"error": error}, status=400) |
| 196 | |
| 197 | if not data and request.headers: |
| 198 | data = request.headers |
| 199 | |
| 200 | data = dict(data) # mutableifying |
| 201 | |
| 202 | authorization = decode_authorization_header(data) |
| 203 | |
| 204 | if authorization == dict() or u"oauth_consumer_key" not in authorization: |
| 205 | error = "Missing required parameter." |
| 206 | return json_response({"error": error}, status=400) |
| 207 | |
| 208 | # check the client_id |
| 209 | client_id = authorization[u"oauth_consumer_key"] |
| 210 | client = Client.query.filter_by(id=client_id).first() |
| 211 | |
| 212 | if client == None: |
| 213 | # client_id is invalid |
| 214 | error = "Invalid client_id" |
| 215 | return json_response({"error": error}, status=400) |
| 216 | |
| 217 | # make request token and return to client |
| 218 | request_validator = GMGRequestValidator(authorization) |
| 219 | rv = RequestTokenEndpoint(request_validator) |
| 220 | tokens = rv.create_request_token(request, authorization) |
| 221 | |
| 222 | # store the nonce & timestamp before we return back |
| 223 | nonce = authorization[u"oauth_nonce"] |
| 224 | timestamp = authorization[u"oauth_timestamp"] |
| 225 | timestamp = datetime.datetime.fromtimestamp(float(timestamp)) |
| 226 | |
| 227 | nc = NonceTimestamp(nonce=nonce, timestamp=timestamp) |
| 228 | nc.save() |
| 229 | |
| 230 | return form_response(tokens) |
| 231 | |
| 232 | @require_active_login |
| 233 | def authorize(request): |
| 234 | """ Displays a page for user to authorize """ |
| 235 | if request.method == "POST": |
| 236 | return authorize_finish(request) |
| 237 | |
| 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 |
| 242 | err_msg = _("Must provide an oauth_token.") |
| 243 | return render_400(request, err_msg=err_msg) |
| 244 | |
| 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) |
| 249 | |
| 250 | if oauth_request.used: |
| 251 | return authorize_finish(request) |
| 252 | |
| 253 | if oauth_request.verifier is None: |
| 254 | orequest = GMGRequest(request) |
| 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 | |
| 279 | # AuthorizationEndpoint |
| 280 | return render_to_response( |
| 281 | request, |
| 282 | "mediagoblin/api/authorize.html", |
| 283 | context |
| 284 | ) |
| 285 | |
| 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() |
| 294 | |
| 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 | ) |
| 324 | |
| 325 | @csrf_exempt |
| 326 | def access_token(request): |
| 327 | """ Provides an access token based on a valid verifier and request token """ |
| 328 | data = request.headers |
| 329 | |
| 330 | parsed_tokens = decode_authorization_header(data) |
| 331 | |
| 332 | if parsed_tokens == dict() or "oauth_token" not in parsed_tokens: |
| 333 | error = "Missing required parameter." |
| 334 | return json_response({"error": error}, status=400) |
| 335 | |
| 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) |
| 342 | |