Fixes tests
[mediagoblin.git] / mediagoblin / federation / views.py
CommitLineData
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 17import datetime
4990b47c 18
405aa45a 19from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
2b60a56c 20 RequestTokenEndpoint, AccessTokenEndpoint)
405aa45a 21
1e2675b0 22from mediagoblin.decorators import require_active_login, oauth_required
d41c6a53 23from mediagoblin.tools.translate import pass_to_ugettext
4990b47c 24from mediagoblin.meddleware.csrf import csrf_exempt
786bbd79 25from mediagoblin.tools.request import decode_request
405aa45a 26from mediagoblin.tools.response import (render_to_response, redirect,
2b60a56c 27 json_response, render_400,
28 form_response)
4990b47c 29from mediagoblin.tools.crypto import random_string
c33a34d4 30from mediagoblin.tools.validator import validate_email, validate_url
405aa45a 31from mediagoblin.federation.forms import AuthorizeForm
786bbd79 32from mediagoblin.federation.exceptions import ValidationException
33from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
34from mediagoblin.federation.tools.request import decode_authorization_header
cfe7054c 35from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
c840cb66 36
37# possible client types
38client_types = ["web", "native"] # currently what pump supports
39
4990b47c 40@csrf_exempt
c840cb66 41def 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
111
112 # save it
113 client = Client(
114 id=client_id,
115 secret=client_secret,
116 expirey=expirey_db,
117 application_type=data["application_type"],
d41c6a53 118 )
c33a34d4 119
120 else:
d41c6a53 121 error = "Invalid registration type"
122 return json_response({"error": error}, status=400)
c33a34d4 123
124 logo_url = data.get("logo_url", client.logo_url)
125 if logo_url is not None and not validate_url(logo_url):
d41c6a53 126 error = "Logo URL {0} is not a valid URL.".format(logo_url)
127 return json_response(
128 {"error": error},
129 status=400
130 )
c33a34d4 131 else:
132 client.logo_url = logo_url
405aa45a 133
134 client.application_name = data.get("application_name", None)
c33a34d4 135
136 contacts = data.get("contact", None)
137 if contacts is not None:
138 if type(contacts) is not unicode:
d41c6a53 139 error = "Contacts must be a string of space-seporated email addresses."
140 return json_response({"error": error}, status=400)
c33a34d4 141
142 contacts = contacts.split()
143 for contact in contacts:
144 if not validate_email(contact):
145 # not a valid email
d41c6a53 146 error = "Email {0} is not a valid email.".format(contact)
147 return json_response({"error": error}, status=400)
c33a34d4 148
149
150 client.contacts = contacts
151
152 request_uri = data.get("request_uris", None)
153 if request_uri is not None:
154 if type(request_uri) is not unicode:
d41c6a53 155 error = "redirect_uris must be space-seporated URLs."
156 return json_respinse({"error": error}, status=400)
c33a34d4 157
158 request_uri = request_uri.split()
159
160 for uri in request_uri:
161 if not validate_url(uri):
162 # not a valid uri
d41c6a53 163 error = "URI {0} is not a valid URI".format(uri)
164 return json_response({"error": error}, status=400)
c33a34d4 165
166 client.request_uri = request_uri
167
168
4990b47c 169 client.save()
c840cb66 170
c33a34d4 171 expirey = 0 if client.expirey is None else client.expirey
172
4990b47c 173 return json_response(
174 {
d41c6a53 175 "client_id": client.id,
176 "client_secret": client.secret,
177 "expires_at": expirey,
4990b47c 178 })
d41c6a53 179
d41c6a53 180@csrf_exempt
181def request_token(request):
182 """ Returns request token """
183 try:
184 data = decode_request(request)
185 except ValueError:
186 error = "Could not decode data."
187 return json_response({"error": error}, status=400)
188
405aa45a 189 if data == "":
d41c6a53 190 error = "Unknown Content-Type"
191 return json_response({"error": error}, status=400)
192
2b60a56c 193 if not data and request.headers:
194 data = request.headers
195
196 data = dict(data) # mutableifying
405aa45a 197
2b60a56c 198 authorization = decode_authorization_header(data)
d41c6a53 199
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()
208 if client is None:
209 # client_id is invalid
210 error = "Invalid client_id"
211 return json_response({"error": error}, status=400)
212
2b60a56c 213 # make request token and return to client
214 request_validator = GMGRequestValidator(authorization)
d41c6a53 215 rv = RequestTokenEndpoint(request_validator)
405aa45a 216 tokens = rv.create_request_token(request, authorization)
d41c6a53 217
cfe7054c 218 # store the nonce & timestamp before we return back
219 nonce = authorization[u"oauth_nonce"]
220 timestamp = authorization[u"oauth_timestamp"]
221 timestamp = datetime.datetime.fromtimestamp(int(timestamp))
222
223 nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
224 nc.save()
225
2b60a56c 226 return form_response(tokens)
405aa45a 227
228class WTFormData(dict):
229 """
230 Provides a WTForm usable dictionary
231 """
232 def getlist(self, key):
233 v = self[key]
234 if not isinstance(v, (list, tuple)):
235 v = [v]
236 return v
237
238@require_active_login
d41c6a53 239def authorize(request):
240 """ Displays a page for user to authorize """
405aa45a 241 if request.method == "POST":
242 return authorize_finish(request)
243
d41c6a53 244 _ = pass_to_ugettext
245 token = request.args.get("oauth_token", None)
246 if token is None:
247 # no token supplied, display a html 400 this time
405aa45a 248 err_msg = _("Must provide an oauth_token.")
d41c6a53 249 return render_400(request, err_msg=err_msg)
250
405aa45a 251 oauth_request = RequestToken.query.filter_by(token=token).first()
252 if oauth_request is None:
253 err_msg = _("No request token found.")
254 return render_400(request, err_msg)
255
256 if oauth_request.used:
257 return authorize_finish(request)
258
259 if oauth_request.verifier is None:
786bbd79 260 orequest = GMGRequest(request)
405aa45a 261 request_validator = GMGRequestValidator()
262 auth_endpoint = AuthorizationEndpoint(request_validator)
263 verifier = auth_endpoint.create_verifier(orequest, {})
264 oauth_request.verifier = verifier["oauth_verifier"]
265
266 oauth_request.user = request.user.id
267 oauth_request.save()
268
269 # find client & build context
270 client = Client.query.filter_by(id=oauth_request.client).first()
271
272 authorize_form = AuthorizeForm(WTFormData({
273 "oauth_token": oauth_request.token,
274 "oauth_verifier": oauth_request.verifier
275 }))
276
277 context = {
278 "user": request.user,
279 "oauth_request": oauth_request,
280 "client": client,
281 "authorize_form": authorize_form,
282 }
283
284
d41c6a53 285 # AuthorizationEndpoint
405aa45a 286 return render_to_response(
287 request,
288 "mediagoblin/api/authorize.html",
289 context
290 )
291
292
293def authorize_finish(request):
294 """ Finishes the authorize """
295 _ = pass_to_ugettext
296 token = request.form["oauth_token"]
297 verifier = request.form["oauth_verifier"]
298 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
299 oauth_request = oauth_request.first()
d41c6a53 300
405aa45a 301 if oauth_request is None:
302 # invalid token or verifier
303 err_msg = _("No request token found.")
304 return render_400(request, err_msg)
305
306 oauth_request.used = True
307 oauth_request.updated = datetime.datetime.now()
308 oauth_request.save()
309
310 if oauth_request.callback == "oob":
311 # out of bounds
312 context = {"oauth_request": oauth_request}
313 return render_to_response(
314 request,
315 "mediagoblin/api/oob.html",
316 context
317 )
318
319 # okay we need to redirect them then!
320 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
321 oauth_request.token,
322 oauth_request.verifier
323 )
324
325 return redirect(
326 request,
327 querystring=querystring,
328 location=oauth_request.callback
329 )
d41c6a53 330
331@csrf_exempt
332def access_token(request):
333 """ Provides an access token based on a valid verifier and request token """
2b60a56c 334 data = request.headers
405aa45a 335
2b60a56c 336 parsed_tokens = decode_authorization_header(data)
337
338 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
339 error = "Missing required parameter."
405aa45a 340 return json_response({"error": error}, status=400)
341
2b60a56c 342
343 request.oauth_token = parsed_tokens["oauth_token"]
344 request_validator = GMGRequestValidator(data)
345 av = AccessTokenEndpoint(request_validator)
346 tokens = av.create_access_token(request, {})
347 return form_response(tokens)
1e2675b0 348