blob: dd8b9bce1def6786f8e02ecfa2af6ad003eae286 [file] [log] [blame] [edit]
# Copyright 2013 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Main entry point for Swarming service.
This file contains the URL handlers for all the Swarming service URLs,
implemented using the webapp2 framework.
"""
import collections
import json
import logging
import os
import webapp2
import handlers_bot
import handlers_endpoints
import template
from components import auth
from components import utils
from server import acl
from server import bot_groups_config
from server import config
from server import hmac_secret
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
# Helper class for displaying the sort options in html templates.
SortOptions = collections.namedtuple('SortOptions', ['key', 'name'])
### is_admin pages.
class RestrictedConfigHandler(auth.AuthenticatingHandler):
@auth.autologin
@auth.require(acl.can_view_config)
def get(self):
# Template parameters schema matches settings_info() return value.
self.response.write(template.render(
'swarming/restricted_config.html', config.settings_info()))
### Redirectors.
class BotsListHandler(auth.AuthenticatingHandler):
"""Redirects to a list of known bots."""
@auth.public
def get(self):
limit = int(self.request.get('limit', 100))
dimensions = (
l.strip() for l in self.request.get('dimensions', '').splitlines()
)
dimensions = [i for i in dimensions if i]
new_ui_link = '/botlist?l=%d' % limit
if dimensions:
new_ui_link += '&f=' + '&f='.join(dimensions)
self.redirect(new_ui_link)
class BotHandler(auth.AuthenticatingHandler):
"""Redirects to a page about the bot, including last tasks and events."""
@auth.public
def get(self, bot_id):
self.redirect('/bot?id=%s' % bot_id)
class TasksHandler(auth.AuthenticatingHandler):
"""Redirects to a list of all task requests."""
@auth.public
def get(self):
limit = int(self.request.get('limit', 100))
task_tags = [
line for line in self.request.get('task_tag', '').splitlines() if line
]
new_ui_link = '/tasklist?l=%d' % limit
if task_tags:
new_ui_link += '&f=' + '&f='.join(task_tags)
self.redirect(new_ui_link)
class TaskHandler(auth.AuthenticatingHandler):
"""Redirects to a page containing task request and result."""
@auth.public
def get(self, task_id):
self.redirect('/task?id=%s' % task_id)
### Public pages.
class UIHandler(auth.AuthenticatingHandler):
"""Serves the landing page for the new UI of the requested page.
This landing page is stamped with the OAuth 2.0 client id from the
configuration.
"""
@auth.public
def get(self, page):
page = page or 'swarming'
params = {
'client_id': config.settings().ui_client_id,
}
# Can cache for 1 week, because the only thing that would change in this
# template is the oauth client id, which changes very infrequently.
self.response.cache_control.no_cache = None
self.response.cache_control.public = True
self.response.cache_control.max_age = 604800
try:
self.response.write(template.render(
'wcui/public_%s_index.html' % page, params))
except template.TemplateNotFound:
self.abort(404, 'Page %s not found.', page)
def get_content_security_policy(self):
# We use iframes to display pages at display_server_url_template. Need to
# allow it in CSP.
csp = super(UIHandler, self).get_content_security_policy()
csp['frame-src'].append("'self'")
tmpl = config.settings().display_server_url_template
if tmpl:
if not tmpl.startswith('/'):
# We assume the template specifies '%s' in its last path component.
# We strip it to get a "parent" path that we can put into CSP. Note that
# allowing an entire display server domain is unnecessary wide.
csp['frame-src'].append(tmpl[:tmpl.rfind('/')+1])
extra = config.settings().extra_child_src_csp_url
# Note that frame-src was once child-src, which was deprecated and support
# was dropped by some browsers. See
# https://bugs.chromium.org/p/chromium/issues/detail?id=839909
csp['frame-src'].extend(extra)
return csp
class WarmupHandler(webapp2.RequestHandler):
def get(self):
auth.warmup()
bot_groups_config.warmup()
hmac_secret.warmup()
utils.get_module_version_list(None, None)
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
self.response.write('ok')
class EmailHandler(webapp2.RequestHandler):
"""Blackhole any email sent."""
def post(self, to):
pass
class TempTQPlaceholderHandler(webapp2.RequestHandler):
def post(self, title):
logging.info('Task %s:\n%r', title, json.loads(self.request.body))
def get_routes():
routes = [
('/_ah/mail/<to:.+>', EmailHandler),
('/_ah/warmup', WarmupHandler),
# TODO(vadimsh): This is temporary until there's a Go handler.
('/internal/tasks/t/rbe-enqueue/<title:.+>', TempTQPlaceholderHandler),
]
if not utils.should_disable_ui_routes():
routes.extend([
# Frontend pages. They return HTML.
# Public pages.
('/<page:(botlist|tasklist|task|bot|)>', UIHandler),
# This is for https://aip.dev/122#resource-uris
('/tasks/<task_id:[0-9a-fA-F]+>', TaskHandler),
# These were the very old (pre-2016) links, so this redirects
# them to the modern url style.
('/user/tasks', TasksHandler),
('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler),
('/restricted/bots', BotsListHandler),
('/restricted/bot/<bot_id:[^/]+>', BotHandler),
# Admin pages.
# TODO(maruel): Get rid of them.
('/restricted/config', RestrictedConfigHandler),
])
return [webapp2.Route(*i) for i in routes]
def create_application(debug):
routes = []
routes.extend(get_routes())
routes.extend(handlers_bot.get_routes())
routes.extend(handlers_endpoints.get_routes())
return webapp2.WSGIApplication(routes, debug=debug)