Left: | ||
Right: |
OLD | NEW |
---|---|
1 # This file is part of the Juju GUI, which lets users view and manage Juju | 1 # This file is part of the Juju GUI, which lets users view and manage Juju |
2 # environments within a graphical interface (https://launchpad.net/juju-gui). | 2 # environments within a graphical interface (https://launchpad.net/juju-gui). |
3 # Copyright (C) 2013 Canonical Ltd. | 3 # Copyright (C) 2013 Canonical Ltd. |
4 # | 4 # |
5 # This program is free software: you can redistribute it and/or modify it under | 5 # This program is free software: you can redistribute it and/or modify it under |
6 # the terms of the GNU Affero General Public License version 3, as published by | 6 # the terms of the GNU Affero General Public License version 3, as published by |
7 # the Free Software Foundation. | 7 # the Free Software Foundation. |
8 # | 8 # |
9 # This program is distributed in the hope that it will be useful, but WITHOUT | 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
10 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | 10 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
11 # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | 11 # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 # Affero General Public License for more details. | 12 # Affero General Public License for more details. |
13 # | 13 # |
14 # You should have received a copy of the GNU Affero General Public License | 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/>. | 15 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 |
17 """Juju GUI server HTTP/HTTPS handlers.""" | 17 """Juju GUI server HTTP/HTTPS handlers.""" |
18 | 18 |
19 from collections import deque | 19 from collections import deque |
20 import logging | 20 import logging |
21 import os | 21 import os |
22 import time | 22 import time |
23 import urlparse | |
23 | 24 |
24 from tornado import ( | 25 from tornado import ( |
25 escape, | 26 escape, |
26 gen, | 27 gen, |
27 httpclient, | 28 httpclient, |
28 web, | 29 web, |
29 websocket, | 30 websocket, |
30 ) | 31 ) |
31 from tornado.ioloop import IOLoop | 32 from tornado.ioloop import IOLoop |
32 | 33 |
33 from guiserver import get_version | 34 from guiserver import get_version |
34 from guiserver.auth import ( | 35 from guiserver.auth import ( |
35 AuthMiddleware, | 36 AuthMiddleware, |
36 User, | 37 User, |
37 ) | 38 ) |
38 from guiserver.bundles.base import DeployMiddleware | 39 from guiserver.bundles.base import DeployMiddleware |
39 from guiserver.clients import websocket_connect | 40 from guiserver.clients import websocket_connect |
40 from guiserver.utils import ( | 41 from guiserver.utils import ( |
41 clone_request, | 42 clone_request, |
42 get_headers, | 43 get_headers, |
43 join_url, | 44 join_url, |
44 json_decode_dict, | 45 json_decode_dict, |
45 request_summary, | 46 request_summary, |
46 wrap_write_message, | 47 wrap_write_message, |
47 ) | 48 ) |
48 | 49 |
49 | 50 |
51 # Define the path to the fallback charm icon hosted by charmworld. | |
52 DEFAULT_CHARM_ICON_PATH = '/static/img/charm_160.svg' | |
53 | |
54 | |
50 class WebSocketHandler(websocket.WebSocketHandler): | 55 class WebSocketHandler(websocket.WebSocketHandler): |
51 """WebSocket handler supporting secure WebSockets. | 56 """WebSocket handler supporting secure WebSockets. |
52 | 57 |
53 This handler acts as a proxy between the browser connection and the | 58 This handler acts as a proxy between the browser connection and the |
54 Juju API server. It also handles API authentication and requests for | 59 Juju API server. It also handles API authentication and requests for |
55 bundles deployment (using the juju-deployer deployment format). | 60 bundles deployment (using the juju-deployer deployment format). |
56 | 61 |
57 Relevant attributes: | 62 Relevant attributes: |
58 | 63 |
59 - connected: True if the current browser is connected, False otherwise; | 64 - connected: True if the current browser is connected, False otherwise; |
(...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
218 | 223 |
219 @classmethod | 224 @classmethod |
220 def get_absolute_path(cls, root, path): | 225 def get_absolute_path(cls, root, path): |
221 """See tornado.web.StaticFileHandler.get_absolute_path.""" | 226 """See tornado.web.StaticFileHandler.get_absolute_path.""" |
222 return os.path.join(root, 'index.html') | 227 return os.path.join(root, 'index.html') |
223 | 228 |
224 | 229 |
225 class ProxyHandler(web.RequestHandler): | 230 class ProxyHandler(web.RequestHandler): |
226 """An HTTP(S) proxy from the server to the given target URL.""" | 231 """An HTTP(S) proxy from the server to the given target URL.""" |
227 | 232 |
228 def initialize(self, target_url): | 233 def initialize(self, target_url, validate_cert=True): |
229 """Initialize the proxy. | 234 """Initialize the proxy. |
230 | 235 |
231 Receive the target URL where to redirect to. | 236 Receive the target URL where to redirect to, and a flag indicating |
237 whether to validate remote server certificates. | |
232 """ | 238 """ |
233 self.target_url = target_url | 239 self.target_url = target_url |
240 self.validate_cert = validate_cert | |
234 | 241 |
235 @gen.coroutine | 242 @gen.coroutine |
236 def get(self, path): | 243 def get(self, path): |
237 """Handle GET requests. | 244 """Handle GET requests. |
238 | 245 |
239 Receive a path that will be used as part of the resulting URL used to | 246 Receive a path that will be used as part of the resulting URL used to |
240 retrieve the response. | 247 retrieve the response. |
241 The response will then be sent back to the client. | 248 The response will then be sent back to the client. |
242 """ | 249 """ |
243 url = join_url(self.target_url, path, self.request.query) | 250 url = join_url(self.target_url, path, self.request.query) |
244 # Server certificates are not validated: we use this function to | 251 response = yield self.send_request(url) |
245 # connect to juju-core, and we would need to obtain ca-certificates | 252 if response is not None: |
246 # from it. Unfortunately we don't have that information, and for this | 253 self.send_response(response) |
247 # reason we skip validation for both WebSocket and HTTPS connections. | 254 |
248 # This is not ideal but currently is our best option. | 255 # Handle POST requests the same way GET ones are handled. |
bac
2014/04/09 16:32:42
s/ones/requests/ -- just reads better.
frankban
2014/04/09 16:49:06
Done.
| |
249 request = clone_request(self.request, url, validate_cert=False) | 256 post = get |
257 | |
258 @gen.coroutine | |
259 def send_request(self, url): | |
260 """Send an asynchronous request to the given URL. | |
261 | |
262 Return the server response. | |
263 If an error occurs in the communication, return None and call | |
264 self._send_error with the given error. | |
265 """ | |
266 request = clone_request( | |
267 self.request, url, validate_cert=self.validate_cert) | |
250 client = httpclient.AsyncHTTPClient() | 268 client = httpclient.AsyncHTTPClient() |
251 try: | 269 try: |
252 response = yield client.fetch(request) | 270 response = yield client.fetch(request) |
253 except httpclient.HTTPError as err: | 271 except httpclient.HTTPError as err: |
254 response = getattr(err, 'response', None) | 272 response = getattr(err, 'response', None) |
255 if not response: | 273 if not response: |
256 self._send_error(url, err) | 274 self._send_error(url, err) |
257 return | 275 raise gen.Return(response) |
258 self._send_response(response) | |
259 | 276 |
260 # Handle POST requests the same way GET ones are handled. | 277 def send_response(self, response): |
261 post = get | |
262 | |
263 def _send_response(self, response): | |
264 """Prepare and send the response to the client.""" | 278 """Prepare and send the response to the client.""" |
265 self.set_status(response.code) | 279 self.set_status(response.code) |
266 set_header = self.set_header | 280 set_header = self.set_header |
267 for key, value in response.headers.items(): | 281 for key, value in response.headers.items(): |
268 set_header(key, value) | 282 set_header(key, value) |
269 body = response.body | 283 body = response.body |
270 if body: | 284 if body: |
271 self.write(body) | 285 self.write(body) |
272 | 286 |
273 def _send_error(self, url, exception): | 287 def _send_error(self, url, exception): |
274 """Send a 500 internal server error to the client.""" | 288 """Send a 500 internal server error to the client.""" |
275 msg = 'error fetching data from {}: {}'.format( | 289 msg = 'error fetching data from {}: {}'.format( |
276 url.encode('utf-8'), exception) | 290 url.encode('utf-8'), exception) |
277 logging.error(msg) | 291 logging.error(msg) |
278 self.set_status(500) | 292 self.set_status(500) |
279 self.write('Internal server error:\n{}'.format(msg)) | 293 self.write('Internal server error:\n{}'.format(msg)) |
280 | 294 |
281 | 295 |
296 class JujuProxyHandler(ProxyHandler): | |
297 """A specialized proxy handler used for the juju-core HTTP API.""" | |
298 | |
299 def initialize(self, target_url, charmworld_url): | |
300 """Initialize the proxy. | |
301 | |
302 Receive the target URL where to redirect to, and the charmworld URL | |
303 used to retrieve the default charm icon. | |
304 """ | |
305 # Server certificates are not validated: we use this handler to connect | |
306 # to juju-core, and we would need to obtain ca-certificates from it. | |
307 # Unfortunately we don't have that information, and for this reason we | |
308 # skip validation for both WebSocket and HTTPS connections. This is not | |
309 # ideal but currently is our best option. | |
310 super(JujuProxyHandler, self).initialize( | |
311 target_url, validate_cert=False) | |
312 self.default_charm_icon_url = urlparse.urljoin( | |
313 charmworld_url, DEFAULT_CHARM_ICON_PATH) | |
314 | |
315 @gen.coroutine | |
316 def get(self, path): | |
317 """Handle GET requests. | |
318 See the ProxyHandler.get method. | |
319 | |
320 Override to handle the case a when a charm icon is not found. | |
bac
2014/04/09 16:32:42
s/a when/when/ or /for when/?
frankban
2014/04/09 16:49:06
Done.
| |
321 """ | |
322 url = join_url(self.target_url, path, self.request.query) | |
323 response = yield self.send_request(url) | |
324 if response is not None: | |
325 if response.code == 404 and self._charm_icon_requested(path): | |
326 # This is a request for a charm icon file, and the icon is not | |
327 # found: redirect to the fallback icon hosted on charmworld. | |
328 self.redirect(self.default_charm_icon_url) | |
329 else: | |
330 # Return the response to the client as usual. | |
331 self.send_response(response) | |
332 | |
333 def _charm_icon_requested(self, path): | |
334 """Return True if the current request is for a charm icon.""" | |
335 return ( | |
336 # The request is for a local charm. | |
337 path == 'charms' and | |
338 # The charm URL is specified. | |
339 self.get_argument('url', None) and | |
340 # The icon file is requested. | |
341 self.get_argument('file', None) == 'icon.svg' | |
342 ) | |
343 | |
344 | |
282 class InfoHandler(web.RequestHandler): | 345 class InfoHandler(web.RequestHandler): |
283 """Return information about the GUI server.""" | 346 """Return information about the GUI server.""" |
284 | 347 |
285 def initialize(self, apiurl, apiversion, deployer, sandbox, start_time): | 348 def initialize(self, apiurl, apiversion, deployer, sandbox, start_time): |
286 """Initialize the handler.""" | 349 """Initialize the handler.""" |
287 self.apiurl = apiurl | 350 self.apiurl = apiurl |
288 self.apiversion = apiversion | 351 self.apiversion = apiversion |
289 self.deployer = deployer | 352 self.deployer = deployer |
290 self.sandbox = sandbox | 353 self.sandbox = sandbox |
291 self.start_time = start_time | 354 self.start_time = start_time |
(...skipping 19 matching lines...) Expand all Loading... | |
311 | 374 |
312 | 375 |
313 class HttpsRedirectHandler(web.RequestHandler): | 376 class HttpsRedirectHandler(web.RequestHandler): |
314 """Permanently redirect all the requests to the equivalent HTTPS URL.""" | 377 """Permanently redirect all the requests to the equivalent HTTPS URL.""" |
315 | 378 |
316 def get(self): | 379 def get(self): |
317 """Handle GET requests.""" | 380 """Handle GET requests.""" |
318 request = self.request | 381 request = self.request |
319 url = 'https://{}{}'.format(request.host, request.uri) | 382 url = 'https://{}{}'.format(request.host, request.uri) |
320 self.redirect(url, permanent=True) | 383 self.redirect(url, permanent=True) |
OLD | NEW |